ES6的学习总结(上)

let和const

let声明的变量只在它所在的代码块有效。

for (let i = 0; i < 10; i++) {
  // ...
}

console.log(i);
// ReferenceError: i is not defined

对于var,变量ivar命令声明的,在全局范围内都有效,所以全局只有一个变量i。每一次循环,变量i的值都会发生改变,而循环内被赋给数组a的函数内部的console.log(i),里面的i指向的就是全局的i。也就是说,所有数组a的成员里面的i,指向的都是同一个i,导致运行时输出的是最后一轮的i的值,也就是 10。

对于let,如果使用let,声明的变量仅在块级作用域内有效,最后输出的是 6。

var a = [];
for (var i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 10

var a = [];
for (let i = 0; i < 10; i++) {
  a[i] = function () {
    console.log(i);
  };
}
a[6](); // 6

另外,for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。

for (let i = 0; i < 3; i++) {
  let i = 'abc';
  console.log(i);
}
// abc
// abc
// abc

上面代码正确运行,输出了 3 次abc。这表明函数内部的变量i与循环变量i不在同一个作用域,有各自单独的作用域(同一个作用域不可使用 let 重复声明同一个变量)。

对于let变量必须先声明后使用,否则会报错。不存在变量提升。

ES6 明确规定,如果区块中存在letconst命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。(“暂时性死区”)

var tmp = 123;

if (true) {
  tmp = 'abc'; // ReferenceError
  let tmp;
}

不允许重复声明,var允许重复定义,但是let和const是不允许的。

// 报错
function func() {
  let a = 10;
  var a = 1;
}

// 报错
function func() {
  let a = 10;
  let a = 1;
}

块级作用域

ES5 只有全局作用域和函数作用域,没有块级作用域,ES6引入了块级作用域的概念。大括号包裹的就是块级作用域。

没有块级作用域引发的后果:

1. 内层变量可能会覆盖外层变量。

var tmp = new Date();

function f() {
  console.log(tmp);
  if (false) {
    var tmp = 'hello world';
  }
}

f(); // undefined

 内层变量tmp被提升到了函数开始位置,这个时候相当于:var tmp; console.log(tmp);

2. 用来计数的循环变量泄露为全局变量

var s = 'hello';

for (var i = 0; i < s.length; i++) {
  console.log(s[i]);
}

console.log(i); // 5

有了块级作用域,上层变量就不会被下层影响。

function f1() {
  let n = 5;
  if (true) {
    let n = 10;
  }
  console.log(n); // 5
}

 有了块级作用域,IIFE写法也就被淘汰了。

// IIFE 写法
(function () {
  var tmp = ...;
  ...
}());

// 块级作用域写法
{
  let tmp = ...;
  ...
}

ES6 的块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域。

// 第一种写法,报错
if (true) let x = 1;

// 第二种写法,不报错
if (true) {
  let x = 1;
}

考虑到环境导致的行为差异太大(不同环境可能会将function当成let或者var去处理),应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。

// 块级作用域内部的函数声明语句,建议不要使用
{
  let a = 'secret';
  function f() {
    return a;
  }
}

// 块级作用域内部,优先使用函数表达式
{
  let a = 'secret';
  let f = function () {
    return a;
  };
}

Const

const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。
const foo = {};

// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123

// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only

顶层对象

顶层对象,在浏览器环境指的是window对象,在 Node 指的是global对象。ES5 之中,顶层对象的属性与全局变量是等价的。

window.a = 1;
a // 1

a = 2;
window.a // 2

 ES6 为了改变这一点,一方面规定,为了保持兼容性,var命令和function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。

JavaScript 语言存在一个顶层对象,它提供全局环境(即全局作用域),所有代码都是在这个环境中运行。但是,顶层对象在各种实现里面是不统一的。

  • 浏览器里面,顶层对象是window,但 Node 和 Web Worker 没有window
  • 浏览器和 Web Worker 里面,self也指向顶层对象,但是 Node 没有self
  • Node 里面,顶层对象是global,但其他环境都不支持

以下是常用的两种方式获取顶层对象。

// 方法一
(typeof window !== 'undefined'
   ? window
   : (typeof process === 'object' &&
      typeof require === 'function' &&
      typeof global === 'object')
     ? global
     : this);

// 方法二
var getGlobal = function () {
  if (typeof self !== 'undefined') { return self; }
  if (typeof window !== 'undefined') { return window; }
  if (typeof global !== 'undefined') { return global; }
  throw new Error('unable to locate global object');
};

数据解构

事实上,只要某种数据结构具有 Iterator 接口,都可以采用数组形式的解构赋值。

let [foo, [[bar], baz]] = [1, [[2], 3]];
foo // 1
bar // 2
baz // 3

let [ , , third] = ["foo", "bar", "baz"];
third // "baz"

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

let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]

