开始阅读JavaScript高级程序设计(第5版)学习JS,总共有1000+页,非常全面,短期看完不太现实,找到了一篇博客,花些时间跟着这篇博客过一下红宝书。
红宝书《JavaScript高级程序设计(第5版)》学习大纲 - 大前端全栈开发 - SegmentFault 思否
数据类型:
书接3章上。
Symbol类型:
Symbol 是 ES6 引入的一种新的原始数据类型,它的主要作用是创建唯一的、不可变的值,主要用于对象属性的键。这个键一般是“隐藏“的,即使描述字符串完全相同,也是不同的键。也不会出现在常规遍历中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const obj = {
normalKey: "普通键,看得见",
[Symbol("hiddenKey")]: "隐藏键,看不见"
};
// 常规遍历看不到 Symbol 键
for (let key in obj) {
console.log(key); // 只输出 "normalKey"
}
console.log(Object.keys(obj)); // ["normalKey"]
console.log(Object.values(obj)); // ["普通键,看得见"]
// 但 Symbol 确实存在
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(hiddenKey)]
console.log(obj[Symbol("hiddenKey")]); // "隐藏键,看不见"
符号的基本用法:
symbol 不存在字面量写法,必须使用Symbol()函数初始化。
调用Symbol()函数时,也可以传入一个字符串参数作为对符号的描述(description),将来可以通过这个字符串来调试代码。但是,这个字符串参数与符号定义或标识完全无关:
1
2
3
4
5
6
7
8
9
10
11
let sym = Symbol();
console.log(typeof sym); // symbol
let genericSymbol = Symbol();
let otherGenericSymbol = Symbol();
let fooSymbol = Symbol('foo'); // 每个Symbol都是唯一的
let otherFooSymbol = Symbol('foo'); // 即使描述相同,也是不同的Symbol
console.log(genericSymbol == otherGenericSymbol); // false
console.log(fooSymbol == otherFooSymbol); // false
Symbol()函数不能与new关键字一起作为构造函数使用。这样做是为了避免创建符号包装对象,像使用Boolean、String或Number那样,它们都支持构造函数且可用于初始化包含原始值的包装对象:
1
2
3
4
5
6
7
8
9
10
let myBoolean = new Boolean();
console.log(typeof myBoolean); // "object"
let myString = new String();
console.log(typeof myString); // "object"
let myNumber = new Number();
console.log(typeof myNumber); // "object"
let mySymbol = new Symbol(); // TypeError: Symbol is not a constructor
如果你确实想使用符号包装对象,可以借用Object()函数:
1
2
3
let mySymbol = Symbol();
let myWrappedSymbol = Object(mySymbol);
console.log(typeof myWrappedSymbol); // "object"
全局符号注册表:
正如上面提及的,一般新建的symbol默认都是不相同的,但如果要在运行时让不同部分共享和重用一个符号实例,可以考虑使用全局符号注册表来创建符号,这是一张独立的表,用Symbol.for()方法可以实现这一点。
第一次使用某个字符串调用时,它会检查全局运行时注册表,发现不存在对应的符号,于是就会生成一个新符号实例并添加到注册表中。后续使用相同字符串的调用同样会检查注册表,发现存在与该字符串对应的符号,然后就会返回该符号实例。
1
2
3
4
let fooGlobalSymbol = Symbol.for('foo'); // 创建新符号
let otherFooGlobalSymbol = Symbol.for('foo'); // 重用已有符号
console.log(fooGlobalSymbol === otherFooGlobalSymbol); // true
即使采用相同的符号描述,在全局注册表中定义的符号跟使用Symbol()定义的符号也并不等同:
1
2
3
4
let localSymbol = Symbol('foo');
let globalSymbol = Symbol.for('foo');
console.log(localSymbol === globalSymbol); // false
全局注册表中的符号必须使用字符串键来创建,因此作为参数传给Symbol.for()的任何值都会被转换为字符串。此外,注册表中使用的键同时也会被用作符号描述。
1
2
let emptyGlobalSymbol = Symbol.for();
console.log(emptyGlobalSymbol); // Symbol(undefined)
还可以使用Symbol.keyFor()来查询全局注册表,这个方法接收符号,返回该全局符号对应的字符串键。如果查询的不是全局符号,则返回undefined。
1
2
3
4
5
6
7
// 创建全局符号
let s = Symbol.for('foo');
console.log(Symbol.keyFor(s)); // foo
// 创建普通符号
let s2 = Symbol('bar');
console.log(Symbol.keyFor(s2)); // undefined
使用符号作为属性:
有三种方法可以使用symbol作为属性:
对象字面量(使用计算属性语法,最常用)
Object.defineProperty()
Object.defineProperties()
对象字面量只能在计算属性语法中使用符号作为属性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
let s1 = Symbol('foo'),
s2 = Symbol('bar'),
s3 = Symbol('baz'),
s4 = Symbol('qux');
let o = {
[s1]: 'foo val'
};
// 这样也可以:o[s1] = 'foo val'; 在计算属性语法中使用符号作为属性
console.log(o);
// {Symbol(foo): foo val}
Object.defineProperty(o, s2, {value: 'bar val'});
console.log(o);
// {Symbol(foo): foo val, Symbol(bar): bar val}
Object.defineProperties(o, {
[s3]: {value: 'baz val'},
[s4]: {value: 'qux val'}
});
console.log(o);
// {Symbol(foo): foo val, Symbol(bar): bar val,
// Symbol(baz): baz val, Symbol(qux): qux val}
类似于Object.getOwnPropertyNames()返回对象实例的常规属性数组,Object.getOwnPropertySymbols()返回对象实例的符号属性数组。这两个方法的返回值彼此互斥。Object.getOwnPropertyDescriptors()会返回同时包含常规和符号属性描述符的对象。Reflect.ownKeys()会返回两种类型的键:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let s1 = Symbol('foo'),
s2 = Symbol('bar');
let o = {
[s1]: 'foo val',
[s2]: 'bar val',
baz: 'baz val',
qux: 'qux val'
};
console.log(Object.getOwnPropertySymbols(o));
// [Symbol(foo), Symbol(bar)]
console.log(Object.getOwnPropertyNames(o));
// ["baz", "qux"]
console.log(Object.getOwnPropertyDescriptors(o));
// {baz: {...}, qux: {...}, Symbol(foo): {...}, Symbol(bar): {...}}
console.log(Reflect.ownKeys(o));
// ["baz", "qux", Symbol(foo), Symbol(bar)]
因为符号属性是对内存中符号的一个引用,所以直接创建并用作属性的符号不会丢失。但是,如果没有显式地保存对这些属性的引用,那么必须遍历对象的所有符号属性才能找到相应的属性键:
1
2
3
4
5
6
7
8
9
10
11
12
13
let o = {
[Symbol('foo')]: 'foo val',
[Symbol('bar')]: 'bar val'
};
console.log(o);
// {Symbol(foo): "foo val", Symbol(bar): "bar val"}
let barSymbol = Object.getOwnPropertySymbols(o)
.find((symbol) => symbol.toString().match(/bar/));
console.log(barSymbol);
// Symbol(bar)
常用内置符号:
ECMAScript定义了一批常用内置符号(well-known symbol),用于暴露语言内部行为,开发者可以直接访问、重写或模拟这些行为。
这些内置符号最重要的用途之一是重新定义它们,从而改变原生结构的行为。但值得注意的是,普通symbol键和内置的symbol功能是不同的两套机制,普通symbol作为一个唯一的键存在,而内置symbol实则是一个特殊的协议接口,而这个功能搭载在symbol特性上,利用symbol的唯一性,正好不会引发任何冲突。
常用内置符号让开发者能够深度定制对象的行为,让自定义对象也能像内置对象(如 Array、String)一样,支持各种语言特性。
比如,我们知道for-of循环会在相关对象上使用Symbol.iterator属性,那么就可以通过在自定义对象上重新定义Symbol.iterator的值,来改变for-of在迭代该对象时的行为。
在提到ECMAScript规范时,经常会引用符号在规范中的名称,前缀为@@。比如,@@iterator指的就是Symbol.iterator。
内置符号有若干,内容较多,当需要时可以在书本回顾。
Object类型:
ECMAScript中的对象其实就是一组数据和功能的集合。对象通过new操作符后跟对象类型的名称来创建。开发者可以通过创建Object类型的实例来创建自己的对象,然后再给对象添加属性和方法:
1
let o = new Object();
Object的实例本身并不是很有用,但理解与它相关的概念非常重要。类似Java中的java.lang.Object,ECMAScript中的Object也是派生其他对象的基类。Object类型的所有属性和方法在派生的对象上同样存在。
每个Object实例都有如下属性和方法。
严格来讲,ECMA-262中对象的行为不一定适合JavaScript中的其他对象。比如浏览器环境中的BOM和DOM对象,它们都是由宿主环境定义和提供的宿主对象。而宿主对象不受ECMA-262约束,所以它们可能会也可能不会继承Object。
操作符:
内容较多,参考红宝书。
语句:
for-in语句:
for…in遍历键,for…of遍历值。
for-in语句是一种严格的迭代语句,用于枚举对象中的非符号键属性,语法如下:
1
for (property in expression) statement
这个例子使用for-in循环显示了BOM对象window的所有属性。每次执行循环,都会给变量propName赋予一个window对象的属性作为值,直到window的所有属性都被枚举一遍。与for循环一样,这里控制语句中的const也不是必需的。但为了确保这个局部变量不被修改,推荐使用const。 ECMAScript中对象的属性是无序的,因此for-in语句不能保证返回对象属性的顺序。所有可枚举的属性都会返回一次,但返回的顺序可能会因浏览器而异。
1
2
3
for (const propName in window) {
document.write(propName);
}
for-of语句:
for-of语句是一种严格的迭代语句,用于遍历可迭代对象的元素:
1
for (property of expression) statement
我们使用for-of语句显示了一个包含4个元素的数组中的所有元素。循环会一直持续到将所有元素都迭代完。与for循环一样,这里控制语句中的const也不是必需的。但为了确保这个局部变量不被修改,推荐使用const。
for-of循环会按照可迭代对象的next()方法产生值的顺序迭代元素。如果尝试迭代的变量不支持迭代,则for-of语句会抛出错误。
1
2
3
for (const el of [2,4,6,8]) {
document.write(el);
}
for-of语句的扩展版for-await-of循环支持生成期约(promise)的异步可迭代对象,相关内容将在第7章介绍。
标签语句:
标签语句用于给语句加标签:
1
label: statement
start是一个标签,可以在后面通过break或continue语句引用。标签语句的典型应用场景是嵌套循环。一般在内层循环达成某些条件,需要影响外层循环时,可以使用break label或者continue label来实现目的。
1
2
3
start: for (let i = 0; i < count; i++) {
console.log(i);
}
函数:
ECMAScript中的函数使用function关键字或箭头语法声明,后跟一组参数,然后是函数体。
函数的基本语法:
1
2
3
function functionName(arg0, arg1,...,argN) {
statements
}
ECMAScript中的函数不需要指定是否返回值。任何函数在任何时间都可以使用return语句来返回函数的值,用法是后跟要返回的值。
1
2
3
function sum(num1, num2) {
return num1 + num2;
}
最佳实践是函数要么返回值,要么不返回值。只在某个条件下返回值的函数会带来麻烦,尤其是调试时。
严格模式对函数也有一些限制:
函数不能以eval或arguments作为名称;
函数的参数不能叫eval或arguments;
两个命名参数不能拥有同一个名称。如果违反上述规则,则会导致语法错误,代码也不会执行。






