ES6 详细笔记
ES6中的特性
- let 和 const
- 变量的赋值解构
- 字符串的扩展
- 正则的扩展
- 数值的扩展
- 函数的扩展
- 对象的扩展
- 运算符的扩展
- Symbol
- Set 和 Map
- Proxy
- Reflect
- Promise
- Iterator
- for … of
- Generator
- async
- Class
- Module
这里按照ES6标准入门的目录顺序写的笔记,只写了一部分,后序的部分分成了好几篇笔记写的。
文章目录
- ES6 详细笔记
1.let/const
Ⅰ-概括与总结
声明
- const命令: 声明常量
- let命令: 声明变量
作用范围
var命令在全局代码中执行const命令和let命令只能在代码块中执行
赋值使用
const命令声明常量后必须立马赋值let命令声明变量后可立马赋值或使用时赋值
注:声明方法: var、const、let、function、class、import
重点难点
- 不允许重复声明
- 未定义就使用会报错:
const命令和let命令不存在变量提升 - 暂时性死区: 在代码块内使用
const命令和let命令声明变量之前, 该变量都不可用
Ⅱ-let关键字
let 关键字用来声明变量, 使用 let 声明的变量有几个特点:
- 不允许重复声明
- 块级作用域
- 不存在变量提升
- 不影响作用域链
Ⅲ-const关键字
const 关键字用来声明常量 , const 声明有以下特点:
- 不允许重复声明
值不允许修改- 不存在变量提升
- 块级作用域
- 声明必须赋初始值
- 标识符一般为大写
注意: 对象属性修改和数组元素变化不会触发 const 错误
Ⅳ-块级作用域
① 为什么需要块级作用域?
ES5 只有全局作用域和函数作用域, 没有块级作用域, 这带来很多不合理的场景.
第一种场景, 内层变量可能会覆盖外层变量.
var tmp = new Date();
function f() {
console.log(tmp);
if (false) { var tmp = 'hhhh'; }
}
f(); // undefined
/*********** 上面写法实际上等于这样 **********************/
var tmp = new Date();
function f() {
var tmp = undefined;
console.log(tmp); //所以这里打印是undefined
if (false) { tmp = 'hhhh'; }
}
上面代码的原意是, if代码块的外部使用外层的tmp变量, 内部使用内层的tmp变量. 但是, 函数 [ f ] 执行后, 输出结果为 undefined , 原因在于变量提升, 导致内层的tmp变量覆盖了外层的tmp变量.
第二种场景, 用来计数的循环变量泄露为全局变量.
var s = 'hhhhhh';
for (var i = 0; i < s.length; i++) { console.log(s[i]);}
console.log(i); // 6
② ES6 的块级作用域
let实际上为 JavaScript 新增了块级作用域.
function f1() {
let n = 5;
if (true) { let n = 10; }
console.log(n); // 5
}
上面的函数有两个代码块, 都声明了变量n, 运行后输出 5. 这表示外层代码块不受内层代码块的影响. 如果两次都使用var定义变量n, 最后输出的值才是 10.
ES6 允许块级作用域的任意嵌套.
{{{{
{let insane = 'Hello World'}
console.log(insane); // 报错 因为外层不能取到内层数据
}}}};
上面代码使用了一个五层的块级作用域, 每一层都是一个单独的作用域. 第四层作用域无法读取第五层作用域的内部变量.
内层作用域可以定义外层作用域的同名变量.
{{{{
let insane = 'Hello World';
{let insane = 'Hello World'} //可以这样命名,不会报错
}}}};
块级作用域的出现, 实际上使得获得广泛应用的匿名立即执行函数表达式(匿名 IIFE)不再必要了.
// IIFE 写法
(function () {
var tmp ;
...
}());
// 块级作用域写法
{
let tmp ;
...
}
③ 块级作用域与函数声明
ES6 引入了块级作用域, 明确允许在块级作用域之中声明函数. ES6 规定, 块级作用域之中, 函数声明语句的行为类似于let, 在块级作用域之外不可引用.
举例:
function f() { console.log('我在外面!'); }
(function () {
// 重复声明一次函数f
if (false) { function f() { console.log('我在里面!'); }}
f();
}());
如果在 ES6 浏览器中运行一下上面的代码, 是会报错的, 这是为什么呢?
// 浏览器的 ES6 环境
function f() { console.log('我在外面!'); }
(function () {
// 重复声明一次函数f
if (false) { function f() { console.log('我在里面!'); } }
f();
}());
// Uncaught TypeError: f is not a function
原来, 如果改变了块级作用域内声明的函数的处理规则, 显然会对老代码产生很大影响. 为了减轻因此产生的不兼容问题 , ES6 在附录 B里面规定, 浏览器的实现可以不遵守上面的规定, 有自己的行为方式.
- 允许在块级作用域内声明函数.
- 函数声明类似于
var, 即会提升到全局作用域或函数作用域的头部. - 同时, 函数声明还会提升到所在的块级作用域的头部.
注意, 上面三条规则只对 ES6 的浏览器实现有效, 其他环境的实现不用遵守, 还是将块级作用域的函数声明当作let处理.
根据这三条规则, 浏览器的 ES6 环境中, 块级作用域内声明的函数, 行为类似于var声明的变量. 上面的栗子实际运行的代码如下.
// 浏览器的 ES6 环境
function f() { console.log('我在外面!'); }
(function () {
var f = undefined;
if (false) { function f() { console.log('我在里面!'); }}
f();
}());
// Uncaught TypeError: f is not a function
考虑到环境导致的行为差异太大, 应该避免在块级作用域内声明函数. 如果确实需要, 也应该写成函数表达式, 而不是函数声明语句.
// 块级作用域内部的函数声明语句, 建议不要使用
{
let a = 'secret';
function f() { return a; }
}
// 块级作用域内部, 优先使用函数表达式
{
let a = 'secret';
let f = function () {
return a;
};
}
另外, 还有一个需要注意的地方. ES6 的块级作用域必须有大括号, 如果没有大括号 , JavaScript 引擎就认为不存在块级作用域.
// 第一种写法, 报错
if (true) let x = 1;
// 第二种写法, 不报错
if (true) {
let x = 1;
}
2.赋值解构
Ⅰ-概括总结
如果解构不成功,变量的值就等于undefined。
let [foo] = [];
let [bar, foo] = [1];
以上两种情况都属于解构不成功,foo的值都会等于undefined。
undefined 和 null 无法转为对象, 因此无法进行解构
- 匹配模式: 只要等号两边的模式相同, 左边的变量就会被赋予对应的值
- 解构赋值规则: 只要等号右边的值不是对象或数组, 就先将其转为对象
Ⅱ-基本用法
① 默认值
解构赋值允许指定默认值.
let [foo = true] = [];//foo = true
let [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'
如果默认值是一个表达式, 那么这个表达式是惰性求值的, 即只有在用到的时候, 才会求值.
function f() { console.log('aaa');}
let [x = f()] = [1]; //1
上面代码中, 因为x能取到值, 所以函数 [ f ] 根本不会执行. 上面的代码其实等价于下面的代码.
let x;
if ([1] === undefined) { x = f()}
else { x = [1]; }
默认值可以引用解构赋值的其他变量, 但该变量必须已经声明.
let [x = 1, y = x] = []; // x=1; y=1
let [x = 1, y = x] = [2]; // x=2; y=2
let [x = 1, y = x] = [1, 2]; // x=1; y=2
let [x = y, y = 1] = []; // ReferenceError: y is not defined
注:默认值生效的条件是,对象的属性值严格等于undefined。
上面最后一个表达式之所以会报错, 是因为x用y做默认值时, y还没有声明.
解释:let [x = 1, y = x] = [2]; // x=2; y=2
因为右边的 2 不严格等于 undefined,所以默认值(x=1)不生效,所以 x 取值为右边的 2.
Ⅲ-对象的赋值解构
对象的解构与数组有一个重要的不同. 数组的元素是按次序排列的, 变量的取值由它的位置决定;而对象的属性没有次序, 变量必须与属性同名, 才能取到正确的值
let { foo, bar } = { foo: 'aaa', bar: 'bbb' };//foo = "aaa"; bar = "bbb"
如果解构失败, 变量的值等于 undefined .
let {foo} = {bar: 'baz'};//foo = undefined
Ⅳ-字符串的赋值结构
字符串也可以解构赋值. 这是因为此时, 字符串被转换成了一个类似数组的对象.
const [a, b, c, d, e] = 'hello';
//a == "h" ;b == "e" ; c == "l" ; d == "l" ;e == "o"
类似数组的对象都有一个length属性, 因此还可以对这个属性解构赋值.
let {length : len} = 'hello';//len == 5
Ⅴ-函数参数的解构赋值
[[1, 2], [3, 4]].map(([a, b]) => a + b);
// [ 3, 7 ]
函数参数的解构也可以使用默认值.
function move({x = 0, y = 0} = {}) { return [x, y];}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, 0]
move({}); // [0, 0]
move(); // [0, 0]
注意, 下面的写法会得到不一样的结果.
function move({x, y} = { x: 0, y: 0 }) {
return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, undefined]
move({}); // [undefined, undefined]
move(); // [0, 0]
上面代码是为函数move的参数指定默认值, 而不是为变量x和y指定默认值, 所以会得到与前一种写法不同的结果.
undefined 就会触发函数参数的默认值.
[1, undefined, 3].map((x = 'yes') => x);
// [ 1, 'yes', 3 ]
Ⅶ-具体应用场景举例
① 交换变量的值
let x = 1;
let y = 2;
[x, y] = [y, x];
上面代码交换变量x和y的值, 这样的写法不仅简洁, 而且易读, 语义非常清晰.
② 从函数返回多个值
函数只能返回一个值, 如果要返回多个值, 只能将它们放在数组或对象里返回. 有了解构赋值, 取出这些值就非常方便.
// 返回一个数组
function example() { return [1, 2, 3]; }
let [a, b, c] = example();
// 返回一个对象
function example() {
return { foo: 1,bar: 2};
}
let { foo, bar } = example();
③ 函数参数的定义
解构赋值可以方便地将一组参数与变量名对应起来.
// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);
// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});
④ 提取 JSON 数据
解构赋值对提取 JSON 对象中的数据, 尤其有用.
let jsonData = {
id: 42,
status: "OK",
data: [867, 5309]
};
let { id, status, data: number } = jsonData;
console.log(id, status, number);
// 42, "OK", [867, 5309]
⑤ 函数参数的默认值
jQuery.ajax = function (url, {
async = true,
beforeSend = function () {},
cache = true,
complete = function () {},
crossDomain = false,
global = true,
// ... more config
} = {}) {
// ... do stuff
};
指定参数的默认值,就避免了在函数体内部再写var foo = config.foo || 'default foo';这样的语句。
3.字符串的拓展
Ⅰ-概括总结
-
字符串遍历: 可通过 [ for-of ] 遍历字符串
-
字符串模板: 可单行可多行可插入变量的增强版字符串
-
标签模板: 函数参数的特殊调用
-
repeat(): 把字符串重复n次, 返回
新字符串 -
matchAll(): 返回正则表达式在字符串的所有匹配
-
includes(): 是否存在指定字符串
-
startsWith(): 是否存在字符串头部指定字符串
-
endsWith(): 是否存在字符串尾部指定字符串
Ⅱ-模板字符串
$('#result').append(
'There are <b>' + basket.count + '</b> ' +
'items in your basket, ' +
'<em>' + basket.onSale +
'</em> are on sale!'
);
上面这种写法相当繁琐不方便,ES6 引入了模板字符串解决这个问题。
$('#result').append(`
There are <b>${basket.count}</b> items
in your basket, <em>${basket.onSale}</em>
are on sale!
`);
举例:
// 普通字符串
`In JavaScript '\n' is a line-feed.`
// 多行字符串
`In JavaScript this is
not legal.`
console.log(`string text line 1
string text line 2`);
// 字符串中嵌入变量
let name = "Bob", time = "today";
`Hello ${name}, how are you ${time}?`
4.函数的拓展
Ⅰ-函数参数的默认值
ES6 之前,不能直接为函数的参数指定默认值,只能采用变通的方法。
function log(x, y) {
y = y || 'World';
console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello World
上面代码检查函数log的参数y有没有赋值,如果没有,则指定默认值为World。这种写法的缺点在于,如果参数y赋值了,但是对应的布尔值为false,则该赋值不起作用。就像上面代码的最后一行,参数y等于空字符,结果被改为默认值。
为了避免这个问题,通常需要先判断一下参数y是否被赋值,如果没有,再等于默认值。
if (typeof y === 'undefined') {
y = 'World';
}
ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。
①参数默认值的位置
通常情况下, 定义了默认值的参数, 应该是函数的尾参数. 因为这样比较容易看出来, 到底省略了哪些参数. 如果非尾部的参数设置默认值, 实际上这个参数是没法省略的.
// 例一
function f(x = 1, y) { return [x, y];}
f() // [1, undefined]
f(2) // [2, undefined]
f(, 1) // 报错
f(undefined, 1) // [1, 1]
// 例二
function f(x, y = 5, z) { return [x, y, z];}
f() // [undefined, 5, undefined]
f(1) // [1, 5, undefined]
f(1, ,2) // 报错
f(1, undefined, 2) // [1, 5, 2]
上面代码中, 有默认值的参数都不是尾参数. 这时, 无法只省略该参数, 而不省略它后面的参数, 除非显式输入 undefined .
如果传入 undefined , 将触发该参数等于默认值, null 则没有这个效果.
function foo(x = 5, y = 6) { console.log(x, y); }
foo(undefined, null)
// 5 null
上面代码中, x参数对应 undefined , 结果触发了默认值, y参数等于 null , 就没有触发默认值.
② 函数的 length 属性
指定了默认值以后, 函数的length属性, 将返回没有指定默认值的参数个数. 也就是说, 指定了默认值后 , length属性将失真.
(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
(function(...args) {}).length // 0
如果设置了默认值的参数不是尾参数, 那么[ length ]属性也不再计入后面的参数了.
(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1
Ⅲ - 箭头函数
箭头函数有几个使用注意点.
(1)函数体内的 [ this ] 对象, 就是定义时所在的对象, 而不是使用时所在的对象.
(2)不可以当作构造函数, 也就是说, 不可以使用new命令, 否则会抛出一个错误.
(3)不可以使用arguments对象, 该对象在函数体内不存在. 如果要用, 可以用 rest 参数代替.
(4)不可以使用yield命令, 因此箭头函数不能用作 Generator 函数.
①不适用场合
由于箭头函数使得 [ this ] 从“动态”变成“静态”, 下面两个场合不应该使用箭头函数.
第一个场合是定义对象的方法, 且该方法内部包括 [ this ] .
const cat = {
lives: 9,
jumps: () => { this.lives--;}
}
上面代码中, cat.jumps()方法是一个箭头函数, 这是错误的. 调用cat.jumps()时, 如果是普通函数, 该方法内部的 [ this ] 指向cat;如果写成上面那样的箭头函数, 使得 [ this ] 指向全局对象, 因此不会得到预期结果. 这是因为对象不构成单独的作用域, 导致jumps箭头函数定义时的作用域就是全局作用域.
第二个场合是需要动态 [ this ] 的时候, 也不应使用箭头函数.
var button = document.getElementById('press');
button.addEventListener('click', () => {
this.classList.toggle('on');
});
上面代码运行时, 点击按钮会报错, 因为button的监听函数是一个箭头函数, 导致里面的 [ this ] 就是全局对象. 如果改成普通函数, [ this ] 就会动态指向被点击的按钮对象.
Ⅳ - rest 参数
ES6 引入 rest 参数(形式为...变量名), 用于获取函数的多余参数, 这样就不需要使用arguments对象了. rest 参数搭配的变量是一个数组, 该变量将多余的参数放入数组中.
function add(...values) {
let sum = 0;
for (var val of values) {
sum += val;
}
return sum;
}
add(2, 5, 3) // 10
注意 , rest 参数之后不能再有其他参数(即只能是最后一个参数), 否则会报错.
// 报错
function f(a, ...b, c) {
// ...
}
函数的length属性, 不包括 rest 参数
(function(a) {}).length // 1
(function(...a) {}).length // 0
(function(a, ...b) {}).length // 1
Ⅴ - 严格模式
ES2016 做了一点修改, 规定只要函数参数使用了默认值、解构赋值、或者扩展运算符, 那么函数内部就不能显式设定为严格模式, 否则会报错.
// 报错
function doSomething(a, b = a) {
'use strict';
// code
}
// 报错
const doSomething = function ({a, b}) {
'use strict';
// code
};
// 报错
const doSomething = (...a) => {
'use strict';
// code
};
const obj = {
// 报错
doSomething({a, b}) {
'use strict';
// code
}
};
这样规定的原因是, 函数内部的严格模式, 同时适用于函数体和函数参数. 但是, 函数执行的时候, 先执行函数参数, 然后再执行函数体. 这样就有一个不合理的地方, 只有从函数体之中, 才能知道参数是否应该以严格模式执行, 但是参数却应该先于函数体执行.
// 报错
function doSomething(value = 070) {
'use strict';
return value;
}
上面代码中, 参数value的默认值是八进制数070, 但是严格模式下不能用前缀0表示八进制, 所以应该报错. 但是实际上 , JavaScript 引擎会先成功执行value = 070, 然后进入函数体内部, 发现需要用严格模式执行, 这时才会报错.
虽然可以先解析函数体代码, 再执行参数代码, 但是这样无疑就增加了复杂性. 因此, 标准索性禁止了这种用法, 只要参数使用了默认值、解构赋值、或者扩展运算符, 就不能显式指定严格模式.
两种方法可以规避这种限制. 第一种是设定全局性的严格模式, 这是合法的.
'use strict';
function doSomething(a, b = a) {
// code
}
第二种是把函数包在一个无参数的立即执行函数里面.
const doSomething = (function () {
'use strict';
return function(value = 42) {
return value;
};
}());
Ⅵ - name 属性
函数的name属性, 返回该函数的函数名.
function foo() {}
foo.name // "foo"
需要注意的是 , ES6 对这个属性的行为做出了一些修改. 如果将一个匿名函数赋值给一个变量 , ES5 的name属性, 会返回空字符串, 而 ES6 的name属性会返回实际的函数名.
var f = function () {};
// ES5
f.name // ""
// ES6
f.name // "f"
如果将一个具名函数赋值给一个变量, 则 ES5 和 ES6 的name属性都返回这个具名函数原本的名字.
const bar = function baz() {};
// ES5
bar.name // "baz"
// ES6
bar.name // "baz"
Function构造函数返回的函数实例, name属性的值为anonymous.
(new Function).name // "anonymous"
5.数组的拓展
Ⅰ- 概括与总结
新增的拓展
- 扩展运算符(…): 转换数组为用逗号分隔的参数序列(
[...arr], 相当于rest/spread参数的逆运算) - Array.from(): 转换具有 [ Iterator接口 ] 的数据结构为真正数组, 返回新数组
- 类数组对象:
包含length的对象、Arguments对象、NodeList对象 - 可遍历对象:
String、Set结构、Map结构、Generator函数
- 类数组对象:
- Array.of(): 转换一组值为真正数组, 返回新数组
- 实例方法
- copyWithin(): 把指定位置的成员复制到其他位置, 返回原数组
- find(): 返回第一个符合条件的成员
- findIndex(): 返回第一个符合条件的成员索引值
- fill(): 根据指定值填充整个数组, 返回原数组
- keys(): 返回以索引值为遍历器的对象
- values(): 返回以属性值为遍历器的对象
- entries(): 返回以索引值和属性值为遍历器的对象
- 其他:毕竟只是概述,不过多列举,详细看下方
- 数组空位: ES6明确将数组空位转为 undefined (空位处理规不一, 建议避免出现)
扩展运算符在数组中的应用
- 克隆数组:
const arr = [...arr1] - 合并数组:
const arr = [...arr1, ...arr2] - 拼接数组:
arr.push(...arr1) - 代替apply:
Math.max.apply(null, [x, y])=>Math.max(...[x, y]) - 转换字符串为数组:
[..."hello"] - 转换类数组对象为数组:
[...Arguments, ...NodeList] - 转换可遍历对象为数组:
[...String, ...Set, ...Map, ...Generator] - 与数组解构赋值结合:
const [x, ...rest/spread] = [1, 2, 3] - 计算Unicode字符长度:
Array.from("hello").length=>[..."hello"].length
Ⅱ - 扩展运算符
① 含义
扩展运算符(spread)是三个点(...). 它好比 rest 参数的逆运算, 将一个数组转为用逗号分隔的参数序列.
② 替代函数的 apply 方法
由于扩展运算符可以展开数组, 所以不再需要apply方法, 将数组转为函数的参数了.
// ES5 的写法
function f(x, y, z) {
// ...
}
var args = [0, 1, 2];
f.apply(null, args);
// ES6的写法
function f(x, y, z) {
// ...
}
let args = [0, 1, 2];
f(...args);
下面是扩展运算符取代apply方法的一个实际的栗子, 应用Math.max方法, 简化求出一个数组最大元素的写法.
// ES5 的写法
Math.max.apply(null, [14, 3, 77])
// ES6 的写法
Math.max(...[14, 3, 77])
// 等同于
Math.max(14, 3, 77);
上面代码中, 由于 JavaScript 不提供求数组最大元素的函数, 所以只能套用Math.max函数, 将数组转为一个参数序列, 然后求最大值. 有了扩展运算符以后, 就可以直接用Math.max了.
另一个栗子是通过push函数, 将一个数组添加到另一个数组的尾部.
// ES5的 写法
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
Array.prototype.push.apply(arr1, arr2);
// ES6 的写法
let arr1 = [0, 1, 2];
let arr2 = [3, 4, 5];
arr1.push(...arr2);
上面代码的 ES5 写法中, push方法的参数不能是数组, 所以只好通过apply方法变通使用push方法. 有了扩展运算符, 就可以直接将数组传入push方法.
③ 扩展运算符的应用
a) 复制数组
数组是复合的数据类型, 直接复制的话, 只是复制了指向底层数据结构的指针, 而不是克隆一个全新的数组 [浅拷贝].
const a1 = [1, 2];
const a2 = a1;
a2[0] = 2;
a1 // [2, 2]
上面代码中, a2并不是a1的克隆, 而是指向同一份数据的另一个指针. 修改a2, 会直接导致a1的变化.
ES5 只能用变通方法来复制数组.
const a1 = [1, 2];
const a2 = a1.concat();
a2[0] = 2;
a1 // [1, 2]
上面代码中, a1会返回原数组的克隆, 再修改a2就不会对a1产生影响.
扩展运算符提供了复制数组的简便写法. -->这样就不会造成影响
const a1 = [1, 2];
// 写法一
const a2 = [...a1];
// 写法二
const [...a2] = a1;
上面的两种写法, a2都是a1的克隆.
b) 合并数组
扩展运算符提供了数组合并的新写法.
const arr1 = ['a', 'b'];
const arr2 = ['c'];
const arr3 = ['d', 'e'];
// ES5 的合并数组
arr1.concat(arr2, arr3);
// [ 'a', 'b', 'c', 'd', 'e' ]
// ES6 的合并数组
[...arr1, ...arr2, ...arr3]
// [ 'a', 'b', 'c', 'd', 'e' ]
不过, 这两种方法都是浅拷贝 ( 指的是内部数据如 { foo: 1 } 是存地址 ) , 使用的时候需要注意.
const a1 = [{ foo: 1 }];
const a2 = [{ bar: 2 }];
const a3 = a1.concat(a2);
const a4 = [...a1, ...a2];
a3[0] === a1[0] // true
a4[0] === a1[0] // true
上面代码中, [ a3 ] 和 [ a4 ] 是用两种不同方法合并而成的新数组, 但是它们的成员都是对原数组成员的引用, 这就是浅拷贝. 如果修改了引用指向的值, 会同步反映到新数组.
c) 与解构赋值结合
扩展运算符可以与解构赋值结合起来, 用于生成数组.
const [first, ...rest] = [1, 2, 3, 4, 5];
//first == 1
//rest == [2, 3, 4, 5]
const [first, ...rest] = [];
//first == undefined
//rest == []
const [first, ...rest] = ["foo"];
//first == "foo"
//rest == []
如果将扩展运算符用于数组赋值, 只能放在参数的最后一位, 否则会报错.
const [...butLast, last] = [1, 2, 3, 4, 5];
// 报错
const [first, ...middle, last] = [1, 2, 3, 4, 5];
// 报错
d) 字符串
扩展运算符还可以将字符串转为真正的数组.
[...'hello']
// [ "h", "e", "l", "l", "o" ]
e) 实现了 Iterator 接口的对象
任何定义了遍历器(Iterator)接口的对象, 都可以用扩展运算符转为真正的数组.
let nodeList = document.querySelectorAll('div');
let array = [...nodeList];
f) Map 和 Set 结构,Generator 函数
扩展运算符内部调用的是数据结构的 Iterator 接口, 因此只要具有 Iterator 接口的对象, 都可以使用扩展运算符, 比如 Map 结构.
let map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
let arr = [...map.keys()]; // [1, 2, 3]
Ⅲ - Array.from()
① 简单举例
Array.from方法用于将两类对象转为真正的数组: 类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map).
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
// ES5的写法
var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']
// ES6的写法
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']
② 实际应用场景举栗
实际应用中, 常见的类似数组的对象是 DOM 操作返回的 NodeList 集合, 以及函数内部的arguments对象. Array.from都可以将它们转为真正的数组.
// NodeList对象
let ps = document.querySelectorAll('p');
Array.from(ps).filter(p => {
return p.textContent.length > 100;
});
// arguments对象
function foo() {
var args = Array.from(arguments);
// ...
}
上面代码中, querySelectorAll方法返回的是一个类似数组的对象, 可以将这个对象转为真正的数组, 再使用filter方法
只要是部署了 Iterator 接口的数据结构, Array.from都能将其转为数组.
Array.from('hello')
// ['h', 'e', 'l', 'l', 'o']
let namesSet = new Set(['a', 'b'])
Array.from(namesSet) // ['a', 'b']
上面代码中, 字符串和 Set 结构都具有 Iterator 接口, 因此可以被Array.from转为真正的数组.
如果参数是一个真正的数组, Array.from会返回一个一模一样的新数组.
Array.from([1, 2, 3])
// [1, 2, 3]
③ 第二个参数的作用
Array.from还可以接受第二个参数, 作用类似于数组的map方法, 用来对每个元素进行处理, 将处理后的值放入返回的数组.
Array.from(arrayLike, x => x * x);
// 等同于
Array.from(arrayLike).map(x => x * x);
Array.from([1, 2, 3], (x) => x * x)
// [1, 4, 9]
下面的栗子将数组中布尔值为false的成员转为0.
Array.from([1, , 2, , 3], (n) => n || 0)
// [1, 0, 2, 0, 3]
另一个栗子是返回各种数据的类型.
function typesOf () {
return Array.from(arguments, value => typeof value)
}
typesOf(null, [], NaN)
// ['object', 'object', 'number']
Ⅳ- Array.of ( )
① 基本使用
[ Array.of ]方法用于将一组值, 转换为数组.
Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]
Array.of(3).length // 1
这个方法的主要目的, 是弥补数组构造函数Array()的不足. 因为参数个数的不同, 会导致Array()的行为有差异.
Array() // []
Array(3) // [, , ,]
Array(3, 11, 8) // [3, 11, 8]
上面代码中, Array方法没有参数、一个参数、三个参数时, 返回结果都不一样. 只有当参数个数不少于 2 个时, Array()才会返回由参数组成的新数组. 参数个数只有一个时, 实际上是指定数组的长度.
Ⅴ- 数组的实例方法
① 数组实例的 copyWithin()
数组实例的 [ copyWithin() ] 方法, 在当前数组内部, 将指定位置的成员复制到其他位置(会覆盖原有成员), 然后返回当前数组. 也就是说, 使用这个方法, 会修改当前数组.
Array.prototype.copyWithin(target, start = 0, end = this.length)
它接受三个参数.
- target(必需): 从该位置开始替换数据. 如果为负值, 表示倒数.
- start(可选): 从该位置开始读取数据, 默认为 0. 如果为负值, 表示从末尾开始计算.
- end(可选): 到该位置前停止读取数据, 默认等于数组长度. 如果为负值, 表示从末尾开始计算.
这三个参数都应该是数值, 如果不是, 会自动转为数值.
[1, 2, 3, 4, 5].copyWithin(0, 3)
// [4, 5, 3, 4, 5]
上面代码表示将从 3 号位直到数组结束的成员(4 和 5), 复制到从 0 号位开始的位置, 结果覆盖了原来的 1 和 2.
下面是更多栗子.
// 将3号位复制到0号位
[1, 2, 3, 4, 5].copyWithin(0, 3, 4) //从三号位开始读取,到四号位结束,得到[4],将其替换到0号位
// [4, 2, 3, 4, 5]
// -2相当于3号位, -1相当于4号位
[1, 2, 3, 4, 5].copyWithin(0, -2, -1) //从倒数2号位开始读取,到倒数一号位结束,得到[4],将其替换到0号位
// [4, 2, 3, 4, 5]
// 将3号位复制到0号位
[].copyWithin.call({length: 5, 3: 1}, 0, 3)
// {0: 1, 3: 1, length: 5}
// 将2号位到数组结束, 复制到0号位
let i32a = new Int32Array([1, 2, 3, 4, 5]);
i32a.copyWithin(0, 2);
// Int32Array [3, 4, 5, 4, 5]
// 对于没有部署 TypedArray 的 copyWithin 方法的平台
// 需要采用下面的写法
[].copyWithin.call(new Int32Array([1, 2, 3, 4, 5]), 0, 3, 4);
// Int32Array [4, 2, 3, 4, 5]
② 数组实例的 find() 和 findIndex()
数组实例的 [ find ] 方法, 用于找出第一个符合条件的数组成员. 它的参数是一个回调函数, 所有数组成员依次执行该回调函数, 直到找出第一个返回值为true的成员, 然后返回该成员. 如果没有符合条件的成员, 则返回 undefined .
[1, 4, -5, 10].find((n) => n < 0)
// -5
上面代码找出数组中第一个小于 0 的成员.
[1, 5, 10, 15].find(function(value, index, arr) {
return value > 9;
}) // 10
上面代码中, [ find ] 方法的回调函数可以接受三个参数, 依次为当前的值、当前的位置和原数组.
数组实例的 [ findIndex] 方法的用法与 [ find ] 方法非常类似, 返回第一个符合条件的数组成员的位置, 如果所有成员都不符合条件, 则返回-1.
[1, 5, 10, 15].findIndex(function(value, index, arr) {
return value > 9;
}) // 2
这两个方法都可以接受第二个参数, 用来绑定回调函数的this对象.
function f(v){
return v > this.age;
}
let person = {name: 'John', age: 20};
[10, 12, 26, 15].find(f, person); // 26
上面的代码中, [ find ] 函数接收了第二个参数person对象, 回调函数中的this对象指向person对象.
另外, 这两个方法都可以发现NaN, 弥补了数组的 [ indexOf ] 方法的不足.
[NaN].indexOf(NaN) // -1
[NaN].findIndex(y => Object.is(NaN, y)) // 0
上面代码中, [ indexOf ] 方法无法识别数组的NaN成员, 但是 [ findIndex] 方法可以借助 [ Object.is ] 方法做到.
③ 数组实例的 entries(),keys() 和 values()
ES6 提供三个新的方法——[ entries() ], [ keys() ] 和 [ values() ]——用于遍历数组. 它们都返回一个遍历器对象.可以用for...of循环进行遍历, 唯一的区别是[ keys() ]是对键名的遍历、[ values() ]是对键值的遍历, [ entries() ]是对键值对的遍历.
for (let index of ['a', 'b'].keys()) { console.log(index);}
// 0
// 1
for (let elem of ['a', 'b'].values()) { console.log(elem);}
// 'a'
// 'b'
for (let [index, elem] of ['a', 'b'].entries()) { console.log(index, elem);}
// 0 "a"
// 1 "b"
如果不使用for...of循环, 可以手动调用遍历器对象的next方法, 进行遍历.
let letter = ['a', 'b', 'c'];
let entries = letter.entries();
console.log(entries.next().value); // [0, 'a']
console.log(entries.next().value); // [1, 'b']
console.log(entries.next().value); // [2, 'c']
④ 数组实例的 includes()
[ Array.prototype.includes ] 方法返回一个布尔值, 表示某个数组是否包含给定的值, 与字符串的 [ includes ] 方法类似. ES2016 引入了该方法.
[1, 2, 3].includes(2) // true
[1, 2, 3].includes(4) // false
[1, 2, NaN].includes(NaN) // true
该方法的第二个参数表示搜索的起始位置, 默认为0. 如果第二个参数为负数, 则表示倒数的位置, 如果这时它大于数组长度(比如第二个参数为-4, 但数组长度为3), 则会重置为从0开始.
[1, 2, 3].includes(3, 3); // false
[1, 2, 3].includes(3, -1); // true
没有该方法之前, 我们通常使用数组的 [ indexOf ] 方法, 检查是否包含某个值.
if (arr.indexOf(el) !== -1) {
// ...
}
[ indexOf ] 方法有两个缺点, 一是不够语义化, 它的含义是找到参数值的第一个出现位置, 所以要去比较是否不等于-1, 表达起来不够直观. 二是, 它内部使用严格相等运算符(===)进行判断, 这会导致对NaN的误判.
[NaN].indexOf(NaN)
// -1
[ includes ] 使用的是不一样的判断算法, 就没有这个问题.
[NaN].includes(NaN)
// true
下面代码用来检查当前环境是否支持该方法, 如果不支持, 部署一个简易的替代版本.
const contains = (() =>
Array.prototype.includes
? (arr, value) => arr.includes(value)
: (arr, value) => arr.some(el => el === value)
)();
contains(['foo', 'bar'], 'baz'); // => false
另外,Map 和 Set 数据结构有一个has方法, 需要注意与 [ includes ] 区分.
- Map 结构的
has方法, 是用来查找键名的, 比如 [ Map.prototype.has(key) ] 、 [ WeakMap.prototype.has(key) ] 、 [ Reflect.has(target, propertyKey) ] . - Set 结构的
has方法, 是用来查找值的, 比如 [ Set.prototype.has(value) ] 、 [ WeakSet.prototype.has(value) ] .
⑤ 数组实例的 flat(),flatMap()
数组的成员有时还是数组, Array.prototype.flat()用于将嵌套的数组“拉平”, 变成一维的数组. 该方法返回一个新数组, 对原数据没有影响.
[1, 2, [3, 4]].flat()
// [1, 2, 3, 4]
上面代码中, 原数组的成员里面有一个数组, [ flat() ] 方法将子数组的成员取出来, 添加在原来的位置.
[ flat() ] 默认只会“拉平”一层, 如果想要“拉平”多层的嵌套数组, 可以将 [ flat() ] 方法的参数写成一个整数, 表示想要拉平的层数, 默认为1.
[1, 2, [3, [4, 5]]].flat()
// [1, 2, 3, [4, 5]]
[1, 2, [3, [4, 5]]].flat(2)
// [1, 2, 3, 4, 5]
上面代码中, [ flat() ] 的参数为2,表示要“拉平”两层的嵌套数组.
如果不管有多少层嵌套, 都要转成一维数组, 可以用Infinity关键字作为参数.
[1, [2, [3]]].flat(Infinity)
// [1, 2, 3]
如果原数组有空位, [ flat() ] 方法会跳过空位. --> 这个可以用作去除数组中空位,特殊场景好用
[1, 2, , 4, 5].flat()
// [1, 2, 4, 5]
[ flatMap() ] 方法对原数组的每个成员执行一个函数(相当于执行Array.prototype.map()), 然后对返回值组成的数组执行 [ flat() ] 方法. 该方法返回一个新数组, 不改变原数组.
// 相当于 [[2, 4], [3, 6], [4, 8]].flat()
[2, 3, 4].flatMap((x) => [x, x * 2])
// [2, 4, 3, 6, 4, 8]
[ flatMap() ] 只能展开一层数组.
// 相当于 [[[2]], [[4]], [[6]], [[8]]].flat()
[1, 2, 3, 4].flatMap(x => [[x * 2]])
// [[2], [4], [6], [8]]
上面代码中, 遍历函数返回的是一个双层的数组, 但是默认只能展开一层, 因此 [ flatMap() ] 返回的还是一个嵌套数组.
[ flatMap() ] 方法的参数是一个遍历函数, 该函数可以接受三个参数, 分别是当前数组成员、当前数组成员的位置(从零开始)、原数组.
arr.flatMap(function callback(currentValue[, index[, array]]) {
// ...
}[, thisArg])
[ flatMap() ] 方法还可以有第二个参数, 用来绑定遍历函数里面的this.
Ⅵ - 数组的空位
数组的空位指, 数组的某一个位置没有任何值. 比如, Array构造函数返回的数组都是空位.
Array(3) // [, , ,]
上面代码中, Array(3)返回一个具有 3 个空位的数组.
注意, 空位不是undefined, 一个位置的值等于undefined, 依然是有值的. 空位是没有任何值, [ in ]运算符可以说明这一点.
0 in [undefined, undefined, undefined] // true
0 in [, , ,] // false
上面代码说明, 第一个数组的 0 号位置是有值的, 第二个数组的 0 号位置没有值.
ES5 对空位的处理, 已经很不一致了, 大多数情况下会忽略空位.
- forEach(), filter(), reduce(), every() 和 some() 都会跳过空位.
- map() 会跳过空位, 但会保留这个值
- join() 和 toString() 会将空位视为 undefined , 而 undefined 和 null会被处理成空字符串.
// forEach方法
[,'a'].forEach((x,i) => console.log(i)); // 1
// filter方法
['a',,'b'].filter(x => true) // ['a','b']
// every方法
[,'a'].every(x => x==='a') // true
// reduce方法
[1,,2].reduce((x,y) => x+y) // 3
// some方法
[,'a'].some(x => x !== 'a') // false
// map方法
[,'a'].map(x => 1) // [,1]
// join方法
[,'a',undefined,null].join('#') // "#a##"
// toString方法
[,'a',undefined,null].toString() // ",a,,"
ES6 则是明确将空位转为 undefined.
Array.from方法会将数组的空位, 转为 undefined, 也就是说, 这个方法不会忽略空位.
Array.from(['a',,'b'])
// [ "a", undefined, "b" ]
扩展运算符(...)也会将空位转为 undefined.
[...['a',,'b']]
// [ "a", undefined, "b" ]
copyWithin()会连空位一起拷贝.
[,'a','b',,].copyWithin(2,0) // [,"a",,"a"]
fill()会将空位视为正常的数组位置.
new Array(3).fill('a') // ["a","a","a"]
for...of循环也会遍历空位.
let arr = [, ,];
for (let i of arr) {
console.log(1);
}
// 1
// 1
上面代码中, 数组arr有两个空位, for...of并没有忽略它们. 如果改成map方法遍历, 空位是会跳过的.
[ entries() ]、[ keys() ]、[ values() ]、find()和findIndex()会将空位处理成 undefined.
// entries()
[...[,'a'].entries()] // [[0,undefined], [1,"a"]]
// keys()
[...[,'a'].keys()] // [0,1]
// values()
[...[,'a'].values()] // [undefined,"a"]
// find()
[,'a'].find(x => true) // undefined
// findIndex()
[,'a'].findIndex(x => true) // 0
由于空位的处理规则非常不统一, 所以建议避免出现空位.
Ⅶ - Array.prototype.sort() 的排序稳定性
常见的排序算法之中, 插入排序、合并排序、冒泡排序等都是稳定的, 堆排序、快速排序等是不稳定的. 不稳定排序的主要缺点是, 多重排序时可能会产生问题. 假设有一个姓和名的列表, 要求按照“姓氏为主要关键字, 名字为次要关键字”进行排序. 开发者可能会先按名字排序, 再按姓氏进行排序. 如果排序算法是稳定的, 这样就可以达到“先姓氏, 后名字”的排序效果. 如果是不稳定的, 就不行.
早先的 ECMAScript 没有规定, Array.prototype.sort()的默认排序算法是否稳定, 留给浏览器自己决定, 这导致某些实现是不稳定的. ES2019 明确规定, Array.prototype.sort()的默认排序算法必须稳定. 这个规定已经做到了, 现在 JavaScript 各个主要实现的默认排序算法都是稳定的.
6.对象的拓展
Ⅰ- 概括总结
- 简洁表示法: 直接写入变量和函数作为对象的属性和方法(
{ prop, method() {} }) - 属性名表达式: 字面量定义对象时使用
[]定义键([prop], 不能与上同时使用) - 方法的name属性: 返回方法函数名 -->此处与函数很像,因为本质上函数就是一种特殊对象
- 取值函数(getter)和存值函数(setter):
get/set 函数名(属性的描述对象在get和set上) - bind返回的函数:
bound 函数名 - Function构造函数返回的函数实例:
anonymous
- 属性的可枚举性和遍历: 描述对象的
enumerable - super关键字: 指向当前对象的原型对象(只能用在对象的简写方法中
method() {}) - Object.is(): 对比两值是否相等
- Object.assign(): 合并对象(浅拷贝), 返回原对象 (
常用) - Object.getPrototypeOf(): 返回对象的原型对象
- Object.setPrototypeOf(): 设置对象的原型对象
- proto: 返回或设置对象的原型对象
- 描述:
自身、可继承、可枚举、非枚举、Symbol - 遍历
- [ for-in ] : 遍历对象
自身可继承可枚举属性 - [Object.keys()] : 返回对象
自身可枚举属性键 [ key ] 组成的数组 - [Object.getOwnPropertyNames()] : 返回对象
自身非Symbol属性键 [ key ] 组成的数组 Object.getOwnPropertySymbols(): 返回对象自身Symbol属性键 [ key ] 组成的数组Reflect.ownKeys(): 返回对象自身全部属性键 [ key ] 组成的数组
Ⅱ - 属性的简洁表示
① 属性的简写
function f(x, y) { return {x, y};}
// 等同于
function f(x, y) { return {x: x, y: y};}
f(1, 2) // Object {x: 1, y: 2}
const foo = 'bar';
const baz = {foo};
//baz == {foo: "bar"}
// 等同于
const baz = {foo: foo};
② 方法的简写
const o = {
method() { return "Hello!";}
};
// 等同于
const o = {
method: function() {return "Hello!"; }
};
Ⅲ - 属性的可枚举性和遍历
① 可枚举性
描述对象的[ enumerable ] 属性, 称为“可枚举性”, 如果该属性为 [ false ], 就表示某些操作会忽略当前属性.
目前, 有四个操作会忽略enumerable为 [ false ] 的属性.
- for…in循环: 只遍历对象自身的和继承的可枚举的属性.
- Object.keys(): 返回对象自身的所有可枚举的属性的键名.
- JSON.stringify(): 只串行化对象自身的可枚举的属性.
- Object.assign(): 忽略
enumerable为false的属性, 只拷贝对象自身的可枚举的属性.
② 属性的遍历方法
ES6 一共有 5 种方法可以遍历对象的属性.
(1)for…in
for...in循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性).
(2)Object.keys(obj)
Object.keys返回一个数组, 包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名.
(3)Object.getOwnPropertyNames(obj)
Object.getOwnPropertyNames返回一个数组, 包含对象自身的所有属性(不含 Symbol 属性, 但是包括不可枚举属性)的键名.
(4)Object.getOwnPropertySymbols(obj)
Object.getOwnPropertySymbols返回一个数组, 包含对象自身的所有 Symbol 属性的键名.
(5)Reflect.ownKeys(obj)
Reflect.ownKeys返回一个数组, 包含对象自身的(不含继承的)所有键名, 不管键名是 Symbol 或字符串, 也不管是否可枚举.
以上的 5 种方法遍历对象的键名, 都遵守同样的属性遍历的次序规则.
- 首先遍历所有数值键, 按照数值升序排列.
- 其次遍历所有字符串键, 按照加入时间升序排列.
- 最后遍历所有 Symbol 键, 按照加入时间升序排列.
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// ['2', '10', 'b', 'a', Symbol()]
上面代码中, Reflect.ownKeys方法返回一个数组, 包含了参数对象的所有属性. 这个数组的属性次序是这样的, 首先是数值属性2和10, 其次是字符串属性b和a, 最后是 Symbol 属性.
Ⅳ- super 关键字
ES6 又新增了另一个类似的关键字 [ super ], 指向当前对象的原型对象.
const proto = { foo: 'hello'};
const obj = {
foo: 'world',
find() {
return super.foo;
}
};
Object.setPrototypeOf(obj, proto);
obj.find() // "hello"
注意, super关键字表示原型对象时, 只能用在对象的方法之中, 用在其他地方都会报错.
// 报错
const obj = {
foo: super.foo
}
// 报错
const obj = {
foo: () => super.foo
}
// 报错
const obj = {
foo: function () {
return super.foo
}
}
上面三种super的用法都会报错, 因为对于 JavaScript 引擎来说, 这里的super都没有用在对象的方法之中. 第一种写法是super用在属性里面, 第二种和第三种写法是super用在一个函数里面, 然后赋值给foo属性. 目前, 只有对象方法的简写法可以让 JavaScript 引擎确认, 定义的是对象的方法.
Ⅴ - 对象的拓展运算符 ( ... )
① 扩展运算符的解构赋值
扩展运算符的解构赋值, 不能复制继承自原型对象的属性.
let o1 = { a: 1 };
let o2 = { b: 2 };
o2.__proto__ = o1;
let { ...o3 } = o2;
o3 // { b: 2 }
o3.a // undefined
上面代码中, 对象o3复制了o2, 但是只复制了o2自身的属性, 没有复制它的原型对象o1的属性.
const o = Object.create({ x: 1, y: 2 });
o.z = 3;
let { x, ...newObj } = o;
let { y, z } = newObj;
console.log(x,newObj,y,z);//1 { z: 3 } undefined 3
x // 1
y // undefined
z // 3
上面代码中, 变量x是单纯的解构赋值, 所以可以读取对象o继承的属性;变量y和z是扩展运算符的解构赋值, 只能读取对象o自身的属性, 所以变量z可以赋值成功, 变量y取不到值.
7.Symbol
Ⅰ- 概述与总结
ES6 引入了一种新的原始数据类型Symbol, 表示独一无二的值. 它是 JavaScript 语言的第七种数据类型, 前六种是: undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object).
- 定义: 独一无二的值,类似于一种标识唯一性的ID
- 声明:
const set = Symbol(str) - 入参: 字符串(可选)
- 方法:
- Symbol(): 创建以参数作为描述的
Symbol值(不登记在全局环境) - Symbol.for(): 创建以参数作为描述的
Symbol值, 如存在此参数则返回原有的Symbol值(先搜索后创建, 登记在全局环境) - Symbol.keyFor(): 返回已登记的
Symbol值的描述(只能返回Symbol.for()的key) - Object.getOwnPropertySymbols(): 返回对象中所有用作属性名的
Symbol值的数组
- 内置
- Symbol.hasInstance: 指向一个内部方法, 当其他对象使用
instanceof运算符判断是否为此对象的实例时会调用此方法 - Symbol.isConcatSpreadable: 指向一个布尔, 定义对象用于
Array.prototype.concat()时是否可展开 - Symbol.species: 指向一个构造函数, 当实例对象使用自身构造函数时会调用指定的构造函数
- Symbol.match: 指向一个函数, 当实例对象被
String.prototype.match()调用时会重新定义match()的行为 - Symbol.replace: 指向一个函数, 当实例对象被
String.prototype.replace()调用时会重新定义replace()的行为 - Symbol.search: 指向一个函数, 当实例对象被
String.prototype.search()调用时会重新定义search()的行为 - Symbol.split: 指向一个函数, 当实例对象被
String.prototype.split()调用时会重新定义split()的行为 - Symbol.iterator: 指向一个默认遍历器方法, 当实例对象执行 [ for-of ] 时会调用指定的默认遍历器
- Symbol.toPrimitive: 指向一个函数, 当实例对象被转为原始类型的值时会返回此对象对应的原始类型值
- Symbol.toStringTag: 指向一个函数, 当实例对象被
Object.prototype.toString()调用时其返回值会出现在toString()返回的字符串之中表示对象的类型 - Symbol.unscopables: 指向一个对象, 指定使用
with时哪些属性会被with环境排除
- Undefined
- Null
- String
- Number
- Boolean
- Object(包含
Array、Function、Date、RegExp、Error) - Symbol
- bigint, -->BigInt 是一种数字类型的数据
Symbol()生成一个原始类型的值不是对象, 因此Symbol()前不能使用new命令Symbol()参数表示对当前Symbol值的描述, 相同参数的Symbol()返回值不相等Symbol值不能与其他类型的值进行运算Symbol值可通过String()或toString()显式转为字符串Symbol值作为对象属性名时, 此属性是公开属性, 但不是私有属性Symbol值作为对象属性名时, 只能用方括号运算符([])读取, 不能用点运算符(.)读取Symbol值作为对象属性名时, 不会被常规方法遍历得到, 可利用此特性为对象定义非私有但又只用于内部的方法
Ⅱ - 举个简单的例子
注意, Symbol函数前不能使用new命令, 否则会报错. 这是因为生成的 Symbol 是一个原始类型的值, 不是对象. 也就是说, 由于 Symbol 值不是对象, 所以不能添加属性. 基本上, 它是一种类似于字符串的数据类型.
Symbol函数可以接受一个字符串作为参数, 表示对 Symbol 实例的描述, 主要是为了在控制台显示, 或者转为字符串时, 比较容易区分.
let s1 = Symbol('123');
let s2 = Symbol('456');
s1 // Symbol(123) 注意:此处是 Symbol 值
s2 // Symbol(456)
s1.toString() // "Symbol(123)" 注意 此处是字符串
s2.toString() // "Symbol(456)"
如果 Symbol 的参数是一个对象, 就会调用该对象的toString方法, 将其转为字符串, 然后才生成一个 Symbol 值.
const obj = {
toString() {
return 'abc';
}
};
const sym = Symbol(obj);
sym // Symbol(abc) --> [ Symbol 值 ]
Symbol 值不能与其他类型的值进行运算, 会报错.
但是,Symbol 值可以显式转为字符串.
let sym = Symbol('My symbol');
String(sym) // 'Symbol(My symbol)'
sym.toString() // 'Symbol(My symbol)'
另外,Symbol 值也可以转为布尔值, 但是不能转为数值.
let sym = Symbol();
Boolean(sym) // true
!sym // false
if (sym) {}
Number(sym) // TypeError
sym + 2 // TypeError
Ⅲ - Symbol.prototype.description
ES2019 提供了一个实例属性description, 直接返回 Symbol 的描述.
const sym = Symbol('123');
sym.description // "123"
Ⅳ - 作为属性名的 Symbol
① 举个栗子:
由于每一个 Symbol 值都是不相等的, 这意味着 Symbol 值可以作为标识符, 用于对象的属性名, 就能保证不会出现同名的属性. 这对于一个对象由多个模块构成的情况非常有用, 能防止某一个键被不小心改写或覆盖.
let mySymbol = Symbol();
// 第一种写法
let a = {};
a[mySymbol] = 'Hello!';
// 第二种写法
let a = {
[mySymbol]: 'Hello!'
};
// 第三种写法
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });
// 以上写法都得到同样结果
a[mySymbol] // "Hello!"
上面代码通过方括号结构和 [ Object.defineProperty ] , 将对象的属性名指定为一个 Symbol 值.
Ⅴ - 属性名的遍历
Symbol 作为属性名, 遍历对象的时候, 该属性不会出现在for...in、for...of循环中, 也不会被 [Object.keys()] 、 [Object.getOwnPropertyNames()] 、JSON.stringify()返回.
但是, 它也不是私有属性, 有一个Object.getOwnPropertySymbols()方法, 可以获取指定对象的所有 Symbol 属性名. 该方法返回一个数组, 成员是当前对象的所有用作属性名的 Symbol 值.
const obj = {};
let a = Symbol('a');
let b = Symbol('b');
obj[a] = 'Hello';
obj[b] = 'World';
const objectSymbols = Object.getOwnPropertySymbols(obj);
objectSymbols// [Symbol(a), Symbol(b)]
上面代码是Object.getOwnPropertySymbols()方法的示例, 可以获取所有 Symbol 属性名.
Ⅵ - Symbol.for(),Symbol.keyFor()
① Symbol.for()
有时, 我们希望重新使用同一个 Symbol 值, Symbol.for()方法可以做到这一点. 它接受一个字符串作为参数, 然后搜索有没有以该参数作为名称的 Symbol 值. 如果有, 就返回这个 Symbol 值, 否则就新建一个以该字符串为名称的 Symbol 值, 并将其注册到全局.
let s1 = Symbol.for('123');
let s2 = Symbol.for('123');
let h1 = Symbol('456');
let h2 = Symbol('456');
console.log(s1 === s2 ,h1 === h2)//true false
let s1 = Symbol.for('123');
let s2 = Symbol.for('123');
let h1 = Symbol.for('456');
let h2 = Symbol('456');
console.log(s1 === s2 ,h1 === h2)//true false
必须两个变量同时用symbol.for()
Symbol.for()与Symbol()这两种写法, 都会生成新的 Symbol. 它们的区别是 :
- 前者会被登记在全局环境中供搜索, 后者不会.
Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值, 而是会先检查给定的key是否已经存在, 如果不存在才会新建一个值.- 如果你调用
Symbol.for("cat")30 次, 每次都会返回同一个 Symbol 值; - 但是调用
Symbol("cat")30 次, 会返回 30 个不同的 Symbol 值.
② Symbol.keyFor()
Symbol.keyFor()方法返回一个已登记的 Symbol 类型值的key.
let s1 = Symbol.for("123");
console.log(Symbol.keyFor(s1)) // 123
let s2 = Symbol("456");
console.log(Symbol.keyFor(s2)) // undefined
注意, Symbol.for()为 Symbol 值登记的名字, 是全局环境的, 不管有没有在全局环境运行.
function foo() {
return Symbol.for('123'); //在函数内部:局部作用域中运行
}
const x = foo();
const y = Symbol.for('123');
console.log(x === y); // true