let [x, y, ...z] = ['a'];
x // "a"
y // undefined
z // []

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

let [a, [b], d] = [1, [2, 3], 4];
a // 1
b // 2
d // 4

// 报错
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};

let [x, y, z] = new Set(['a', 'b', 'c']);
x // "a"

//fibs是一个 Generator 函数
function* fibs() {
  let a = 0;
  let b = 1;
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

let [first, second, third, fourth, fifth, sixth] = fibs();
sixth // 5

// 解构赋值允许指定默认值,只有当一个数组成员严格等于undefined,默认值才会生效
let [x, y = 'b'] = ['a']; // x='a', y='b'
let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'
let [x = 1] = [null]; // x = null

// 默认值可以引用解构赋值的其他变量,但该变量必须已经声明。
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

// 可以解构对象
let { bar, foo } = { foo: 'aaa', bar: 'bbb' }; // foo = "aaa", bar = "bbb"
let {foo} = {bar: 'baz'}; // foo = undefined
// 这实际上说明,对象的解构赋值是下面形式的简写
let { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' };
let { foo: fooA, bar: barA } = { foo: 'aaa', bar: 'bbb' }; // fooA = 'aaa', barA = 'bbb'

var {x: y = 3} = {}; // y = 3

// 嵌套类型的解构
let obj = {
  p: [
    'Hello',
    { y: 'World' }
  ]
};

//数组本质是特殊的对象,因此可以对数组进行对象属性的解构
let arr = [1, 2, 3];
let {0 : first, [arr.length - 1] : last} = arr;
first // 1
last // 3

// 此时p是模式,不是变量
let { p: [x, { y }] } = obj;
x // "Hello"
y // "World"

// 下面方式可以获得p
let { p, p: [x, { y }] } = obj;
x // "Hello"
y // "World"
p // ["Hello", {y: "World"}]

// 嵌套赋值
let obj = {};
let arr = [];
({ foo: obj.prop, bar: arr[0] } = { foo: 123, bar: true });
obj // {prop:123}
arr // [true]

// 对象的解构赋值可以取到继承的属性。
const obj1 = {};
const obj2 = { foo: 'bar' };
Object.setPrototypeOf(obj1, obj2);
const { foo } = obj1;
foo // "bar"
// 上面代码中,对象obj1的原型对象是obj2。foo属性不是obj1自身的属性,而是继承自obj2的属性,解构赋值可以取到这个属性。

//数组本质是特殊的对象,因此可以对数组进行对象属性的解构
let arr = [1, 2, 3];
let {0 : first, [arr.length - 1] : last} = arr;
first // 1
last // 3

// 字符串的解构赋值
// 字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。

const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值。

let {length : len} = 'hello';
len // 5

// 函数解构
function add([x, y]){
  return x + y;
}

add([1, 2]); // 3

// 变量交换
let x = 1;
let y = 2;
[x, y] = [y, x]; // x= 2, y = 1

遍历Map结构

任何部署了 Iterator 接口的对象,都可以用for...of循环遍历。Map 结构原生支持 Iterator 接口,配合变量的解构赋值,获取键名和键值就非常方便。

const map = new Map();
map.set('first', 'hello');
map.set('second', 'world');

for (let [key, value] of map) {
  console.log(key + " is " + value);
}

字符串扩展

支持字符的 Unicode 表示法

"\u0061"
// "a"
"\uD842\uDFB7"
// "𠮷"
"\u20BB7"
// " 7"
"\u{20BB7}"
// "𠮷"
"\u{41}\u{42}\u{43}"
// "ABC"

ES6 为字符串添加了遍历器接口Iterator,可被for/of

支持字符串模板

`foo ${fn()} bar`
let obj = {x: 1, y: 2};
`${obj.x + obj.y}`

模板标签

alert`hello`
// 等同于
alert(['hello'])

// 但是,如果模板字符里面有变量,就不是简单的调用了,而是会将模板字符串先处理成多个参数,再调用函数。

let a = 5;
let b = 10;

tag`Hello ${ a + b } world ${ a * b }`;
// 等同于
tag(['Hello ', ' world ', ''], 15, 50);

字符串的新增方法

正则扩展

包括:

ES6 出现之前,字符串对象共有 4 个方法,可以使用正则表达式:match()replace()search()split()

ES6 将这 4 个方法,在语言内部全部调用RegExp的实例方法,从而做到所有与正则相关的方法,全都定义在RegExp对象上。

  • String.prototype.match 调用 RegExp.prototype[Symbol.match]
  • String.prototype.replace 调用 RegExp.prototype[Symbol.replace]
  • String.prototype.search 调用 RegExp.prototype[Symbol.search]
  • String.prototype.split 调用 RegExp.prototype[Symbol.split]
// 解构
let {groups: {one, two}} = /^(?<one>.*):(?<two>.*)$/u.exec('foo:bar');
one  // foo
two  // bar

// 替换
let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;
'2015-01-02'.replace(re, '$<day>/$<month>/$<year>')

数值的扩展

ES6允许_作为数字分隔符,无实际意义: 

1_2_3 === 123 // true

函数的扩展

非尾部参数无法省略

function f(x = 1, y) {
  return [x, y];
}
f() // [1, undefined]
f(2) // [2, undefined]
f(, 1) // 报错

函数的 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

箭头函数

function Timer() {
  this.s1 = 0;
  this.s2 = 0;
  // 箭头函数
  setInterval(() => {
    console.log("first:" + this.s1) //this.s1 = 0
    this.s1++
  }, 1000);
  // 普通函数
  setInterval(function () {
    console.log("second:" + this.s2) // this.s1 = undefined
    this.s2++;
  }, 1000);
}

var timer = new Timer();

setTimeout(() => console.log('s1: ', timer.s1), 3100);
setTimeout(() => console.log('s2: ', timer.s2), 3100);
// s1: 3
// s2: 0

Timer函数内部设置了两个定时器,分别使用了箭头函数和普通函数。箭头函数this绑定定义时所在的作用域(即Timer函数),普通函数this指向运行时所在的作用域(即全局对象)

总之,箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。

下面ES6转ES5代码:

// ES6
function foo() {
  setTimeout(() => {
    console.log('id:', this.id);
  }, 100);
}

// ES5
function foo() {
  var _this = this;

  setTimeout(function () {
    console.log('id:', _this.id);
  }, 100);
}
function foo() {
  return () => {
    return () => {
      return () => {
        console.log('id:', this.id);
      };
    };
  };
}

this总是指向foo的this,不管怎么调用,都是最外层对象

var f = foo.call({id: 1});
var t1 = f.call({id: 2})()(); // id: 1
var t2 = f().call({id: 3})(); // id: 1
var t3 = f()().call({id: 4}); // id: 1

// 另外,由于箭头函数没有自己的this,所以当然也就不能用call()、apply()、bind()这些方法去改变this的指向。下面的列子,this总是指向最外层,无论给它绑定哪个对象。

(function() {
  return [
    (() => this.x).bind({ x: 'inner' })()
  ];
}).call({ x: 'outer' });
// ['outer']


// 除了this,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:arguments、super、new.target。

function foo() {
  setTimeout(() => {
    console.log('args:', arguments);
  }, 100);
}

foo(2, 4, 6, 8)
// args: [2, 4, 6, 8]
// 上面代码中,箭头函数内部的变量arguments,其实是函数foo的arguments变量。

不适合使用箭头函数的场景

const cat = {
  lives: 9,
  jumps: () => {
    this.lives--;
  }
}

如果写成上面那样的箭头函数,使得this指向全局对象,因此不会得到预期结果。这是因为对象不构成单独的作用域,导致jumps箭头函数定义时的作用域就是全局作用域。如果是普通函数,该方法内部的this指向cat;所以建议: 对象的属性建议使用传统的写法定义,不要用箭头函数定义。

第二个场合是需要动态this的时候,也不应使用箭头函数。

var button = document.getElementById('press');
button.addEventListener('click', () => {
  this.classList.toggle('on');
});

上面代码运行时,点击按钮会报错,因为button的监听函数是一个箭头函数,导致里面的this就是全局对象。如果改成普通函数,this就会动态指向被点击的按钮对象。

尾调用优化

尾调用(Tail Call)是函数式编程的一个重要概念,本身非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数。

// 是尾调用
function f(x){
  return g(x);
}
// m,n都是尾调用
function f(x) {
  if (x > 0) {
    return m(x)
  }
  return n(x);
}

以下三种情况,都不属于尾调用。

// 情况一
function f(x){
  let y = g(x);
  return y;
}

// 情况二
function f(x){
  return g(x) + 1; 或 return g(x) + q(x);
}

// 情况三
function f(x){
  g(x);
}

 非尾调用会形成很长的调用栈,把下一个调用的信息也存起来,很容易造成内存溢出。

对于递归调用,很耗内存,我们一般需要进行“尾调用优化”,这就是说,ES6 中只要使用尾递归,就不会发生栈溢出,相对节省内存。

function factorial(n, total = 1) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

factorial(5) // 120

ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。

这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。

  • func.arguments:返回调用时函数的参数。
  • func.caller:返回调用当前函数的那个函数。

尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效。

数组的扩展

// 数组的扩展和解构
const arr1 = ['a', 'b'];
const arr2 = ['c'];
[...arr1, ...arr2]
// [ 'a', 'b', 'c' ]

[a, ...rest] = [1,2]
// a = 1
// rest =[2]

// 字符解构
[...'hello']
// [ "h", "e", "l", "l", "o" ]

'x\uD83D\uDE80y'.length // 4 四个字节
[...'x\uD83D\uDE80y'].length // 3 数组长度为3 [ 'x', '🚀', 'y' ]

任何定义了遍历器(Iterator)接口的对象,都可以用扩展运算符转为真正的数组。querySelectorAll方法返回的是一个NodeList对象就实现了 Iterator。

Arrary.from,可以将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)

Array.from(new Set(['a', 'b'])) // ['a', 'b']
Array.from('hello')// ['h', 'e', 'l', 'l', 'o']
Array.from([1, 2, 3]) //[1, 2, 3]

// 类似数组的对象,本质特征只有一点,即必须有length属性
Array.from({0:"x",1:"y", length: 2})// [ 'x', 'y' ]

// Array.of()方法用于将一组值,转换为数组,弥补Array构造函数的不足。
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]

