对象
创建对象
有两种方式创建对象:
let obj = new Object(); // 构造函数语法
let obj1 = {}; // 字面量语法
属性
对象存储属性(键值对),其中:
- 属性的键必须是字符串或者 symbol(通常是字符串)
- 值可以是任何类型
访问属性:
- 点符号:
obj.property
- 方括号
obj["property"]
,方括号允许从变量中获取键
其他操作:
-
删除属性:
delete obj.prop
-
检查是否存在给定键的属性:
"key" in obj
let obj = { name: undefined } console.log(obj.name); // undefined console.log("name" in obj); // true
-
遍历对象:
for(let key in obj)
循环
属性值简写:
function makeUser(name, age) {
return {
name, // 与 name: name 相同
age, // 与 age: age 相同
sex: 'male'
};
}
对象的顺序
整数属性会被进行排序,其他属性则按照创建的顺序显示
let codes = {
"500": "error",
"200": "ok",
"404": "not found",
"101": "switch"
}
整数属性是指与一个整数进行相互转换而不发生变化的字符串
对象引用和复制
对象与原始类型的根本区别之一是,对象是通过引用存储和复制的
赋值了对象的变量存储的是对象在内存中的地址(引用)
// 原始类型复制
// 两个独立的变量:fruit 和 phone,两者都存储着字符串 'apple'
let fruit = 'apple';
let phone = fruit;
// 对象复制
// 变量 man 中存储的实际上是对象 { name: 'Tom' } 的地址
let man = { name: 'Tom' };
let woman = man; // 将该对象的地址赋值给变量 woman,两个变量都指向同一个对象
通过 const
声明的对象是可以被修改的:
const fruit = {
name: 'apple'
};
fruit.name = 'orange';
console.log(fruit.name); // orange
通过引用来比较
两个变量都引用同一对象:
let obj = {};
let obj1 = obj;
console.log(obj === obj1); // true
两个独立的对象:
let obj = {};
let obj1 = {};
console.log(obj === obj1); // false
浅拷贝和深拷贝
假如有一个对象被赋值给了一个变量,直接将这个变量赋值给另一个变量,由于变量存储的是对象的引用,改变一个变量的值另一个变量也会跟着改变
浅拷贝
要复制对象可以创建一个新对象,通过遍历已有对象的属性,并在原始类型值的层面复制它们:
let man = {
name: 'Tom',
age: 3
};
let woman = {};
for (let key in man) {
woman[key] = man[key];
}
woman.name = 'Jerry';
// man 和 woman 互相独立
console.log(man.name); // Tom
console.log(woman.name); // Jerry
也可以使用 Object.assign()
方法,将所有源对象的属性拷贝到目标对象中:
// 拷贝 srcObj1, srcObj2 的属性到 targetObj 中并返回 targetObj
Object.assign(targetObj, srcObj1, srcObj2, ...);
如果被拷贝的属性名已经存在则会覆盖:
let man = { name: 'Tom' };
let woman = Object.assign(man, { name: 'Jerry' }, { age: 3 });
console.log(man); // { name: 'Jerry', age: 3 }
console.log(woman); // { name: 'Jerry', age: 3 }
console.log(man === woman); // true
可以用 Object.assign
代替 for..in
循环来进行简单克隆:
let man = { name: 'Tom', age: 3 };
let woman = Object.assign({}, man);
man.woman = 'Jerry';
console.log(man); // { name: 'Tom', age: 3 }
console.log(woman); // { name: 'Jerry', age: 3 }
深拷贝
对象的属性也可以是对其他对象的引用,如果一个对象的属性不全是原始类型,那么想要复制一个对立的对象,通过之前的方式已经行不通了
需要递归遍历对象的属性,如果是对象,则要复制它的结构
let man = {
name: 'Tom',
age: 3,
friend: {
name: 'Jerry',
age: 2
}
};
// 如果不使用深拷贝
let cloneMan = Object.assign({}, man);
cloneMan.friend.name = 'Jack';
console.log(man.friend.name); // 'Jack'
console.log(cloneMan.friend.name); // 'Jack'
// 简单实现深拷贝
function deepClone(obj) {
let newObj = {};
for (let key in obj) {
// 判断属性值是否为对象
if (typeof obj[key] === 'object') {
newObj[key] = deepClone(obj[key]);
} else {
// 将原始类型直接复制
newObj[key] = obj[key];
}
}
return newObj;
}
let cloneMan = deepClone(man);
cloneMan.friend.name = 'Jack';
console.log(man.friend.name); // Jerry
console.log(cloneMan.friend.name); // Jack
垃圾回收
js 中的垃圾回收是自动完成的,不能强制执行或是阻止执行
可达性
js 中主要的内存管理概念:可达性(Reachability)
可达值是指那些以某种方式可访问或可用的值,它们被存储在内存中:
- 当前执行的函数,它的局部变量和参数
- 当前嵌套调用链上的其他函数、它们的局部变量和参数
- 全局变量
- (还有一些其他的,内部实现)
这些值被称作 根(roots)
如果一个值可以从根通过引用或者引用链进行访问,则认为该值是可达的
let man = {
name: 'Tom'
};
// 如果 man 的值被重写了,man 对 { name: 'Tom'} 对象的引用就没了
man = null;
内部算法
js 引擎中有一个垃圾回收器在后台执行,它监控着所有对象的状态,并删除掉那些已经不可达的
垃圾回收的基本算法被称为 mark-and-sweep
垃圾回收器会定期执行以下“垃圾回收”步骤:
-
标记所有的根
-
遍历并“标记”来自它们的所有引用
-
遍历标记的对象并标记它们的引用。所有被遍历到的对象都会被记住,以免将来再次遍历到同一个对象
……直到所有可达的(从根部)引用都被访问到,没有被标记的对象都会被删除
当对象是可达状态时,它一定是存在于内存中的
this
作为对象属性的函数称为方法:
let man = {
name: 'Tom',
age: 3,
eat: function () {
console.log('eat mouse');
},
// 也可以简写,推荐
sayHi() {
console.log('hello');
}
};
this
的值是在代码运行时计算出来的,它取决于代码上下文,通过点符号被作为方法调用,this 的值就是点符号前的对象
let man = {
name: 'Tom',
age: 3,
sayName() {
console.log(this.name); // this 就是当前调用 sayName 方法的 man
}
};
man.sayName();
在没有对象的情况下:
严格模式:this
为 undefined
非严格模式:this
将会是 全局对象(浏览器中为 window)
箭头函数中的 this
箭头函数没有自己的 this,如果在箭头函数中引用 this,this 的值取决于外部非箭头函数:
let man = {
name: 'Tom',
sayName: () => {
console.log(this.name); // 严格模式下为 undefined
}
};
man.sayName();
构造器和操作符 new
构造器约定以大写字母开头并只能通过 new 操作符创建,**除了箭头函数 **任何函数都可以通过 new 创建,主要用于重复创建对象
function Man(name, age) {
this.name = name;
this.age = age;
}
let tom = new Man('Tom', 3);
当一个函数被使用 new
操作符执行时,它按照以下步骤:
- 一个新的空对象被创建并分配给
this
- 函数体执行。通常它会修改
this
,为其添加新的属性 - 返回
this
的值
function Man(name, age) {
// this = {};(隐式创建)
// 添加属性到 this
this.name = name;
this.age = age;
// return this;(隐式返回)
}
如果没有参数,可以省略 new
后的括号:
let date = new Date;
在一个函数内部,可以使用 new.target
属性来检查它是否被使用 new
进行调用了:
function Man(name) {
if (!new.target) {
return new Man(name);
}
this.name = name;
}
let man = Man('Tom');
console.log(man.name); // 'Tom'
不建议省略 new 关键字
如果一个函数返回一个对象,那么 new
返回那个对象而不是 this
:
let obj = {};
function A() {
return obj;
}
function B() {
return obj;
}
let a = new A();
let b = new B();
console.log(a == b); // true
new function() {}
// 创建一个函数并立即使用 new 调用
let man = new (function () {
this.name = 'Tom';
this.age = 3;
})();
这个构造函数不能被再次调用,因为它不保存在任何地方,只是被创建和调用
可选链 ?.
可选链 ?.
不是一个运算符,而是一个特殊的语法结构,用于安全地访问嵌套对象属性
有三种语法形式:
obj?.prop
—— 如果obj
存在则返回obj.prop
,否则返回undefined
obj?.[prop]
—— 如果obj
存在则返回obj[prop]
,否则返回undefined
obj.method?.()
—— 如果obj.method
存在则调用obj.method()
,否则返回undefined
let user = {
name: 'Jack',
address: {
province: 'province',
'local-street': 'local street'
},
sayHi() {
console.log('hi');
}
};
console.log(user.address?.province); // 'province'
console.log(user.address?.['local-street']); // 'local street'
user.sayHi?.(); // 'hi'
// 以前的方式
user.address && user.address.province
symbol
只有两种原始类型可以用作对象属性键:
- 字符串类型
- symbol 类型
如果使用了别的原始类型作为对象属性,会被自动转换为字符串,但 symbol 类型不会:
let obj = {
13: 'number', // obj['13']
true: 'boolean' // obj['true']
};
let id = Symbol('id');
alert(id); // Error: Cannot convert a Symbol value to a string
alert(id.toString()); // 'Symbol(id)'
// 或者获取 symbol.description 属性,只显示描述
alert(id.description); // 'id'
symbol 是带有可选描述的“原始唯一值”,可以通过 Symbol() 创建:
let id = Symbol();
let id1 = Symbol('id'); // id 是可选的
let id2 = Symbol('id');
console.log(id1 === id2); // false
“隐藏”属性
如果要使用第三方库的对象,添加或修改属性,为了不影响库里面的东西,可以使用 symbol 给对象添加”隐藏“属性:
// 如果这个 user 是第三方库暴露出来的对象
user = { name: 'Tom' };
// 通过 symbol 这种方式是不会对 user 里面的属性产生影响的
let name = Symbol('name');
user[name] = 'Jerry';
// 但如果是这种方式就会直接修改 user 里面的 name 了
// user.name = 'Jerry'
console.log(user); // { name: 'Tom', [Symbol(name)]: 'Jerry' }
如果要在对象字面量 {...}
中使用 symbol,则需要使用方括号把它括起来:
let id = Symbol("id");
let user = {
name: "Tom",
[id]: 1
};
for...in 和 Object.keys() 不会遍历 symbol 属性
但是 Object.assign() 会同时复制字符串和 symbol 属性:
let id = Symbol("id");
let user = {
[id]: 123
};
let clone = Object.assign({}, user);
alert( clone[id] ); // 123
全局 symbol
通常所有的 symbol 都是不同的,即使他们有相同的名字。但有时需要相同名字的 symbol 具有相同的实体,就需要使用全局 symbol 注册表
使用 Symbol.for(key)
从注册表中读取(不存在则创建)symbol:
// 从全局注册表中读取
let id = Symbol.for('id'); // 如果该 symbol 不存在,则创建它
let id2 = Symbol.for('id'); // 再次读取
console.log(id === id2); // true
相反,使用 Symbol.keyFor(sym) 通过全局 symbol 返回一个名字:
let sym = Symbol.for('id'); // 通过 name 获取 symbol
let symName = Symbol.keyFor(sym); // 通过 symbol 获取 name
console.log(sym); // Symbol(id)
console.log(symName); // id
系统 symbol
js 使用了许多系统 symbol,可以使用 Symbol.*
访问,可以使用它们来改变一些内建行为如使用 Symbol.iterator
来进行 迭代 操作,使用 Symbol.toPrimitive
来设置 对象原始值的转换 等等
有一个内建方法
Object.getOwnPropertySymbols(obj)
可以获取所有的 symbol还有一个 Reflect.ownKeys(obj) 方法可以返回一个对象的 所有 键,包括 symbol
对象类型转换
对象在参与运算时会被自动转换为原始值,然后对这些原始值进行运算,并得到运算结果(也是一个原始值)
有三种转换规则,也被成为 "hint":
- ”string“:对象到字符串的转换
- ”number“:对象到数字的转换
- ”default“:当运算符“不确定”期望值的类型时
二元加法
+
可用于字符串(连接),也可以用于数字(相加),“不确定”期望值,使用"default"
hint如果对象被用于与字符串、数字或 symbol 进行
==
比较,这时到底应该进行哪种转换也不是很明确,因此使用"default"
hint
<
和>
这种比较运算符,虽然和二元加一样也可以同时用于字符串和数字,但是他们使用 “number” hint,而不是 “default”(历史原因)
为了进行转换,js 会尝试查找并调用三个对象方法:
Symbol.toPrimitive(hint)
,如果该方法存在- 否则,如果 hint 是 "string",尝试调用
obj.toString()
或obj.valueOf()
,优先调用 obj.toString(),若两种方法都不存在 - 否则,如果 hint 是 "number",尝试调用
obj.valueOf()
或obj.toString()
,优先调用valueOf
方法
这些方法必须返回一个原始值,而不是对象
let man = {
name: 'Tom',
age: 3,
[Symbol.toPrimitive](hint) {
console.log('hint:', hint);
return hint === 'string' ? this.name : this.age;
}
};
console.log(String(man)); // hint: string Tom
console.log(+man); // hint: number 3
console.log(man + 1); // hint: default 4
默认情况下,普通对象具有 toString
和 valueOf
方法:
toString
方法返回一个字符串"[object Object]"
valueOf
方法返回对象自身
let man = {
name: 'Tom',
age: 3,
toString() {
return this.name;
},
valueOf() {
return this.age;
}
};
console.log(String(man)); // Tom
console.log(+man); // 3,
console.log(man + 1); // 4
实际开发通常只实现 obj.toString()
作为字符串转换的“全能”方法就够了:
let man = {
name: 'Tom',
toString() {
return this.name;
}
};
console.log(String(man)); // 'Tom'
console.log(+man); // NaN,首先尝试将对象转换为原始值,然后再参与运算
console.log(man + 250); // 'Tom250'