JavaScript对象 、堆与栈

本文深入探讨JavaScript对象的创建、属性访问、方法、引用复制以及堆栈内存模型。堆栈中,原始数据类型存储在栈中,而引用数据类型存储在堆中。对象作为引用数据类型,其属性可以是任何类型,可通过点符号或方括号访问。对象方法是作为对象属性的函数。使用`for...in`循环遍历对象属性。对象复制时,原始类型按值复制,对象则按引用复制。深拷贝可以使用`Object.assign`或第三方库如lodash的`_.cloneDeep`方法。

堆与栈

栈:原始数据类型(Undefined,Null,Boolean,Number、String) 
堆:引用数据类型(对象、数组和函数)

两种类型的区别是: 区别:
  基础数据类型的数据存储在栈中,变量直接指向的是基础数据类型的值。

  引用数据类型的数据存储在堆中,变量指向的是引用数据类型的地址。

  比较:

   基本数据类型比较时,比较值。

   而引用数据类型比较时,比较内存地址,如果内存地址相同,指向了同一个对象,则相等,否则不相等。
原始数据类型直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储;
引用数据类型存储在堆(heap)中的对象,占据空间大、大小不固定,如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其
在栈中的地址,取得地址后从堆中获得实体

JavaScript 对象

对象是指一个具体的事物。万物皆对象。

一个具体的事物一般都会有行为和特征。

对象可以通过花括号 {…} 和其中包含一些可选的属性来创建。属性是一个键值对,键是一个字符串(也叫做属性名),值可以是任何类型。

我们可以把对象想象成存放文件的橱柜。文件按照他们的名字来排列。这样根据文件名我们就很容易找到、添加或删除一个文件了。

对象的分类

  1. 内建对象

    由ES标准定义的对象,在任何ES实现中都可以使用 ,

    js提供的对象 Math String Number Boolean Function Object

  2. 宿主对象

    主要有浏览器提供的对象。

​ BOM DOM

  1. 自定义对象

开发人员自己定义的对象。

创建一个对象

我们可以用下面两种语法的任一种来创建一个空的对象(“空柜子”):

let user = new Object(); // “构造函数” 的语法
let user = {};  // “字面量” 的语法

通常,我们用花括号。这种方式我们叫做字面量

对象属性

可以说 “JavaScript 对象是变量的容器”。

但是,我们通常认为 “JavaScript 对象是键值对的容器”。

键值对通常写法为 name : value (键与值以冒号分割)。

键值对在 JavaScript 对象通常称为 对象属性

我们可以在创建的时候立即给对象一些属性,在 {...} 里面放置一些键值对。

let user = {     // 一个对象
  name: "John",  // 键 "name",值 "John"
  age: 30        // 键 "age",值 30
};

属性有键(或者也可以叫做名字,标识符),在冒号的前面 ":",值在冒号的右边。

user 对象中, 有两个属性:

  1. 第一个的键是 "name" 值是 "John"
  2. 第二个的键是 "age" 值是 30

是一个陈列着标记有两个 “name” 和 “age” 签名文件的橱柜。

访问对象属性

  1. 对象名.属性名
// 读取文件的属性:
alert( user.name ); // John
alert( user.age ); // 30
  1. 对象名[‘属性名’]
person["lastName"];

修改对象的属性值;

  1. 对象名.属性=值;
 man.age = 222;
  1. 对象名[‘属性名’]=值

    man['age'] = 222
    

对象方法

对象的方法定义了一个函数,并作为对象的属性存储。

对象方法通过添加 () 调用 (作为一个函数)。

你可以使用以下语法创建对象方法:

skill : function() {
    // 代码 
}

访问对象方法

hero.skill();

移除对象的属性

移除一个属性,我们用 delete 操作:

delete user.age;

检查属性是否存在

console.log( key in obj ) //返回一个布尔值,key是属性名 obj对象名   

计算属性

我们可以在对象字面量中使用方括号。这叫做计算属性

例如:

let fruit = prompt("Which fruit to buy?", "apple");

let bag = {
  [fruit]: 5, // 属性名从 fruit 变量中计算
};

alert( bag.apple ); // 5 如果 fruit="apple"

计算属性的含义很简单:[fruit] 含义是这个值从 fruit 变量中获取。

所以,如果一个人输入 "apple"bag 将是 {apple: 5}

本质上,这跟下面的语法相同:

let fruit = prompt("Which fruit to buy?", "apple");
let bag = {};

// 从 fruit 变量中获取值
bag[fruit] = 5;

…但是看起来好多了。

我们在方括号中可以用更复杂的表达式:

let fruit = 'apple';
let bag = {
  [fruit + 'Computers']: 5 // bag.appleComputers = 5
};