// copyWithin()方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组,会修改当前数组。
// 将3号位开始,4号位结束的元素,替换到0号位开始
[1, 2, 3, 4, 5].copyWithin(0, 3, 4)
// [4, 2, 3, 4, 5]

// find和findIndex,用于查找元素,比较常用:
[1, 4, -5, 10].find((n) => n < 0)// -5  返回找到的元素,否则undefined
[1, 4, -5, 10].findIndex((n) => n < 0) //2 // 返回下标,否则-1
// 由于NaN === NaN // false
[NaN].indexOf(NaN) // -1
[NaN].findIndex(y => Object.is(NaN, y))// 0 弥补了这个问题

// fill方法使用给定值,填充一个数组。
['a', 'b', 'c'].fill(7) // [7, 7, 7]
['a', 'b', 'c'].fill(7, 1, 2) // ['a', 7, 'c'] 指定填充的起始位置和结束位置

// entries(),keys() 和 values(), 用于遍历数组。它们都返回一个遍历器对象,可以用for...of循环进行遍历
for (let index of ['a', 'b'].keys()) {
  console.log(index);
}
// 0
// 1

for (let value of ['a', 'b'].values()) {
  console.log(value);
}
// 'a'
// 'b'

for (let [index, value] of ['a', 'b'].entries()) {
  console.log(index, value);
}
// 0 "a"
// 1 "b"

// Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似
[1, 2, 3].includes(2)     // true
[1, 2, 3].includes(4)     // false
[1, 2, NaN].includes(NaN) // true


// 数组的空位,数组的空位指的是,数组的某一个位置没有任何值,比如Array()构造函数返回的数组都是空位。空位不是undefined,是没值。
0 in [undefined, undefined, undefined] // true
0 in [, , ,] // false

// 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(['a',,'b'])
// [ "a", undefined, "b" ]
[...['a',,'b']]
// [ "a", undefined, "b" ]

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

// sort
arr.sort((x,y)=> {return -1 or 1})

对象的扩展

// 允许简写对象
const foo = 'bar';
const baz = {foo}; // 等价 {foo: foo}

// 表达式作为属性名,这时要将表达式放在方括号之内。
obj['a' + 'bc'] = 123;
let propKey = 'foo';
let obj = {
  [propKey]: true,
  ['a' + 'bc']: 123
};

//属性名表达式与简洁表示法,不能同时使用,会报错
// 报错
const foo = 'bar';
const bar = 'abc';
const baz = { [foo] };

// super 关键字
// 我们知道,this关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字super,指向当前对象的原型对象。
// super关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错。
const proto = {
  foo: 'hello'
};

const obj = {
  foo: 'world',
  find() {
    return super.foo;
  }
};

