0
点赞
收藏
分享

微信扫一扫

【es6】详细笔记

ES6 详细笔记

ES6中的特性

  • let 和 const
  • 变量的赋值解构
  • 字符串的扩展
  • 正则的扩展
  • 数值的扩展
  • 函数的扩展
  • 对象的扩展
  • 运算符的扩展
  • Symbol
  • Set 和 Map
  • Proxy
  • Reflect
  • Promise
  • Iterator
  • for … of
  • Generator
  • async
  • Class
  • Module

这里按照ES6标准入门的目录顺序写的笔记,只写了一部分,后序的部分分成了好几篇笔记写的。

文章目录

1.let/const

Ⅰ-概括与总结

声明

  • const命令: 声明常量
  • let命令: 声明变量

作用范围

  • var命令在全局代码中执行
  • const命令let命令只能在代码块中执行

赋值使用

  • const命令声明常量后必须立马赋值
  • let命令声明变量后可立马赋值或使用时赋值

​ 注:声明方法: varconstletfunctionclassimport

重点难点

  • 不允许重复声明
  • 未定义就使用会报错: 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

上面最后一个表达式之所以会报错, 是因为xy做默认值时, 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的参数指定默认值, 而不是为变量xy指定默认值, 所以会得到与前一种写法不同的结果.

undefined 就会触发函数参数的默认值.

[1, undefined, 3].map((x = 'yes') => x);
// [ 1, 'yes', 3 ]

Ⅶ-具体应用场景举例

① 交换变量的值

let x = 1;
let y = 2;
[x, y] = [y, x];

上面代码交换变量xy的值, 这样的写法不仅简洁, 而且易读, 语义非常清晰.

② 从函数返回多个值

函数只能返回一个值, 如果要返回多个值, 只能将它们放在数组或对象里返回. 有了解构赋值, 取出这些值就非常方便.

// 返回一个数组
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接口 ] 的数据结构为真正数组, 返回新数组
    1. 类数组对象: 包含length的对象Arguments对象NodeList对象
    2. 可遍历对象: StringSet结构Map结构Generator函数
  • Array.of(): 转换一组值为真正数组, 返回新数组
  • 实例方法
    1. copyWithin(): 把指定位置的成员复制到其他位置, 返回原数组
    2. find(): 返回第一个符合条件的成员
    3. findIndex(): 返回第一个符合条件的成员索引值
    4. fill(): 根据指定值填充整个数组, 返回原数组
    5. keys(): 返回以索引值为遍历器的对象
    6. values(): 返回以属性值为遍历器的对象
    7. entries(): 返回以索引值和属性值为遍历器的对象
    8. 其他:毕竟只是概述,不过多列举,详细看下方
  • 数组空位: 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 , 而 undefinednull会被处理成空字符串.
// 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.对象的拓展

Ⅰ- 概括总结

  1. 简洁表示法: 直接写入变量和函数作为对象的属性和方法({ prop, method() {} })
  2. 属性名表达式: 字面量定义对象时使用[]定义键([prop], 不能与上同时使用)
  3. 方法的name属性: 返回方法函数名 -->此处与函数很像,因为本质上函数就是一种特殊对象
  • 取值函数(getter)和存值函数(setter): get/set 函数名(属性的描述对象在getset上)
  • bind返回的函数: bound 函数名
  • Function构造函数返回的函数实例: anonymous
  1. 属性的可枚举性和遍历: 描述对象的enumerable
  2. super关键字: 指向当前对象的原型对象(只能用在对象的简写方法中method() {})
  3. Object.is(): 对比两值是否相等
  4. Object.assign(): 合并对象(浅拷贝), 返回原对象 (常用)
  5. Object.getPrototypeOf(): 返回对象的原型对象
  6. Object.setPrototypeOf(): 设置对象的原型对象
  7. proto: 返回或设置对象的原型对象
  1. 描述: 自身可继承可枚举非枚举Symbol
  2. 遍历
  • [ 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 ] 的属性.

  1. for…in循环: 只遍历对象自身的和继承的可枚举的属性.
  2. Object.keys(): 返回对象自身的所有可枚举的属性的键名.
  3. JSON.stringify(): 只串行化对象自身的可枚举的属性.
  4. Object.assign(): 忽略enumerablefalse的属性, 只拷贝对象自身的可枚举的属性.

② 属性的遍历方法

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 种方法遍历对象的键名, 都遵守同样的属性遍历的次序规则.

  1. 首先遍历所有数值键, 按照数值升序排列.
  2. 其次遍历所有字符串键, 按照加入时间升序排列.
  3. 最后遍历所有 Symbol 键, 按照加入时间升序排列.
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// ['2', '10', 'b', 'a', Symbol()]

上面代码中, Reflect.ownKeys方法返回一个数组, 包含了参数对象的所有属性. 这个数组的属性次序是这样的, 首先是数值属性210, 其次是字符串属性ba, 最后是 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继承的属性;变量yz是扩展运算符的解构赋值, 只能读取对象o自身的属性, 所以变量z可以赋值成功, 变量y取不到值.

7.Symbol

Ⅰ- 概述与总结

ES6 引入了一种新的原始数据类型Symbol, 表示独一无二的值. 它是 JavaScript 语言的第七种数据类型, 前六种是: undefinednull、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object).

  1. 定义: 独一无二的值,类似于一种标识唯一性的ID
  2. 声明: const set = Symbol(str)
  3. 入参: 字符串(可选)
  4. 方法:
  • Symbol(): 创建以参数作为描述的Symbol值(不登记在全局环境)
  • Symbol.for(): 创建以参数作为描述的Symbol值, 如存在此参数则返回原有的Symbol值(先搜索后创建, 登记在全局环境)
  • Symbol.keyFor(): 返回已登记的Symbol值的描述(只能返回Symbol.for()key)
  • Object.getOwnPropertySymbols(): 返回对象中所有用作属性名的Symbol值的数组
  1. 内置
  • 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(包含ArrayFunctionDateRegExpError)
  • 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...infor...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
举报

相关推荐

0 条评论