let user = new Object(); // “构造函数” 的语法
let user = {}; // “字面量” 的语法
//我们可以用 delete 操作符移除属性:
delete user.age;
//我们也可以用多字词语来作为属性名,但必须给它们加上引号:
let user = {
name: "John",
age: 30,
"likes birds": true // 多词属性名必须加引号
};
方括号
对于多词属性,点操作就不能用了:
// 这将提示有语法错误
user.likes birds = true
JavaScript 理解不了。它认为我们在处理 user.likes,然后在遇到意外的 birds 时给出了语法错误。
点符号要求 key 是有效的变量标识符。这意味着:不包含空格,不以数字开头,也不包含特殊字符(允许使用 $ 和 _)。
⚠️ 点符号不能以类似的方式使用:
let user = {
name: "John",
age: 30
};
let key = "name";
alert( user.key ) // undefined
属性名称限制
对象的属性名可以是保留字
🤔 属性名可以是任何字符串或者 symbol。其他类型会被自动地转换为字符串
let obj = {
0: "test" // 等同于 "0": "test"
};
// 都会输出相同的属性(数字 0 被转为字符串 "0")
alert( obj["0"] ); // test
alert( obj[0] ); // test (相同的属性)
属性存在性判断(in)
只提一点,这个属性存的值就是undefined,此时in
判断是true
遍历对象的顺序
TIP
整数属性
会被进行排序,其他属性则按照创建的顺序显示
let codes = {
"49": "Germany",
"41": "Switzerland",
"44": "Great Britain",
// ..,
"1": "USA"
};
for(let code in codes) {
alert(code); // 1, 41, 44, 49
}
注意这里的整数属性,例如1.2,+88都是不行的
对象与基本数据类型的区别
- 复制上 基本数据类型复制真的会在内存再创建一个一样的数据,而对象复制只是复制引用
- 赋值上 赋值了对象的变量存储的不是对象本身,而是该对象“在内存中的地址” —— 换句话说就是对该对象的“引用”。
对象的比较
仅当两个对象为同一对象时,两者才相等。
let a = {};
let b = {}; // 两个独立的对象
alert( a == b ); // false
浅拷贝
可以循环,assign,扩展运算符
对象中的函数
对象中的函数也被叫做方法
// 方法简写看起来更好,对吧?
let user = {
sayHi() { // 与 "sayHi: function(){...}" 一样
alert("Hello");
}
};
方法中的 “this”
通常,对象方法需要访问对象中存储的信息才能完成其工作。
为了访问该对象,方法中可以使用 this 关键字。
“this” 不受限制
JavaScript 中的 this 可以用于任何函数,即使它不是对象的方法。
function sayHi() {
alert( this.name );
}
this 的值是在代码运行时计算出来的,它取决于代码上下文。
这个规则很简单:如果 obj.f() 被调用了,则 this 在 f 函数调用期间是 obj。
🚀 在没有对象的情况下调用:this == undefined
我们甚至可以在没有对象的情况下调用函数:
function sayHi() {
alert(this);
}
sayHi(); // undefined
在这种情况下,严格模式下的 this 值为 undefined。如果我们尝试访问 this.name,将会报错。
在非严格模式的情况下,this 将会是 全局对象(浏览器中的 window,我们稍后会在 全局对象 一章中学习它)。这是一个历史行为,"use strict" 已经将其修复了。
通常这种调用是程序出错了。如果在一个函数内部有 this,那么通常意味着它是在对象上下文环境中被调用的。
箭头函数没有自己的 “this”
举个例子,这里的 arrow() 使用的 this 来自于外部的 user.sayHi() 方法:
let user = {
firstName: "Ilya",
sayHi() {
let arrow = () => alert(this.firstName);
arrow();
}
};
user.sayHi(); // Ilya
🤔 分析
箭头函数没有this,他会继承创建他时的this(词法环境),这里创建的时刻就是sayHi被调用的时刻,此时运行sayHi的this是user,所以箭头函数中的this也是user
构造函数
构造函数在技术上是常规函数。不过有两个约定:
- 它们的命名以大写字母开头。
- 它们只能由 "new" 操作符来执行。
function User(name) {
this.name = name;
this.isAdmin = false;
}
let user = new User("Jack");
alert(user.name); // Jack
alert(user.isAdmin); // false
当一个函数被使用 new 操作符执行时,它按照以下步骤:
- 一个新的空对象被创建并分配给 this。
- 函数体执行。通常它会修改 this,为其添加新的属性。
- 返回 this 的值。
换句话说,new User(...) 做的就是类似的事情:
function User(name) {
// this = {};(隐式创建)
// 添加属性到 this
this.name = name;
this.isAdmin = false;
// return this;(隐式返回)
}
构造函数的 return(扩展)
通常,构造器没有 return 语句。它们的任务是将所有必要的东西写入 this,并自动转换为结果。
但是,如果这有一个 return 语句,那么规则就简单了:
- 如果 return 返回的是一个对象,则返回这个对象,而不是 this。每次new返回的对象都不一样
- 如果 return 返回的是一个原始类型,则忽略。
垃圾回收
对于开发者来说,JavaScript 的内存管理是自动的、无形的。我们创建的原始值、对象、函数……这一切都会占用内存。
当我们不再需要某个东西时会发生什么?JavaScript 引擎如何发现它并清理它?
可达性
“可达”值是那些以某种方式可访问或可用的值。它们被存储在内存中。
- 这里列出固有的可达值的基本集合,这些值明显不能被释放。
比方说:
- 当前执行的函数,它的局部变量和参数。
- 当前嵌套调用链上的其他函数、它们的局部变量和参数。
- 全局变量。
- (还有一些其他的,内部实现) 这些值被称作 根(roots)。
- 如果一个值可以从根通过引用或者引用链进行访问,则认为该值是可达的。
比方说,如果全局变量中有一个对象,并且该对象有一个属性引用了另一个对象,则 该 对象被认为是可达的。而且它引用的内容也是可达的。下面是详细的例子。
在 JavaScript 引擎中有一个被称作 垃圾回收器
的东西在后台执行。它监控着所有对象的状态,并删除掉那些已经不可达的。
内部算法
垃圾回收的基本算法被称为 “mark-and-sweep”
定期执行以下“垃圾回收”步骤:
- 垃圾收集器找到所有的根,并“标记”(记住)它们。
- 然后它遍历并“标记”来自它们的所有引用。
- 然后它遍历标记的对象并标记 它们的 引用。所有被遍历到的对象都会被记住,以免将来再次遍历到同一个对象。
- ……如此操作,直到所有可达的(从根部)引用都被访问到。
- 没有被标记的对象都会被删除。
例如,使我们的对象有如下的结构:
我们可以清楚地看到右侧有一个“无法到达的岛屿”。现在我们来看看“标记和清除”垃圾收集器如何处理它。
第一步标记所有的根:
然后,我们跟随它们的引用标记它们所引用的对象:
……如果还有引用的话,继续标记:
现在,无法通过这个过程访问到的对象被认为是不可达的,并且会被删除。
一些优化建议:
- 分代收集(Generational collection)—— 对象被分成两组:“新的”和“旧的”。在典型的代码中,许多对象的生命周期都很短:它们出现、完成它们的工作并很快死去,因此在这种情况下跟踪新对象并将其从内存中清除是有意义的。那些长期存活的对象会变得“老旧”,并且被检查的频次也会降低。
- 增量收集(Incremental collection)—— 如果有许多对象,并且我们试图一次遍历并标记整个对象集,则可能需要一些时间,并在执行过程中带来明显的延迟。因此,引擎将现有的整个对象集拆分为多个部分,然后将这些部分逐一清除。这样就会有很多小型的垃圾收集,而不是一个大型的。这需要它们之间有额外的标记来追踪变化,但是这样会带来许多微小的延迟而不是一个大的延迟。
- 闲时收集(Idle-time collection)—— 垃圾收集器只会在 CPU 空闲时尝试运行,以减少可能对代码执行的影响。
可选链 "?."
如果可选链 ?. 前面的值为 undefined 或者 null,它会停止运算并返回 undefined。
变体.() .[]:
let key = "firstName";
let user1 = {
firstName: "John"
};
let user2 = null;
alert( user1?.[key] ); // John
alert( user2?.[key] ); // undefined
Symbol
let id = Symbol();
创建时,我们可以给 symbol 一个描述(也称为 symbol 名),这在代码调试时非常有用:
// id 是描述为 "id" 的 symbol
let id = Symbol("id");
// symbol.description
给对象设置symbol类型的键
// ...
let id = Symbol("id");
user[id] = "Their id value"; //这里一定要用方括号,因为点号会被当做string类型的键
对象字面量中的 symbol
如果我们要在对象字面量 {...} 中使用 symbol,则需要使用方括号把它括起来。
let id = Symbol("id");
let user = {
name: "John",
[id]: 123 // 而不是 "id":123
};
🚀
symbol会在for in 中跳过
🤔 Vue源码中symbol的使用
export const FRAGMENT = Symbol(__DEV__ ? `Fragment` : ``)
export const TELEPORT = Symbol(__DEV__ ? `Teleport` : ``)
export const SUSPENSE = Symbol(__DEV__ ? `Suspense` : ``)
export const KEEP_ALIVE = Symbol(__DEV__ ? `KeepAlive` : ``)
export const BASE_TRANSITION = Symbol(__DEV__ ? `BaseTransition` : ``)
export const OPEN_BLOCK = Symbol(__DEV__ ? `openBlock` : ``)
这里的description只会在dev环境才会有,它的作用就是便于调试。
我们并不关心这个变量的值是什么,只需要它全局唯一,这里使用 symbol 类型再合适不过。
系统 symbol
JavaScript 内部有很多“系统” symbol,我们可以使用它们来微调对象的各个方面。
它们都被列在了 众所周知的 symbol 表的规范中:
- Symbol.hasInstance
- Symbol.isConcatSpreadable
- Symbol.iterator
- Symbol.toPrimitive
- ……等等。
例如,Symbol.toPrimitive 允许我们将对象描述为原始值转换。我们很快就会看到它的使用。
对象 —— 原始值转换
所有的对象在布尔上下文(context)中均为 true
if([]){
//进入执行
}
hint
"string" 对象到字符串的转换,当我们对期望一个字符串的对象执行操作时,如 “alert”:
// 输出
alert(obj);
// 将对象作为属性键
anotherObj[obj] = 123;
"number" 对象到数字的转换,例如当我们进行数学运算时:
// 显式转换
let num = Number(obj);
// 数学运算(除了二元加法)
let n = +obj; // 一元加法
let delta = date1 - date2;
// 小于/大于的比较
let greater = user1 > user2;
"default" 在少数情况下发生,当运算符“不确定”期望值的类型时。
例如,二元加法 + 可用于字符串(连接),也可以用于数字(相加)。因此,当二元加法得到对象类型的参数时,它将依据 "default" hint 来对其进行转换。
此外,如果对象被用于与字符串、数字或 symbol 进行 == 比较,这时到底应该进行哪种转换也不是很明确,因此使用 "default" hint。
// 二元加法使用默认 hint
let total = obj1 + obj2;
// obj == number 使用默认 hint
if (user == 1) { ... };
像 < 和 > 这样的小于/大于比较运算符,也可以同时用于字符串和数字。不过,它们使用 “number” hint,而不是 “default”。这是历史原因。
为了进行转换,JavaScript 尝试查找并调用三个对象方法:
- 调用
obj[Symbol.toPrimitive](hint)
—— 带有 symbol 键 Symbol.toPrimitive(系统 symbol)的方法,如果这个方法存在的话, - 否则,如果 hint 是 "string" —— 尝试调用 obj.toString() 或 obj.valueOf(),无论哪个存在。
- 否则,如果 hint 是 "number" 或 "default" —— 尝试调用 obj.valueOf() 或 obj.toString(),无论哪个存在。
🚀 Symbol.toPrimitive
obj[Symbol.toPrimitive] = function(hint) {
// 这里是将此对象转换为原始值的代码
// 它必须返回一个原始值
// hint = "string"、"number" 或 "default" 中的一个
}
let user = {
name: "John",
money: 1000,
[Symbol.toPrimitive](hint) {
alert(`hint: ${hint}`);
return hint == "string" ? `{name: "${this.name}"}` : this.money;
}
};
// 转换演示:
alert(user); // hint: string -> {name: "John"}
alert(+user); // hint: number -> 1000
alert(user + 500); // hint: default -> 1500
🚀 toString/valueOf
这些方法必须返回一个原始值。如果 toString 或 valueOf 返回了一个对象,那么返回值会被忽略(和这里没有方法的时候相同)。
默认情况下,普通对象具有 toString 和 valueOf 方法:
- toString 方法返回一个字符串 "[object Object]"。
- valueOf 方法返回对象自身。
let user = {name: "John"};
alert(user); // [object Object]
alert(user.valueOf() === user); // true
重写
let user = {
name: "John",
money: 1000,
// 对于 hint="string"
toString() {
return `{name: "${this.name}"}`;
},
// 对于 hint="number" 或 "default"
valueOf() {
return this.money;
}
};
alert(user); // toString -> {name: "John"}
alert(+user); // valueOf -> 1000
alert(user + 500); // valueOf -> 1500