Object.setPrototypeOf(obj, proto);
obj.find() // "hello"

// 报错
const obj = {
  foo: super.foo
}

// 报错
const obj = {
  foo: () => super.foo
}

// 报错
const obj = {
  foo: function () {
    return super.foo
  }
}

对象的解构赋值

let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x // 1
y // 2
z // { a: 3, b: 4 }

Object新增方法

// Object.is就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。
Object.is('foo', 'foo')
// true
Object.is({}, {})
// false

不同之处只有两个:一是+0不等于-0,二是NaN等于自身。

+0 === -0 //true
NaN === NaN // false

Object.is(+0, -0) // false
Object.is(NaN, NaN) // true

// Object.assign()方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。
const target = { a: 1 };

const source1 = { b: 2 };
const source2 = { c: 3 };

Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}

// Object.getOwnPropertyDescriptors()方法,返回指定对象所有自身属性(非继承属性)的描述对象。
const obj = {
  foo: 123,
  get bar() { return 'abc' }
};

Object.getOwnPropertyDescriptors(obj)
// { foo:
//    { value: 123,
//      writable: true,
//      enumerable: true,
//      configurable: true },
//   bar:
//    { get: [Function: get bar],
//      set: undefined,
//      enumerable: true,
//      configurable: true } }

// Object.setPrototypeOf方法的作用与__proto__相同,用来设置一个对象的原型对象(prototype),返回参数对象本身
// 格式
Object.setPrototypeOf(object, prototype)

// 用法
const o = Object.setPrototypeOf({}, null);
//该方法等同于下面的函数。
function setPrototypeOf(obj, proto) {
  obj.__proto__ = proto;
  return obj;
}

// Object.getPrototypeOf()该方法与Object.setPrototypeOf方法配套,用于读取一个对象的原型对象。
Object.getPrototypeOf(obj);

// Object.keys(),Object.values(),Object.entries()

运算符扩展

指数运算符

2 ** 2 // 4
2 ** 3 // 8

链判断运算符

