Study Notes
本博主会持续更新各种前端的技术,如果各位道友喜欢,可以关注、收藏、点赞下本博主的文章。
let 与块级作用域
let 语句声明一个块级作用域的本地变量,并且可选的将其初始化为一个值。
描述
let 允许你声明一个作用域被限制在 块级中的变量、语句或者表达式。与 var 关键字不同的是, var 声明的变量只能是全局或者整个函数块的。 var 和 let 的不同之处在于后者是在编译时才初始化(见下面)。
作用域规则
let 声明的变量只在其声明的块或子块中可用,这一点,与 var 相似。二者之间最主要的区别在于 var 声明的变量的作用域是整个封闭函数。
function varTest() {
var x = 1;
{
var x = 2; // 同样的变量!
console.log(x); // 2
}
console.log(x); // 2
}
function letTest() {
let x = 1;
{
let x = 2; // 不同的变量
console.log(x); // 2
}
console.log(x); // 1
}
varTest();
letTest();
重复声明
在同一个函数或块作用域中重复声明同一个变量会引起 SyntaxError。
if (x) {
let foo;
let foo; // SyntaxError thrown.
}
在 switch 语句中只有一个块,你可能因此而遇到错误。
let x = 1;
switch (x) {
case 0:
let foo;
break;
case 1:
let foo; // SyntaxError for redeclaration.
break;
}
然而,需要特别指出的是,一个嵌套在 case 子句中的块会创建一个新的块作用域的词法环境,就不会产生上诉重复声明的错误。
let x = 1;
switch (x) {
case 0: {
let foo;
break;
}
case 1: {
let foo; // SyntaxError for redeclaration.
break;
}
}
暂存死区
与通过 var 声明的有初始化值 undefined 的变量不同,通过 let 声明的变量直到它们的定义被执行时才初始化。在变量初始化前访问该变量会导致 ReferenceError。该变量处在一个自块顶部到初始化处理的“暂存死区”中。
function do_something() {
console.log(bar); // undefined
console.log(foo); // ReferenceError
var bar = 1;
let foo = 2;
}
do_something();
暂存死区与 typeof
与通过 var 声明的变量, 有初始化值 undefined 和只是未声明的变量不同的是,如果使用 typeof 检测在暂存死区中的变量, 会抛出 ReferenceError 异常:
// prints out 'undefined'
console.log(typeof undeclaredVariable);
// results in a 'ReferenceError'
console.log(typeof i);
let i = 10;
暂存死区和静态作用域/詞法作用域的相关例子
由于词法作用域,表达式(foo + 55)内的标识符 foo 被认为是 if 块的 foo 变量,而不是值为 33 的块外面的变量 foo。
在同一行,这个 if 块中的 foo 已经在词法环境中被创建了,但是还没有到达(或者终止)它的初始化(这是语句本身的一部分)。
这个 if 块里的 foo 还依旧在暂存死区里。
function test() {
var foo = 33;
{
let foo = foo + 55; // ReferenceError
}
}
test();
在以下情况下,这种现象可能会使您感到困惑。 let n of n.a 已经在 for 循环块的私有范围内。因此,标识符 n.a 被解析为位于指令本身("let n")中的“ n”对象的属性“ a”。
在没有执行到它的初始化语句之前,它仍旧存在于暂存死区中。
function go(n) {
// n here is defined!
console.log(n); // Object {a: [1,2,3]}
for (let n of n.a) {
// ReferenceError
console.log(n);
}
}
go({ a: [1, 2, 3] });
块级作用域
用在块级作用域中时, let 将变量的作用域限制在块内, 而 var 声明的变量的作用域是在函数内.
var a = 1;
var b = 2;
if (a === 1) {
var a = 11; // the scope is global
let b = 22; // the scope is inside the if-block
console.log(a); // 11
console.log(b); // 22
}
console.log(a); // 11
console.log(b); // 2
而这种 var 与 let 合并的声明方式会报 SyntaxError 错误, 因为 var 会将变量提升至块的顶部, 这会导致隐式地重复声明变量.
let x = 1;
{
var x = 2; // SyntaxError for re-declaration
}
const
常量是块级作用域,很像使用 let 语句定义的变量。常量的值不能通过重新赋值来改变,并且不能重新声明。
描述
此声明创建一个常量,其作用域可以是全局或本地声明的块。 与 var 变量不同,全局常量不会变为 window 对象的属性。需要一个常数的初始化器;也就是说,您必须在声明的同一语句中指定它的值(这是有道理的,因为以后不能更改)。
const 声明创建一个值的只读引用。但这并不意味着它所持有的值是不可变的,只是变量标识符不能重新分配。例如,在引用内容是对象的情况下,这意味着可以改变对象的内容(例如,其参数)。
关于“暂存死区”的所有讨论都适用于 let 和 const。
一个常量不能和它所在作用域内的其他变量或函数拥有相同的名称。
// 注意: 常量在声明的时候可以使用大小写,但通常情况下全部用大写字母。
// 定义常量MY_FAV并赋值7
const MY_FAV = 7;
// 报错
MY_FAV = 20;
// 输出 7
console.log("my favorite number is: " + MY_FAV);
// 尝试重新声明会报错
const MY_FAV = 20;
// MY_FAV 保留给上面的常量,这个操作会失败
var MY_FAV = 20;
// 也会报错
let MY_FAV = 20;
// 注意块范围的性质很重要
if (MY_FAV === 7) {
// 没问题,并且创建了一个块作用域变量 MY_FAV
// (works equally well with let to declare a block scoped non const variable)
let MY_FAV = 20;
// MY_FAV 现在为 20
console.log('my favorite number is ' + MY_FAV);
// 这被提升到全局上下文并引发错误
var MY_FAV = 20;
}
// MY_FAV 依旧为7
console.log("my favorite number is " + MY_FAV);
// 常量要求一个初始值
const FOO; // SyntaxError: missing = in const declaration
// 常量可以定义成对象
const MY_OBJECT = {"key": "value"};
// 重写对象和上面一样会失败
MY_OBJECT = {"OTHER_KEY": "value"};
// 对象属性并不在保护的范围内,下面这个声明会成功执行
MY_OBJECT.key = "otherValue";
// 也可以用来定义数组
const MY_ARRAY = [];
// It's possible to push items into the array
// 可以向数组填充数据
MY_ARRAY.push('A'); // ["A"]
// 但是,将一个新数组赋给变量会引发错误
MY_ARRAY = ['B']
解构赋值
解构赋值语法是一种 Javascript 表达式。通过解构赋值, 可以将属性/值从对象/数组中取出,赋值给其他变量。
解构数组
变量声明并赋值时的解构
const foo = ['one', 'two', 'three'];
const [one, two, three] = foo;
console.log(one); // "one"
console.log(two); // "two"
console.log(three); // "three"
变量先声明后赋值时的解构
通过解构分离变量的声明,可以为一个变量赋值。
let a, b;
[a, b] = [1, 2];
console.log(a); // 1
console.log(b); // 2
默认值
为了防止从数组中取出一个值为 undefined 的对象,可以在表达式左边的数组中为任意对象预设默认值。
const [c = 5, d = 7] = [1];
console.log(c); // 1
console.log(d); // 7
交换变量
在一个解构表达式中可以交换两个变量的值。
没有解构赋值的情况下,交换两个变量需要一个临时变量(或者用低级语言中的 XOR-swap 技巧)。
let e = 1;
let f = 3;
[e, f] = [f, e];
console.log(e); // 3
console.log(f); // 1
解析一个从函数返回的数组
从一个函数返回一个数组是十分常见的情况。解构使得处理返回值为数组时更加方便。
在下面例子中,要让 [1, 2] 成为函数的 f() 的输出值,可以使用解构在一行内完成解析。
function fn() {
return [1, 2];
}
const [g, h] = fn();
console.log(g); // 1
console.log(h); // 2
忽略某些返回值
你也可以忽略你不感兴趣的返回值:
function fn1() {
return [1, 2, 3];
}
const [i, , j] = fn1();
console.log(i); // 1
console.log(j); // 3
将剩余数组赋值给一个变量
当解构一个数组时,可以使用剩余模式,将数组剩余部分赋值给一个变量。
const [k, ...l] = [1, 2, 3];
console.log(k); // 1
console.log(l); // [2, 3]
用正则表达式匹配提取值
用正则表达式的 exec() 方法匹配字符串会返回一个数组,该数组第一个值是完全匹配正则表达式的字符串,然后的值是匹配正则表达式括号内内容部分。解构赋值允许你轻易地提取出需要的部分,忽略完全匹配的字符串——如果不需要的话。
function parseProtocol(url) {
const parsedURL = /^(\w+)\:\/\/([^\/]+)\/(.*)$/.exec(url);
if (!parsedURL) {
return false;
}
console.log(parsedURL); // ["https://developer.mozilla.org/en-US/Web/JavaScript", "https", "developer.mozilla.org", "en-US/Web/JavaScript"]
const [, protocol] = parsedURL;
return protocol;
}
console.log(
parseProtocol('https://developer.mozilla.org/en-US/Web/JavaScript'),
); // "https"
解构对象
基本赋值
const o = { p: 42, q: true };
const { p, q } = o;
console.log(p); // 42
console.log(q); // true
无声明赋值
一个变量可以独立于其声明进行解构赋值。
let o, p;
({ o, p } = { o: 1, p: 2 });
console.log(o); // 1
console.log(p); // 2
给新的变量名赋值
可以从一个对象中提取变量并赋值给和对象属性名不同的新的变量名。
const { p: age, q: bar } = { p: 42, q: true };
console.log(age); // 42
console.log(bar); // true
默认值
变量可以先赋予默认值。当要提取的对象没有对应的属性,变量就被赋予默认值。
const { q = 10, r = 5 } = { q: 3 };
console.log(q); // 3
console.log(r); // 5
给新的变量命名并提供默认值
一个属性可以同时 1)从一个对象解构,并分配给一个不同名称的变量 2)分配一个默认值,以防未解构的值是 undefined。
const { a: aa = 10, b: bb = 5 } = { a: 3 };
console.log(aa); // 3
console.log(bb); // 5
函数参数默认值
function drawES2015Chart({
size = 'big',
cords = { x: 0, y: 0 },
radius = 25,
} = {}) {
console.log(size, cords, radius);
// do some chart drawing
}
drawES2015Chart({
cords: { x: 18, y: 30 },
radius: 30,
});
解构嵌套对象和数组
const metadata = {
title: 'Scratchpad',
translations: [
{
locale: 'de',
localization_tags: [],
last_edit: '2014-04-14T08:43:37',
url: '/de/docs/Tools/Scratchpad',
title: 'JavaScript-Umgebung',
},
],
url: '/en-US/docs/Tools/Scratchpad',
};
let {
title: englishTitle, // rename
translations: [
{
title: localeTitle, // rename
},
],
} = metadata;
console.log(englishTitle); // "Scratchpad"
console.log(localeTitle); // "JavaScript-Umgebung"
For of 迭代和解构
const people = [
{
name: 'Mike Smith',
family: {
mother: 'Jane Smith',
father: 'Harry Smith',
sister: 'Samantha Smith',
},
age: 35,
},
{
name: 'Tom Jones',
family: {
mother: 'Norah Jones',
father: 'Richard Jones',
brother: 'Howard Jones',
},
age: 25,
},
];
for (const {
name: n,
family: { father: f },
} of people) {
console.log('Name: ' + n + ', Father: ' + f);
}
// "Name: Mike Smith, Father: Harry Smith"
// "Name: Tom Jones, Father: Richard Jones"
从作为函数实参的对象中提取数据
function userId({ id }) {
return id;
}
function whoIs({ displayName, fullName: { firstName: name } }) {
console.log(displayName + ' is ' + name);
}
const user = {
id: 42,
displayName: 'jdoe',
fullName: {
firstName: 'John',
lastName: 'Doe',
},
};
console.log('userId: ' + userId(user)); // "userId: 42"
whoIs(user); // "jdoe is John"
对象属性计算名和解构
let key = 'z';
let { [key]: s } = { z: 'bar' };
console.log(s); // "bar"
对象解构中的 Rest
Rest 属性收集那些尚未被解构模式拾取的剩余可枚举属性键。
let { t, u, ...rest } = { t: 10, u: 20, w: 30, x: 40 };
console.log(t); // 10
console.log(u); // 20
console.log(rest); // { c: 30, d: 40 }
解构对象时会查找原型链(如果属性不在对象自身,将从原型链中查找)
// 声明对象 和 自身 self 属性
let obj = { self: '123' };
// 在原型链中定义一个属性 prot
obj.__proto__.prot = '456';
// test
const { self, prot } = obj;
console.log(self); // "123"
console.log(prot); // "456"(访问到了原型链)
Template String(模板字符串)
模板字面量 是允许嵌入表达式的字符串字面量。你可以使用多行字符串和字符串插值功能。它们在 ES2015 规范的先前版本中被称为“模板字符串”。
描述
模板字符串使用反引号 (``) 来代替普通字符串中的用双引号和单引号。模板字符串可以包含特定语法(${expression})的占位符。占位符中的表达式和周围的文本会一起传递给一个默认函数,该函数负责将所有的部分连接起来,如果一个模板字符串由表达式开头,则该字符串被称为带标签的模板字符串,该表达式通常是一个函数,它会在模板字符串处理后被调用,在输出最终结果前,你都可以通过该函数来对模板字符串进行操作处理。在模版字符串内使用反引号(`)时,需要在它前面加转义符(\)。
`\`` === '`'; // --> true
多行字符串
在新行中插入的任何字符都是模板字符串中的一部分,使用普通字符串,你可以通过以下的方式获得多行字符串:
console.log('string text line 1\n' + 'string text line 2');
// "string text line 1
// string text line 2"
要获得同样效果的多行字符串,只需使用如下代码:
console.log(`string text line 1
string text line 2`);
// "string text line 1
// string text line 2"
插入表达式
在普通字符串中嵌入表达式,必须使用如下语法:
const a = 5;
const b = 10;
console.log('Fifteen is ' + (a + b) + ' and\nnot ' + (2 * a + b) + '.');
// "Fifteen is 15 and
// not 20."
现在通过模板字符串,我们可以使用一种更优雅的方式来表示:
const a = 5;
const b = 10;
console.log(`Fifteen is ${a + b} and
not ${2 * a + b}.`);
// "Fifteen is 15 and
// not 20."
带标签的模板字符串
更高级的形式的模板字符串是带标签的模板字符串。标签使您可以用函数解析模板字符串。标签函数的第一个参数包含一个字符串值的数组。其余的参数与表达式相关。最后,你的函数可以返回处理好的的字符串(或者它可以返回完全不同的东西 , 如下个例子所述)。用于该标签的函数的名称可以被命名为任何名字。
const person = 'Mike';
const age = 28;
function myTag(strings, personExp, ageExp) {
const str0 = strings[0]; // "that "
const str1 = strings[1]; // " is a "
let ageStr;
if (ageExp > 99) {
ageStr = 'centenarian';
} else {
ageStr = 'youngster';
}
return str0 + personExp + str1 + ageStr;
}
const output = myTag`that ${person} is a ${age}`;
console.log(output);
// that Mike is a youngster
正如下面例子所展示的,标签函数并不一定需要返回一个字符串。
function template(strings, ...keys) {
return function (...values) {
const dict = values[values.length - 1] || {};
const result = [strings[0]];
keys.forEach(function (key, i) {
const value = Number.isInteger(key) ? values[key] : dict[key];
result.push(value, strings[i + 1]);
});
return result.join('');
};
}
const t1Closure = template`${0}${1}${0}!`;
console.log(t1Closure('Y', 'A')); // "YAY!"
const t2Closure = template`${0} ${'foo'}!`;
console.log(t2Closure('Hello', { foo: 'World' })); // "Hello World!"
原始字符串
在标签函数的第一个参数中,存在一个特殊的属性 raw ,我们可以通过它来访问模板字符串的原始字符串,而不经过特殊字符的替换。
function tag(strings) {
return strings.raw[0];
}
console.log(tag`string text line 1 \n string text line 2`);
// logs "string text line 1 \n string text line 2" ,
带标签的模版字面量及转义序列
自 ES2016 起,带标签的模版字面量遵守以下转义序列的规则:
- Unicode 字符以"\u"开头,例如\u00A9
- Unicode 码位用"\u{}"表示,例如\u{2F804}
- 十六进制以"\x"开头,例如\xA9
- 八进制以""和数字开头,例如\251
这表示类似下面这种带标签的模版是有问题的,因为对于每一个 ECMAScript 语法,解析器都会去查找有效的转义序列,但是只能得到这是一个形式错误的语法:
function latex(str) {
return { cooked: str[0], raw: str.raw[0] };
}
latex`\unicode`;
// 在较老的ECMAScript版本中报错(ES2016及更早)
// SyntaxError: malformed Unicode character escape sequence
ES2018 关于非法转义序列的修订
带标签的模版字符串应该允许嵌套支持常见转义序列的语言(例如 DSLs、LaTeX)。ECMAScript 提议模版字面量修订(第 4 阶段,将要集成到 ECMAScript 2018 标准) 移除对 ECMAScript 在带标签的模版字符串中转义序列的语法限制。
不过,非法转义序列在"cooked"当中仍然会体现出来。它们将以 undefined 元素的形式存在于"cooked"之中:
Functions Default(函数默认参数)
函数默认参数允许在没有值或 undefined 被传入时使用默认形参。
传入 undefined vs 其他假值
function test(num = 1) {
console.log(typeof num);
console.log(num);
}
test();
/**
* number
* 1
*/
test(undefined);
/**
* number
* 1
*/
test('');
/**
* string
* ''
*/
test(null);
/**
* object
* null
*/
调用时解析
在函数被调用时,参数默认值会被解析,所以不像 Python 中的例子,每次函数调用时都会创建一个新的参数对象。
function append(value, array = []) {
array.push(value);
return array;
}
console.log(append(1)); //[1]
console.log(append(2)); //[2], 不是 [1, 2]
默认参数可用于后面的默认参数
已经遇到的参数可用于以后的默认参数:
function greet(name, greeting, message = greeting + ' ' + name) {
return [name, greeting, message];
}
console.log(greet('David', 'Hi')); // ["David", "Hi", "Hi David"]
console.log(greet('David', 'Hi', 'Happy Birthday!')); // ["David", "Hi", "Happy Birthday!"]
位于默认参数之后非默认参数
在 Gecko 26 (Firefox 26 / Thunderbird 26 / SeaMonkey 2.23 / Firefox OS 1.2)之前,以下代码会造成 SyntaxError 错误。这已经在 bug 1022967 中修复,并在以后的版本中按预期方式工作。参数仍然设置为从左到右,覆盖默认参数,即使后面的参数没有默认值。
function f(x = 1, y) {
console.log([x, y]);
}
f(); // [1, undefined]
f(2); // [2, undefined]
有默认值的解构参数
你可以通过解构赋值为参数赋值:
function fn([x, y] = [1, 2], { z: z } = { z: 3 }) {
console.log(x + y + z);
}
fn(); // 6
Functions Rest Parameters(剩余参数)
剩余参数语法允许我们将一个不定数量的参数表示为一个数组。
剩余参数和 arguments 对象的区别
剩余参数和 arguments 对象之间的区别主要有三个:
- 剩余参数只包含那些没有对应形参的实参,而 arguments 对象包含了传给函数的所有实参。
- arguments 对象不是一个真正的数组,而剩余参数是真正的 Array 实例,也就是说你能够在它上面直接使用所有的数组方法,比如 sort,map,forEach 或 pop。
- arguments 对象还有一些附加的属性 (如 callee 属性)。
解构剩余参数
剩余参数可以被解构,这意味着他们的数据可以被解包到不同的变量中。请参阅解构赋值。
function f(...[a, b, c]) {
console.log(a + b + c);
}
f(1); // NaN (b 和 c 是 undefined)
f(1, 2, 3); // 6
f(1, 2, 3, 4); // 6
demo
下例中,剩余参数包含了从第二个到最后的所有实参,然后用第一个实参依次乘以它们:
function multiply(multiplier, ...theArgs) {
return theArgs.map(function (element) {
return multiplier * element;
});
}
console.log(multiply(2, 1, 2, 3)); // [2, 4, 6]
下例演示了你可以在剩余参数上使用任意的数组方法,而 arguments 对象不可以:
function sortRestArgs(...theArgs) {
return theArgs.sort();
}
console.log(sortRestArgs(5, 3, 7, 1)); // 弹出 1,3,5,7
function sortArguments() {
return arguments.sort(); // 不会执行到这里
}
console.log(sortArguments(5, 3, 7, 1)); // 抛出TypeError异常:arguments.sort is not a function
为了在 arguments 对象上使用 Array 方法,它必须首先被转换为一个真正的数组。
function sortArguments1() {
const args = Array.prototype.slice.call(arguments);
return args.sort();
}
console.log(sortArguments1(5, 3, 7, 1)); // shows 1, 3, 5, 7
Arrow Functions(箭头函数)
箭头函数表达式的语法比函数表达式更简洁,并且没有自己的 this,arguments,super 或 new.target。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。
描述
引入箭头函数有两个方面的作用:更简短的函数并且不绑定 this。
更短的函数
const elements = ['Hydrogen', 'Helium', 'Lithium', 'Beryllium'];
console.log(
elements.map(function (element) {
return element.length;
}),
); // 返回数组:[8, 6, 7, 9]
// 上面的普通函数可以改写成如下的箭头函数
console.log(
elements.map((element) => {
return element.length;
}),
); // [8, 6, 7, 9]
// 当箭头函数只有一个参数时,可以省略参数的圆括号
console.log(
elements.map((element) => {
return element.length;
}),
); // [8, 6, 7, 9]
// 当箭头函数的函数体只有一个 `return` 语句时,可以省略 `return` 关键字和方法体的花括号
console.log(elements.map((element) => element.length)); // [8, 6, 7, 9]
// 在这个例子中,因为我们只需要 `length` 属性,所以可以使用参数解构
// 需要注意的是字符串 `"length"` 是我们想要获得的属性的名称,而 `lengthFooBArX` 则只是个变量名,
// 可以替换成任意合法的变量名
console.log(elements.map(({ length: lengthFooBArX }) => lengthFooBArX)); // [8, 6, 7, 9]
没有单独的 this
在箭头函数出现之前,每一个新函数根据它是被如何调用的来定义这个函数的 this 值:
- 如果是该函数是一个构造函数,this 指针指向一个新的对象
- 在严格模式下的函数调用下,this 指向 undefined
- 如果是该函数是一个对象的方法,则它的 this 指针指向这个对象
function Person() {
// Person() 构造函数定义 `this`作为它自己的实例.
this.age = 0;
setTimeout(function growUp() {
// 在非严格模式, growUp()函数定义 `this`作为全局对象,
// 与在 Person()构造函数中定义的 `this`并不相同.
this.age++;
console.log(this.age); // NaN
}, 1000);
}
new Person();
在 ECMAScript 3/5 中,通过将 this 值分配给封闭的变量,可以解决 this 问题。
function Person() {
const that = this;
that.age = 0;
setTimeout(function growUp() {
// 回调引用的是`that`变量, 其值是预期的对象.
that.age++;
console.log(that.age); // 1
}, 1000);
}
new Person();
箭头函数不会创建自己的 this,它只会从自己的作用域链的上一层继承 this。因此,在下面的代码中,传递给 setInterval 的函数内的 this 与封闭函数中的 this 值相同:
function Person() {
this.age = 0;
setTimeout(() => {
this.age++; // |this| 正确地指向 p 实例
console.log(this.age); // 1
}, 1000);
}
new Person();
与严格模式的关系
鉴于 this 是词法层面上的,严格模式中与 this 相关的规则都将被忽略。
const f = () => {
return this;
};
function f1() {
'use strict';
return this;
}
function f2() {
return this;
}
console.log(f() === global); // false
console.log(f1() === global); // false
console.log(f2() === global); // true
通过 call 或 apply 调用
由于 箭头函数没有自己的 this 指针,通过 call() 或 apply() 方法调用一个函数时,只能传递参数(不能绑定 this),他们的第一个参数会被忽略。(这种现象对于 bind 方法同样成立)
const adder = {
base: 1,
add: function (a) {
const f = (v) => v + this.base;
return f(a);
},
addThruCall: function (a) {
const f = (v) => v + this.base;
const b = {
base: 2,
};
return f.call(b, a);
},
};
console.log(adder.add(1)); // 输出 2
console.log(adder.addThruCall(1)); // 仍然输出 2
不绑定 arguments
箭头函数不绑定 Arguments 对象。
const foo = (...args) => {
console.log(arguments[0]); // {}
console.log(args); // [1,2]
};
foo(1, 2);
箭头函数不能用作构造器,和 new 一起用会抛出错误。
箭头函数没有 prototype 属性。
yield 关键字通常不能在箭头函数中使用(除非是嵌套在允许使用的函数内)。因此,箭头函数不能用作函数生成器。
Object
Object 构造函数创建一个对象包装器。
Object 构造函数的方法
通过复制一个或多个对象来创建一个新的对象。
// 复制一个对象
const obj = { a: 1 };
const copy = Object.assign({}, obj);
console.log(copy); // { a: 1 }
// 合并对象
const o1 = { a: 1 };
const o2 = { b: 2 };
const o3 = { c: 3 };
const obj1 = Object.assign(o1, o2, o3);
console.log(obj1); // { a: 1, b: 2, c: 3 }
console.log(o1); // { a: 1, b: 2, c: 3 }, 注意目标对象自身也会改变。
使用指定的原型对象和属性创建一个新对象。
// 用Object.create实现类式继承
// Shape - 父类(superclass)
function Shape() {
this.x = 0;
this.y = 0;
}
// 父类的方法
Shape.prototype.move = function (x, y) {
this.x += x;
this.y += y;
console.info('Shape moved.');
};
// Rectangle - 子类(subclass)
function Rectangle() {
Shape.call(this); // call super constructor.
}
// 子类续承父类
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;
const rect = new Rectangle();
console.log('Is rect an instance of Rectangle?', rect instanceof Rectangle); // true
console.log('Is rect an instance of Shape?', rect instanceof Shape); // true
rect.move(1, 1); // Outputs, 'Shape moved.'
给对象添加一个属性并指定该属性的配置。
...Object方法等
Proxy(代理)
Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。
参数
target
要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
handler
一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。
handler 对象的方法
handler 对象是一个容纳一批特定属性的占位符对象。它包含有 Proxy 的各个捕获器(trap)。
所有的捕捉器是可选的。如果没有定义某个捕捉器,那么就会保留源对象的默认行为。
handler.getPrototypeOf()
Object.getPrototypeOf 方法的捕捉器。
handler.setPrototypeOf()
Object.setPrototypeOf 方法的捕捉器。
handler.isExtensible()
Object.isExtensible 方法的捕捉器。
handler.preventExtensions()
Object.preventExtensions 方法的捕捉器。
handler.getOwnPropertyDescriptor()
Object.getOwnPropertyDescriptor 方法的捕捉器。
handler.defineProperty()
Object.defineProperty 方法的捕捉器。
handler.has()
in 操作符的捕捉器。
handler.get()
属性读取操作的捕捉器。
handler.set()
属性设置操作的捕捉器。
handler.deleteProperty()
delete 操作符的捕捉器。
handler.ownKeys()
Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器。
handler.apply()
函数调用操作的捕捉器。
handler.construct()
new 操作符的捕捉器。
demo
const handler = {
get: function (obj, prop) {
return prop in obj ? obj[prop] : 37;
},
};
const p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;
console.log(p.a, p.b); // 1, undefined
console.log('c' in p, p.c); // false, 37
let validator = {
set: function (obj, prop, value) {
if (prop === 'age') {
if (!Number.isInteger(value)) {
throw new TypeError('The age is not an integer');
}
if (value > 200) {
throw new RangeError('The age seems invalid');
}
}
// The default behavior to store the value
obj[prop] = value;
// 表示成功
return true;
},
};
let person = new Proxy({}, validator);
person.age = 100;
console.log(person.age);
// 100
person.age = 'young';
// 抛出异常: Uncaught TypeError: The age is not an integer
person.age = 300;
// 抛出异常: Uncaught RangeError: The age seems invalid
具体使用查看Proxy
Reflect
Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与 proxy handlers 的方法相同。Reflect 不是一个函数对象,因此它是不可构造的。
描述
与大多数内置对象不同,Reflect 不是一个构造函数。你不能将其与一个 new 运算符一起使用,或者将 Reflect 对象作为一个函数来调用。Reflect 的所有属性和方法都是静态的(就像 Math 对象)。
静态方法
Reflect.apply(target, thisArgument, argumentsList)
对一个函数进行调用操作,同时可以传入一个数组作为调用参数。和 Function.prototype.apply() 功能类似。
console.log(Reflect.apply(Math.floor, undefined, [1.75]));
// 1;
console.log(
Reflect.apply(String.fromCharCode, undefined, [104, 101, 108, 108, 111]),
);
// "hello"
console.log(
Reflect.apply(RegExp.prototype.exec, /ab/, ['confabulation']).index,
);
// 4
console.log(Reflect.apply(''.charAt, 'ponies', [3]));
// "i"
Reflect.construct(target, argumentsList[, newTarget])
对构造函数进行 new 操作,相当于执行 new target(...args)。
function OneClass() {
this.name = 'one';
}
function OtherClass() {
this.name = 'other';
}
// 创建一个对象:
const obj1 = Reflect.construct(OneClass, global, OtherClass);
// 与上述方法等效:
const obj2 = Object.create(OtherClass.prototype);
OneClass.apply(obj2, global);
console.log(obj1.name); // 'one'
console.log(obj2.name); // 'one'
console.log(obj1 instanceof OneClass); // false
console.log(obj2 instanceof OneClass); // false
console.log(obj1 instanceof OtherClass); // true
console.log(obj2 instanceof OtherClass); // true
Reflect.defineProperty(target, propertyKey, attributes)
和 Object.defineProperty() 类似。如果设置成功就会返回 true
const student = {};
console.log(Reflect.defineProperty(student, 'name', { value: 'Mike' })); // true
console.log(student.name); // "Mike"
Reflect.deleteProperty(target, propertyKey)
作为函数的 delete 操作符,相当于执行 delete target[name]。
const student = {};
console.log(Reflect.defineProperty(student, 'name', { value: 'Mike' })); // true
console.log(student.name); // "Mike"
// Reflect.deleteProperty
const obj = { x: 1, y: 2 };
console.log(Reflect.deleteProperty(obj, 'x')); // true
console.log(obj); // { y: 2 }
const arr = [1, 2, 3, 4, 5];
console.log(Reflect.deleteProperty(arr, '3')); // true
console.log(arr); // [1, 2, 3, , 5]
// 如果属性不存在,返回 true
console.log(Reflect.deleteProperty({}, 'foo')); // true
// 如果属性不可配置,返回 false
console.log(Reflect.deleteProperty(Object.freeze({ foo: 1 }), 'foo')); // false
Reflect.get(target, propertyKey[, receiver])
获取对象身上某个属性的值,类似于 target[name]。
let obj3 = { x: 1, y: 2 };
console.log(Reflect.get(obj3, 'x')); // 1
// Array
console.log(Reflect.get(['zero', 'one'], 1)); // "one"
// Proxy with a get handler
const x = { p: 1 };
const objProxy = new Proxy(x, {
get(t, k, r) {
return k + 'bar';
},
});
console.log(Reflect.get(objProxy, 'foo')); // "foobar"
Reflect.getOwnPropertyDescriptor(target, propertyKey)
类似于 Object.getOwnPropertyDescriptor()。如果给定属性存在于对象上,则返回它的属性描述符,否则返回 undefined。
console.log(Reflect.getOwnPropertyDescriptor({ x: 'hello' }, 'x'));
// {value: "hello", writable: true, enumerable: true, configurable: true}
console.log(Reflect.getOwnPropertyDescriptor({ x: 'hello' }, 'y'));
// undefined
console.log(Reflect.getOwnPropertyDescriptor([], 'length'));
// {value: 0, writable: true, enumerable: false, configurable: false}
Reflect.getPrototypeOf(target)
类似于 Object.getPrototypeOf()。返回指定对象的原型
// 如果参数为 Object,返回结果相同
console.log(Object.getPrototypeOf({})); // {}
console.log(Reflect.getPrototypeOf({})); // {}
// 在 ES5 规范下,对于非 Object,抛异常
console.log(Object.getPrototypeOf('foo')); // Throws TypeError
console.log(Reflect.getPrototypeOf('foo')); // Throws TypeError
// 在 ES2015 规范下,Reflect 抛异常, Object 强制转换非 Object
console.log(Object.getPrototypeOf('foo')); // [String: '']
console.log(Reflect.getPrototypeOf('foo')); // Throws TypeError
// 如果想要模拟 Object 在 ES2015 规范下的表现,需要强制类型转换
console.log(Reflect.getPrototypeOf(Object('foo'))); // [String: '']
Reflect.has(target, propertyKey)
判断一个对象是否存在某个属性,和 in 运算符 的功能完全相同。
console.log(Reflect.has({ x: 0 }, 'x')); // true
console.log(Reflect.has({ x: 0 }, 'y')); // false
// 如果该属性存在于原型链中,返回true
console.log(Reflect.has({ x: 0 }, 'toString')); // true
// Proxy 对象的 .has() 句柄方法
const obj4Proxy = new Proxy(
{},
{
has(t, k) {
return k.startsWith('door');
},
},
);
console.log(Reflect.has(obj4Proxy, 'doorbell')); // true
console.log(Reflect.has(obj4Proxy, 'dormitory')); // false
Reflect.isExtensible(target)
类似于 Object.isExtensible().判断一个对象是否可扩展
const empty = {};
console.log(Reflect.isExtensible(empty)); // === true
Reflect.preventExtensions(empty);
console.log(Reflect.isExtensible(empty)); // === false
Reflect.ownKeys(target)
返回一个包含所有自身属性(不包含继承属性)的数组。(类似于 Object.keys(), 但不会受 enumerable 影响).
console.log(Reflect.ownKeys({ z: 3, y: 2, x: 1 })); // [ "z", "y", "x" ]
console.log(Reflect.ownKeys([1])); // [ "0", "length"]
const sym = Symbol.for('comet');
const sym2 = Symbol.for('meteor');
const obj5 = {
[sym]: 0,
str: 0,
'773': 0,
'0': 0,
[sym2]: 0,
'-1': 0,
'8': 0,
'second str': 0,
};
console.log(Reflect.ownKeys(obj5));
// [ "0", "8", "773", "str", "-1", "second str", Symbol(comet), Symbol(meteor) ]
Reflect.preventExtensions(target)
类似于 Object.preventExtensions()。阻止新属性添加到对象。
const empty = {};
console.log(Reflect.isExtensible(empty)); // === true
Reflect.preventExtensions(empty);
console.log(Reflect.isExtensible(empty)); // === false
Reflect.set(target, propertyKey, value[, receiver])
将值分配给属性的函数。返回一个 Boolean,如果更新成功,则返回 true。
// Object
const obj6 = {};
Reflect.set(obj6, 'prop', 'value');
console.log(obj6.prop); // "value"
// Array
const arr1 = ['duck', 'duck', 'duck'];
Reflect.set(arr1, 2, 'goose');
console.log(arr1[2]); // "goose"
Reflect.setPrototypeOf(target, prototype)
除了返回类型以外,静态方法 Reflect.setPrototypeOf() 与 Object.setPrototypeOf() 方法是一样的。它可设置对象的原型(即内部的 [[Prototype]] 属性)为另一个对象或 null,如果操作成功返回 true,否则返回 false。
console.log(Reflect.setPrototypeOf({}, Object.prototype)); // true
// 可以将对象的[[Prototype]]更改为null。
console.log(Reflect.setPrototypeOf({}, null)); // true
// 如果目标不可扩展,则返回false。
console.log(Reflect.setPrototypeOf(Object.freeze({}), null)); // false
// 如果导致原型链循环,则返回false。
const target = {};
const proto = Object.create(target);
console.log(Reflect.setPrototypeOf(target, proto)); // false
Set
Set 对象允许你存储任何类型的唯一值,无论是原始值或者是对象引用。
Set 对象是值的集合,你可以按照插入的顺序迭代它的元素。 Set 中的元素只会出现一次,即 Set 中的元素是唯一的。
demo
/**
* add() 方法用来向一个 Set 对象的末尾添加一个指定的值
*/
const mySet = new Set();
mySet.add(1);
mySet.add(5).add('some text').add(1); // 可以链式调用
console.log(mySet);
// Set {1, 5, "some text"} 重复的值没有被添加进去
/**
* delete() 方法可以从一个 Set 对象中删除指定的元素。
*/
mySet.delete('bar'); // 返回 false,不包含 "bar" 这个元素
mySet.delete(1); // 返回 true,删除成功
console.log(mySet); // Set {5, "some text"}
/**
* clear() 方法用来清空一个 Set 对象中的所有元素。
*/
mySet.clear();
console.log(mySet); // Set {}
/**
* entries() 方法返回一个新的迭代器对象 ,这个对象的元素是类似 [value, value] 形式的数组,value 是集合对象中的每个元素,迭代器对象元素的顺序即集合对象中元素插入的顺序。由于集合对象不像 Map 对象那样拥有 key,然而,为了与 Map 对象的 API 形式保持一致,故使得每一个 entry 的 key 和 value 都拥有相同的值,因而最终返回一个 [value, value] 形式的数组。
*/
mySet.add('foobar');
mySet.add(1);
mySet.add('baz');
const setIter = mySet.entries();
console.log(setIter.next().value); // ["foobar", "foobar"]
console.log(setIter.next().value); // [1, 1]
console.log(setIter.next().value); // ["baz", "baz"]
/**
* forEach 方法会根据集合中元素的插入顺序,依次执行提供的回调函数。
*/
function logSetElements(value1, value2, set) {
console.log(`s[ ${value1} ] = ${value2} =>${[...set]}`);
}
new Set(['foo', 'bar', undefined]).forEach(logSetElements);
// s[ foo ] = foo =>foo,bar,
// s[ bar ] = bar =>foo,bar,
// s[ undefined ] = undefined =>foo,bar,
/**
* has() 方法返回一个布尔值来指示对应的值value是否存在Set对象中。
*/
console.log(mySet.has('foobar')); // 返回 true
console.log(mySet.has('bar')); // 返回 false
const obj1 = { key1: 1 };
mySet.add(obj1);
console.log(mySet.has(obj1)); // 返回 true
console.log(mySet.has({ key1: 1 })); // 会返回 false,因为其是另一个对象的引用
mySet.add({ key1: 1 }); // 现在 mySet 中有2条(不同引用的)对象了
console.log(mySet);
// Set { 'foobar', 1, 'baz', { key1: 1 }, { key1: 1 } }
/**
* values() 方法返回一个 Iterator 对象,该对象按照原Set 对象元素的插入顺序返回其所有元素。
*/
const setIter1 = mySet.values();
console.log(setIter1.next().value); // foobar
console.log(setIter1.next().value); // 1
console.log(setIter1.next().value); // baz
/**
* keys() 方法是这个方法的别名 (与 Map 对象相似); 它的行为与 value 方法完全一致,返回 Set 对象的元素。
*/
const setIter2 = mySet.keys();
console.log(setIter2.next().value); // foobar
console.log(setIter2.next().value); // 1
console.log(setIter2.next().value); // baz
console.log('---------------------');
/**
* @@iterator 属性的初始值和 values 属性的初始值是同一个函数。
*/
for (const v of mySet) {
console.log(v);
}
Map
Map 对象保存键值对,并且能够记住键的原始插入顺序。任何值(对象或者原始值) 都可以作为一个键或一个值。
描述
一个 Map 对象在迭代时会根据对象中元素的插入顺序来进行 — 一个 for...of 循环在每次迭代后会返回一个形式为[key,value]的数组。
Objects 和 maps 的比较
Objects 和 Maps 类似,它们都允许你按键存取一个值、删除键、检测一个键是否绑定了值。因此(并且也没有其他内建的替代方式了)过去我们一直都把对象当成 Maps 使用。不过 Maps 和 Objects 有一些重要的区别,在下列情况里使用 Map 会是更好的选择:
Map | Object | |
---|---|---|
意外的键 | Map 默认情况不包含任何键。只包含显式插入的键。 | 一个 Object 有一个原型, 原型链上的键名有可能和你自己在对象上的设置的键名产生冲突。 |
键的类型 | 一个 Map 的键可以是任意值,包括函数、对象或任意基本类型。 | 一个 Object 的键必须是一个 String 或是 Symbol。 |
键的顺序 | Map 中的 key 是有序的。因此,当迭代的时候,一个 Map 对象以插入的顺序返回键值。 | 一个 Object 的键是无序的 |
Size | Map 的键值对个数可以轻易地通过 size 属性获取 | Object 的键值对个数只能手动计算 |
迭代 | Map 是 iterable 的,所以可以直接被迭代。 | 迭代一个 Object 需要以某种方式获取它的键然后才能迭代。 |
性能 | 在频繁增删键值对的场景下表现更好。 | 在频繁添加和删除键值对的场景下未作出优化。 |
demo
/**
* set() 方法为 Map 对象添加或更新一个指定了键(key)和值(value)的(新)键值对。
*/
const myMap = new Map();
myMap.set('bar', 'baz');
myMap.set(1, 'foo');
console.log(myMap); // Map { 'bar' => 'baz', 1 => 'foo' }
/**
* get() 方法返回某个 Map 对象中的一个指定元素。
*/
console.log(myMap.get('bar')); // foo
console.log(myMap.get('baz')); // undefined
/**
* forEach() 方法将会以插入顺序对 Map 对象中的每一个键值对执行一次参数中提供的回调函数。
*/
function logMapElements(value, key, map) {
console.log(`'m[ ${key} ] = ${value} => ${map}`);
}
new Map([
['foo', 3],
['bar', {}],
['baz', undefined],
]).forEach(logMapElements);
// m[foo] = 3 => [object Map]
// m[bar] = [object Object] => [object Map]
// m[baz] = undefined => [object Map]
/**
* entries() 方法返回一个新的包含 [key, value] 对的 Iterator 对象,返回的迭代器的迭代顺序与 Map 对象的插入顺序相同。
*/
const mapIter = myMap.entries();
console.log(mapIter.next().value); // [ 'bar', 'baz' ]
console.log(mapIter.next().value); // [ 1, 'foo' ]
/**
* 方法has() 返回一个bool值,用来表明map 中是否存在指定元素.
*/
console.log(myMap.has('bar')); // returns true
console.log(myMap.has('baz')); // returns false
/**
* keys() 返回一个引用的 Iterator 对象。它包含按照顺序插入 Map 对象中每个元素的key值。
*/
const mapIter1 = myMap.keys();
console.log(mapIter1.next().value); // bar
console.log(mapIter1.next().value); // 1
/**
* values() 方法返回一个新的Iterator对象。它包含按顺序插入Map对象中每个元素的value值。
*/
const mapIter2 = myMap.values();
console.log(mapIter2.next().value); // baz
console.log(mapIter2.next().value); // foo
/**
* @@iterator 属性的初始值与 entries 属性的初始值是同一个函数对象。
*/
for (const entry of myMap) {
console.log(entry);
}
// [ 'bar', 'baz' ]
// [ 1, 'foo' ]
/**
* delete() 方法用于移除 Map 对象中指定的元素。
*/
myMap.delete('bar');
console.log(myMap); // Map { 1 => 'foo' }
/**
* clear()方法会移除Map对象中的所有元素。
*/
myMap.clear();
console.log(myMap); // Map { }
Symbol
symbol 是一种基本数据类型 (primitive data type)。Symbol()函数会返回 symbol 类型的值,该类型具有静态属性和静态方法。它的静态属性会暴露几个内建的成员对象;它的静态方法会暴露全局的 symbol 注册,且类似于内建对象类,但作为构造函数来说它并不完整,因为它不支持语法:"new Symbol()"。
每个从 Symbol()返回的 symbol 值都是唯一的。一个 symbol 值能作为对象属性的标识符;这是该数据类型仅有的目的。
描述
直接使用 Symbol()创建新的 symbol 类型,并用一个可选的字符串作为其描述。
const sym1 = Symbol();
const sym2 = Symbol('foo');
const sym3 = Symbol('foo');
上面的代码创建了三个新的 symbol 类型。 注意,Symbol("foo") 不会强制将字符串 “foo” 转换成 symbol 类型。它每次都会创建一个新的 symbol 类型:
console.log(Symbol('foo') === Symbol('foo')); // false
下面带有 new 运算符的语法将抛出 TypeError 错误:
const sym = new Symbol(); // TypeError
这会阻止创建一个显式的 Symbol 包装器对象而不是一个 Symbol 值。围绕原始数据类型创建一个显式包装器对象从 ECMAScript 6 开始不再被支持。 然而,现有的原始包装器对象,如 new Boolean、new String 以及 new Number,因为遗留原因仍可被创建。
如果你真的想创建一个 Symbol 包装器对象 (Symbol wrapper object),你可以使用 Object() 函数:
const sym = Symbol('foo');
console.log(typeof sym); // "symbol"
const symObj = Object(sym);
console.log(typeof symObj); // "object"
全局共享的 Symbol
上面使用 Symbol() 函数的语法,不会在你的整个代码库中创建一个可用的全局 symbol 类型。 要创建跨文件可用的 symbol,甚至跨域(每个都有它自己的全局作用域) , 使用 Symbol.for() 方法和 Symbol.keyFor() 方法从全局的 symbol 注册表设置和取得 symbol。
在对象中查找 Symbol 属性
Object.getOwnPropertySymbols() 方法让你在查找一个给定对象的符号属性时返回一个 symbol 类型的数组。注意,每个初始化的对象都是没有自己的 symbol 属性的,因此这个数组可能为空,除非你已经在对象上设置了 symbol 属性。
方法
/**
* Symbol.for(key) 方法会根据给定的键 key,来从运行时的 symbol 注册表中找到对应的 symbol,如果找到了,则返回它,否则,新建一个与该键关联的 symbol,并放入全局 symbol 注册表中。
*/
Symbol('foo'); // 创建一个 symbol 并放入 symbol 注册表中,键为 "foo"
console.log(Symbol.for('foo')); // 从 symbol 注册表中读取键为"foo"的 symbol
console.log(Symbol.for('bar') === Symbol.for('bar')); // true,证明了上面说的
console.log(Symbol('bar') === Symbol('bar')); // false,Symbol() 函数每次都会返回新的一个 symbol
const sym = Symbol.for('mario');
console.log(sym.toString());
// "Symbol(mario)",mario 既是该 symbol 在 symbol 注册表中的键名,又是该 symbol 自身的描述字符串
/**
* Symbol.keyFor(sym) 方法用来获取 symbol 注册表中与某个 symbol 关联的键。
*/
// 创建一个 symbol 并放入 Symbol 注册表,key 为 "foo"
const globalSym = Symbol.for('foo');
console.log(Symbol.keyFor(globalSym)); // foo
// 创建一个 symbol,但不放入 symbol 注册表中
const localSym = Symbol();
console.log(Symbol.keyFor(localSym)); // undefined,所以是找不到 key 的
// well-known symbol 们并不在 symbol 注册表中
console.log(Symbol.keyFor(Symbol.iterator)); // undefined
demo
对 symbol 使用 typeof 运算符
console.log(typeof Symbol() === 'symbol'); // true
console.log(typeof Symbol('foo') === 'symbol'); // true
console.log(typeof Symbol.iterator === 'symbol'); // true
Symbol 类型转换
当使用 symbol 值进行类型转换时需要注意一些事情:
- 尝试将一个 symbol 值转换为一个 number 值时,会抛出一个 TypeError 错误 (e.g. +sym or sym | 0).
- 使用宽松相等时, Object(sym) == sym returns true.
- 这会阻止你从一个 symbol 值隐式地创建一个新的 string 类型的属性名。例如,Symbol("foo") + "bar" 将抛出一个 TypeError (can't convert symbol to string).
- "safer" String(sym) conversion 的作用会像 symbol 类型调用 Symbol.prototype.toString() 一样,但是注意 new String(sym) 将抛出异常。
Symbols 与 for...in 迭代
Symbols 在 for...in 迭代中不可枚举。另外,Object.getOwnPropertyNames() 不会返回 symbol 对象的属性,但是你能使用 Object.getOwnPropertySymbols() 得到它们。
const obj = {};
obj[Symbol('a')] = 'a';
obj[Symbol.for('b')] = 'b';
obj['c'] = 'c';
obj.d = 'd';
for (const i in obj) {
console.log(i);
}
// c
// d
Symbols 与 JSON.stringify()
当使用 JSON.stringify() 时,以 symbol 值作为键的属性会被完全忽略:
console.log(JSON.stringify({ [Symbol('foo')]: 'foo' }));
// '{}'
Symbol 包装器对象作为属性的键
当一个 Symbol 包装器对象作为一个属性的键时,这个对象将被强制转换为它包装过的 symbol 值:
const sym1 = Symbol('foo');
const obj1 = { [sym1]: 1 };
console.log(obj1[sym1]); // 1
console.log(obj1[Object(sym1)]); // 1
Iterator(迭代协议)
作为 ECMAScript 2015 的一组补充规范,迭代协议并不是新的内置实现或语法,而是协议。这些协议可以被任何遵循某些约定的对象来实现。
迭代协议具体分为两个协议:可迭代协议和迭代器协议。
可迭代协议
可迭代协议允许 JavaScript 对象定义或定制它们的迭代行为,例如,在一个 for..of 结构中,哪些值可以被遍历到。一些内置类型同时是内置可迭代对象,并且有默认的迭代行为,比如 Array 或者 Map,而其他内置类型则不是(比如 Object))。
要成为可迭代对象, 一个对象必须实现 @@iterator 方法。这意味着对象(或者它原型链上的某个对象)必须有一个键为 @@iterator 的属性,可通过常量 Symbol.iterator 访问该属性:
属性 | 值 |
---|---|
[Symbol.iterator] | 返回一个符合迭代器协议的对象的无参数函数。 |
当一个对象需要被迭代的时候(比如被置入一个 for...of 循环时),首先,会不带参数调用它的 @@iterator 方法,然后使用此方法返回的迭代器获得要迭代的值。
值得注意的是调用此零个参数函数时,它将作为对可迭代对象的方法进行调用。 因此,在函数内部,this 关键字可用于访问可迭代对象的属性,以决定在迭代过程中提供什么。
此函数可以是普通函数,也可以是生成器函数,以便在调用时返回迭代器对象。 在此生成器函数的内部,可以使用 yield 提供每个条目。
迭代器协议
迭代器协议定义了产生一系列值(无论是有限个还是无限个)的标准方式。当值为有限个时,所有的值都被迭代完毕后,则会返回一个默认返回值。
只有实现了一个拥有以下语义(semantic)的 next() 方法,一个对象才能成为迭代器:
属性 | 值 |
---|---|
next | 一个无参数函数,返回一个应当拥有以下两个属性的对象: done(boolean) 如果迭代器可以产生序列中的下一个值,则为 false。(这等价于没有指定 done 这个属性。) 如果迭代器已将序列迭代完毕,则为 true。这种情况下,value 是可选的,如果它依然存在,即为迭代结束之后的默认返回值。 value 迭代器返回的任何 JavaScript 值。done 为 true 时可省略。 next() 方法必须返回一个对象,该对象应当有两个属性: done 和 value,如果返回了一个非对象值(比如 false 或 undefined),则会抛出一个 TypeError 异常("iterator.next() returned a non-object value")。 |
demo
// 简单迭代器
const makeIterator = (array) => {
this.index = 0;
return {
next: () => {
return {
value: array[this.index++],
done: this.index > array.length,
};
},
};
};
let mIterator = makeIterator([1, 2]);
console.log(mIterator.next());
console.log(mIterator.next());
console.log(mIterator.next());
// { value: 1, done: false }
// { value: 2, done: false }
// { value: undefined, done: true }
// 对象迭代器
const obj = {
names: ['tom', 'jack', 'jeer'],
age: [22, 11, 33],
[Symbol.iterator]() {
let array = [...this.names, ...this.age];
return {
index: 0,
next() {
return {
value: array[this.index++],
done: this.index > array.length,
};
},
};
},
};
for (let item of obj) {
console.log(item);
}
// tom
// jack
// jeer
// 22
// 11
// 33