方括号比点符号更强大。它允许任何属性名和变量,但写起来也更加麻烦。

大部分时间里,当属性名是已知且简单的时候,用点方法。如果有一些复杂的操作,那么就用方括号。

保留字段可以用作属性名

变量名不能用保留字段,像:“for”, “let”, “return” 等。

对于对象的属性,没有这些限制,都可以的:

let obj = {
  for: 1,
  let: 2,
  return: 3
}

alert( obj.for + obj.let + obj.return );  // 6

基本上,什么都可以,只有一个特殊的:"__proto__" 因为历史原因要特别对待。比如,我们不能把它设置为非对象的值:

let obj = {};
obj.__proto__ = 5;
alert(obj.__proto__); // [object Object],这样不行

我们从代码中可以看出来,把它赋值成 5 被忽略了。

如果我们蓄意去存储随机的键值对或者允许一个访问者去指定键,那可能就会产生很多 bug 并且使对象变得危险。

比如,访问者可能选择 “proto” 作为键,这个赋值的逻辑就失败了(像上面那样)。

有一种让对象把 __proto__ 作为属性的方法,在后面章节会讲到,现在我们先来学习对象的更多知识。 还有另外一种数据结构 Map,我们会在后面章节学到,它支持任意的键值。

for…in 循环遍历对象

为了使用对象所有的属性,就可以利用 for..in 循环。这跟 for(;;) 是完全不一样的东西。

语法:

for(key in object) {
  // 各个属性键值的执行区
}

例如,我们列出 user 所有的属性值:

let user = {
  name: "John",
  age: 30,
  isAdmin: true
};

for(let key in user) {
  // keys
  alert( key );  // name, age, isAdmin
  // 属性键的值
  alert( user[key] ); // John, 30, true
}

注意,所有的 “for” 都允许我们在循环中定义变量,像 let key 这样。

同样,我们可以用其他属性名来代替 key。例如 "for(let prop in obj)" 也很常用。

像对象一样排序

对象有顺序吗?换句话说,如果我们遍历一个对象,我们会按照赋值属性的顺序来获得属性吗?这靠谱吗?

简短的回答是:”有特别的顺序“:整数属性有顺序,其他是按照创建的顺序,细节如下:

例如,让我们考虑一个带有电话号码的对象:

let codes = {
  "49": "Germany",
  "41": "Switzerland",
  "44": "Great Britain",
  // ..,
  "1": "USA"
};

for(let code in codes) {
  alert(code); // 1, 41, 44, 49
}

对象可用于向用户建议选项列表。如果我们的网站主要面向德国用户,可能想让 49 来当做第一个。

然而如果我们执行代码,会看到完全不同的景象:

  • USA (1) 在最前面
  • 然后是 Switzerland (41) 以及其它内容

因为这些电话号码是整数,所以它们以升序来排列。所以我们看到的是 1, 41, 44, 49

整数属性?那是什么?

这里的“整数属性”术语指的是一个字符串,可以在不改变的情况下对整数进行转换。

所以,“49” 是一个整数属性名,因为我们把它转换成整数,再转换回来,它还是一样。但是 “+49” 和 “1.2” 就不行了:

// Math.trunc 是内置的去除小数点的方法。
alert( String(Math.trunc(Number("49"))) ); // "49",同样,整数属性
alert( String(Math.trunc(Number("+49"))) ); // "49",不同于 "+49" ⇒ 不是整数属性
alert( String(Math.trunc(Number("1.2"))) ); // "1",不同于 "1.2" ⇒ 不是整数属性

…另外一边,如果属性名不是整数,那它们就按照创建时候的顺序来排序

let user = {
  name: "John",
  surname: "Smith"
};
user.age = 25; // 增加一个

// 非整数属性是按照创建的顺序来排列的。
for (let prop in user) {
  alert( prop ); // name, surname, age
}

所以,这就解决了电话号码的问题,我们把整数属性转换成非整数的,在前面增加一个 "+" 就行了。

像这样:

let codes = {
  "+49": "Germany",
  "+41": "Switzerland",
  "+44": "Great Britain",
  // ..,
  "+1": "USA"
};

for(let code in codes) {
  alert( +code ); // 49, 41, 44, 1
}

现在跟预想的一样了。

引用复制

对象和其他原始的类型相比有一个很重要的区别,对象都是按引用存储复制的。

原始类型是:字符串,数字,布尔类型 – 是被整个赋值的。

例如:

let message = "Hello!";
let phrase = message;

结果是我们得到了不同的值,每个存的都是 "Hello!"

对象跟这个不一样。

变量存储的不是对象本身,而是对象的“内存地址”,是对象的引用。

下面是对象的存储结构图:

let user = {
  name: "John"
};

在这里,对象存在内存里面。user 有一个对它的引用。