// 在之前判空需要一层层的判断,很不方便.
// ES2020 引入了“链判断运算符”(optional chaining operator)?.,简化上面的写法。写法如下:
// 上面代码使用了?.运算符,直接在链式调用的时候判断,左侧的对象是否为null或undefined。
// 如果是的,就不再往下运算,而是返回undefined
const firstName = message?.body?.user?.firstName || 'default';

链判断运算符?.有三种写法。

  • obj?.prop // 对象属性是否存在
  • obj?.[expr] // 同上
  • func?.(...args) // 函数或对象方法是否存在

短路机制 - ?.运算符相当于一种短路机制,只要不满足条件,就不再往下执行

括号的影响 - 只对圆括号内部有影响。

(a?.b).c
// 等价于
(a == null ? undefined : a.b).c
// 上面代码中,?.对圆括号外部没有影响,不管a对象是否存在,圆括号后面的.c总是会执行。

以下写法是禁止的,会报错。

// 构造函数
new a?.()
new a?.b()

// 链判断运算符的右侧有模板字符串
a?.`{b}`
a?.b`{c}`

// 链判断运算符的左侧是 super
super?.()
super?.foo

// 链运算符用于赋值运算符左侧
a?.b = c

右侧不得为十进制数值

为了保证兼容以前的代码,允许foo?.3:0被解析成foo ? .3 : 0,因此规定如果?.后面紧跟一个十进制数字,那么?.不再被看成是一个完整的运算符,而会按照三元运算符进行处理

 Null 判断运算符

我们一般通过||运算符指定默认值,ES2020 引入了一个新的 Null 判断运算符??。它的行为类似||,但是只有运算符左侧的值为nullundefined时,才会返回右侧的值。
response.settings?.animationDuration ?? 300
// 上面代码中,如果response.settings是null或undefined,
// 或者response.settings.animationDuration是null或undefined,就会返回默认值300。
// 也就是说,这一行代码包括了两级属性的判断。

 逻辑赋值运算符

ES2021 引入了三个新的逻辑赋值运算符

// 或赋值运算符
x ||= y
// 等同于
x || (x = y)

// 与赋值运算符
x &&= y
// 等同于
x && (x = y)

// Null 赋值运算符
x ??= y
// 等同于
x ?? (x = y)

// 这三个运算符||=、&&=、??=相当于先进行逻辑运算,然后根据运算结果,再视情况进行赋值运算。

// 老的写法
user.id = user.id || 1;

// 新的写法
user.id ||= 1;

// 老的写法
function example(opts) {
  opts.foo = opts.foo ?? 'bar';
  opts.baz ?? (opts.baz = 'qux');
}

// 新的写法
function example(opts) {
  opts.foo ??= 'bar';
  opts.baz ??= 'qux';
}

Symbol

ES5 的对象属性名都是字符串,这容易造成属性名的冲突。比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法(mixin 模式),新方法的名字就有可能与现有方法产生冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。这就是 ES6 引入Symbol的原因。

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

// 没有参数的情况
let s1 = Symbol();
let s2 = Symbol();

s1 === s2 // false

// 有参数的情况
let s1 = Symbol('foo');
let s2 = Symbol('foo');

s1 === s2 // false

// Symbol函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的Symbol函数的返回值是不相等的

s1.description // "foo"
String(s1) // "Symbol(foo)"
s1.toString() // "Symbol(foo)"

由于每一个 Symbol 值都是不相等的,这意味着 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!"

Symbol.for(),Symbol.keyFor() 

Symbol.for()与Symbol()这两种写法,都会生成新的 Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。比如,如果你调用Symbol.for("cat")30 次,每次都会返回同一个 Symbol 值,但是调用Symbol("cat")30 次,会返回 30 个不同的 Symbol 值。

let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');

s1 === s2 // true
// 上面代码中,s1和s2都是 Symbol 值,但是它们都是由同样参数的Symbol.for方法生成的,所以实际上是同一个值。
let s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"

let s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined

Symbol.keyFor()方法返回一个已登记的 Symbol 类型值的key