当对象被复制的时候 – 引用被复制了一份, 对象并没有被复制。

我们想象对象是一个抽屉,变量是一个钥匙,拷贝对象复制了钥匙,但是并没有复制抽屉本身。

例如:

let user = { name: "John" };

let admin = user; // 复制引用

现在我们有了两个变量,但是都指向同一个对象:

我们可以用任何变量去获取抽屉内容,改变它的内容:

let user = { name: 'John' };

let admin = user;

admin.name = 'Pete'; //  改变 "admin" 的引用

alert(user.name); // 'Pete', changes are seen from the "user" reference

上面的例子展示了只存在一个对象,就像我们的抽屉有两把钥匙,如果一个钥匙(admin)去使用了抽屉,稍后使用另外一个钥匙(user)打开的时候,就会看到有变化。

比较引用

等号 == 和严格等 === 对于对象来说没差别。

当两个引用指向同一个对象的时候他们相等。

例如,两个引用指向同一个对象,他们相等:

let a = {};
let b = a; // 复制引用

alert( a == b ); // true,两个变量指向同一个对象
alert( a === b ); // true

如果是两个不同的属性,他们就不相等,即使都是空的。

let a = {};
let b = {}; // 两个独立的对象

alert( a == b ); // false

如果比较两个对象 obj1 > obj2 或者用一个对象比较原始值 obj == 5,对象被转换成原始值。我们不久就会学习到对象的转化是如何实现的,但是事实上,上面的比较真的极少用到,要不就是你代码写错了。

常量对象

一个被 const 修饰的对象可以被修改。

例如:

const user = {
  name: "John"
};

user.age = 25; // (*)

alert(user.age); // 25

看起来好像 (*) 这行会报错,但是不是的,这完全没问题。这是因为 const 仅仅修饰 user。在这里 user 始终存储的都是同一个对象的引用。引用的地址没有变,只是引用的对象被修改了。

如果你想把 user 赋值给其他的什么,那就会报错了,例如:

const user = {
  name: "John"
};

// 错误(不能再给 User 赋值)
user = {
  name: "Pete"
};

…那么我们应该怎么样创建不可变的对象属性呢?如果想让 user.age = 25 这样的赋值报错呢。这也是可以的,

复制和合并,Object.assign

复制一个对象的变量也等同于创建了此对象的另一个引用。

那么我们该怎么复制一个对象呢?创建一份独立的拷贝,一份复制?

如果我们真的想这么做,就需要创建一个新的对象,遍历现有对象的属性,在原始值的状态下复制给新的对象。

像这样:

let user = {
  name: "John",
  age: 30
};

let clone = {}; // 新的空对象

// 复制所有的属性值
for (let key in user) {
  clone[key] = user[key];
}

// 现在复制是独立的复制了
clone.name = "Pete"; // 改变它的值

alert( user.name ); // 原对象属性值不变

我们也可以用[Object.assign](javascript:if(confirm(‘https://developer.mozilla.org/zh/docs/Web/JavaScript/Reference/Global_Objects/Object/assign \n\n���ļ��޷��� Teleport Ultra ����, ��Ϊ ����һ�����·���ⲿ������Ϊ������ʼ��ַ�ĵ�ַ�� \n\n�����ڷ������ϴ���?’))window.location=‘https://developer.mozilla.org/zh/docs/Web/JavaScript/Reference/Global_Objects/Object/assign’) 来实现。

语法是:

Object.assign(dest[, src1, src2, src3...])
  • 参数 destsrc1, ..., srcN(可以有很多个)是对象。
  • 这个方法复制了 src1, ..., srcN 的所有对象到 dest。换句话说,从第二个参数开始,所有对象的属性都复制给了第一个参数对象,然后返回 dest

例如,我们可以用这个方法来把几个对象合并成一个:

let user = { name: "John" };

let permissions1 = { canView: true };
let permissions2 = { canEdit: true };

// 把 permissions1 和 permissions2 的所有属性都拷贝给 user
Object.assign(user, permissions1, permissions2);

// 现在 user = { name: "John", canView: true, canEdit: true }

如果接收的对象(user)已经有了同样属性名的属性,前面的会被覆盖:

let user = { name: "John" };

// 覆盖 name,增加 isAdmin
Object.assign(user, { name: "Pete", isAdmin: true });

// 现在 user = { name: "Pete", isAdmin: true }

我们可以用 Object.assign 来代理简单的复制方法:

let user = {
  name: "John",
  age: 30
};

let clone = Object.assign({}, user);

它复制了 user 对象所有的属性给了一个空对象,然后返回拷贝后的对象。事实上,这跟循环赋值一样,但是更短。

直到现在,我们是假设所有的 user 属性都是原始值,但是如果对象属性指向对象呢?

像这样:

let user = {
  name: "John",
  sizes: {
    height: 182,
    width: 50
  }
};

alert( user.sizes.height ); // 182

现在,并不能拷贝 clone.sizes = user.sizes,因为 user.sizes 是一个对象,它按引用拷贝。所以 cloneuser 共享了一个对象。

像这样:

let user = {
  name: "John",
  sizes: {
    height: 182,
    width: 50
  }
};

let clone = Object.assign({}, user);

alert( user.sizes === clone.sizes ); // true,同一个对象

// user 和 clone 共享 sizes 对象
user.sizes.width++;       // 在这里改变一个属性的值
alert(clone.sizes.width); // 51,在这里查看属性的值

为了解决上面的的问题,我们在复制的时候应该检查 user[key] 的每一个值,如果是一个对象,我们再复制一遍这个对象,这叫做深拷贝。

有一个标准的深拷贝算法,解决上面和一些更复杂的情况,叫做 [Structured cloning algorithm](javascript:if(confirm(‘https://w3c.github.io/html/infrastructure.html \n\n���ļ��޷��� Teleport Ultra ����, ��Ϊ ����һ�����·���ⲿ������Ϊ������ʼ��ַ�ĵ�ַ�� \n\n�����ڷ������ϴ���?’))window.location=‘https://w3c.github.io/html/infrastructure.html#internal-structured-cloning-algorithm’)。为了不重复造轮子,我们使用它的一个 JS 实现的库 [lodash](javascript:if(confirm(‘https://lodash.com/ \n\n���ļ��޷��� Teleport Ultra ����, ��Ϊ ����һ�����·���ⲿ������Ϊ������ʼ��ַ�ĵ�ַ�� \n\n�����ڷ������ϴ���?’))window.location=‘https://lodash.com/’), 方法名叫做 [_.cloneDeep(obj)](javascript:if(confirm(‘https://lodash.com/docs \n\n���ļ��޷��� Teleport Ultra ����, ��Ϊ ����һ�����·���ⲿ������Ϊ������ʼ��ַ�ĵ�ַ�� \n\n�����ڷ������ϴ���?’))window.location=‘https://lodash.com/docs#cloneDeep’)。

总结

对象是具有一些特殊特性的关联数组。

他们存储键值对:

  • 属性的键必须是字符串或者符号(通常是字符串)。
  • 值可以是任何类型。

我们可以用下面的方法获取属性:

  • 点符号: obj.property
  • 方括号 obj["property"],方括号中可以使用变量 obj[varWithKey]

其他操作:

  • 删除属性:delete obj.prop
  • 检查属性是否存在:"key" in obj
  • 遍历对象:for(let key in obj) 循环。

对象根据引用来赋值或者复制。换句话说,变量存的不是对象的"值",而是值的 “引用”(内存地址)。 所以复制变量或者传递变量到方法中只是复制了对象的引用。 所有的引用操作(像增加,删除属性)都作用于同一个对象。

深拷贝的话我们可以使用 Object.assign 或者 [_.cloneDeep(obj)](javascript:if(confirm(‘https://lodash.com/docs \n\n���ļ��޷��� Teleport Ultra ����, ��Ϊ ����һ�����·���ⲿ������Ϊ������ʼ��ַ�ĵ�ַ�� \n\n�����ڷ������ϴ���?’))window.location=‘https://lodash.com/docs#cloneDeep’)。

我们在这一章学习的叫做“基本对象” — 对象。

JavaScript 中还有很多其他类型的对象:

  • Array 存储有序数据集合。
  • Date 存储时间日期。
  • Error 存储错误信息
    bj[“property”],方括号中可以使用变量obj[varWithKey]`。

其他操作:

  • 删除属性:delete obj.prop
  • 检查属性是否存在:"key" in obj
  • 遍历对象:for(let key in obj) 循环。

对象根据引用来赋值或者复制。换句话说,变量存的不是对象的"值",而是值的 “引用”(内存地址)。 所以复制变量或者传递变量到方法中只是复制了对象的引用。 所有的引用操作(像增加,删除属性)都作用于同一个对象。

深拷贝的话我们可以使用 Object.assign 或者 [_.cloneDeep(obj)](javascript:if(confirm(‘https://lodash.com/docs \n\n���ļ��޷��� Teleport Ultra ����, ��Ϊ ����һ�����·���ⲿ������Ϊ������ʼ��ַ�ĵ�ַ�� \n\n�����ڷ������ϴ���?’))window.location=‘https://lodash.com/docs#cloneDeep’)。

我们在这一章学习的叫做“基本对象” — 对象。

JavaScript 中还有很多其他类型的对象:

  • Array 存储有序数据集合。
  • Date 存储时间日期。
  • Error 存储错误信息
  • …等
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值