ES6笔记上(深入浅出ES6—阮一峰)

在线转换

Babel 提供一个REPL在线编译器,可以在线将 ES6 代码转为 ES5 代码。转换后的代码,可以直接作为 ES5 代码插入网页运行。


let和const命令

let和var的区别

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

上面代码中,变量i是var声明的,在全局范围内都有效,所以全局只有一个变量i。每一次循环,变量i的值都会发生改变,而循环内被赋给数组a的function在运行时,会通过闭包读到这同一个变量i,导致最后输出的是最后一轮的i的值,也就是10。

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

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

上面代码中,变量i是let声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是6。

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

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

上面代码输出了3次abc,这表明函数内部的变量i和外部的变量i是分离的。就是说i每循环一次 ,每次产生新的作用域 (块级作用域) 也就是把当前的j值保存下来

不存在变量提升

var命令会发生”变量提升“现象,即变量可以在声明之前使用,值为undefined。这种现象多多少少是有些奇怪的,按照一般的逻辑,变量应该在声明语句之后才可以使用。

为了纠正这种现象,let命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。


// var 的情况
console.log(foo); // 输出undefined
var foo = 2;

// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;

暂时性死区

只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。

var tmp = 123;

if (true) {
tmp = ‘abc’; // ReferenceError
let tmp;
}
上面代码中,存在全局变量tmp,但是块级作用域内let又声明了一个局部变量tmp,导致后者绑定这个块级作用域,所以在let声明变量前,对tmp赋值会报错。

ES6明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。

总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。

ES6 规定暂时性死区和let、const语句不出现变量提升

不允许重复声明

let不允许在相同作用域内,重复声明同一个变量。

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

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

因此,不能在函数内部重新声明参数。

function func(arg) {
  let arg; // 报错
}

function func(arg) {
  {
    let arg; // 不报错
  }
}

块级作用域

ES5 只有全局作用域和函数作用域,没有块级作用域

第一种场景,内层变量可能会覆盖外层变量。

var tmp = new Date();

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

f(); // undefined

上面代码的原意是,if代码块的外部使用外层的tmp变量,内部使用内层的tmp变量。但是,函数f执行后,输出结果为undefined,原因在于变量提升,导致内层的tmp变量覆盖了外层的tmp变量。

第二种场景,用来计数的循环变量泄露为全局变量。

var s = 'hello';

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

console.log(i); // 5

上面代码中,变量i只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。


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'}}}}};

上面代码使用了一个五层的块级作用域。外层作用域无法读取内层作用域的变量。

{{{{
  {let insane = 'Hello World'}
  console.log(insane); // 报错
}}}};

内层作用域可以定义外层作用域的同名变量。

{{{{
  let insane = 'Hello World';
  {let insane = 'Hello World'}
}}}};

块级作用域的出现,实际上使得获得广泛应用的立即执行函数表达式(IIFE)不再必要了。

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

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

块级作用域与函数声明


 function f() { console.log('I am outside!'); }

(function () {
  if (false) {
    // 重复声明一次函数f
    function f() { console.log('I am inside!'); }
  }

  f();
}());

上面代码在 ES5 中运行,会得到“I am inside!”,ES6 就完全不一样了,理论上会得到“I am outside!”。因为块级作用域内声明的函数类似于let,对作用域之外没有影响。

另外,还有一个需要注意的地方,考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。



// 函数声明语句
{
  let a = 'secret';
  function f() {
    return a;
  }
}

// 函数表达式
{
  let a = 'secret';
  let f = function () {
    return a;
  };
}

do 表达式

本质上,块级作用域是一个语句,将多个操作封装在一起,没有返回值。

{
  let t = f();
  t = t * t + 1;
}

上面代码中,块级作用域将两个语句封装在一起。但是,在块级作用域以外,没有办法得到t的值,因为块级作用域不返回值,除非t是全局变量。

do表达式,可以返回值。

let x = do {
  let t = f();
  t * t + 1;
};

上面代码中,变量x会得到整个块级作用域的返回值。


const 命令

基本用法

const声明一个只读的常量。一旦声明,常量的值就不能改变。

const PI = 3.1415;
PI // 3.1415

PI = 3;
// TypeError: Assignment to constant variable.

上面代码表明改变常量的值会报错。

const声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值。

const foo;
// SyntaxError: Missing initializer in const declaration

上面代码表示,对于const来说,只声明不赋值,就会报错。

const的作用域与let命令相同:只在声明所在的块级作用域内有效。

if (true) {
  const MAX = 5;
}
MAX // Uncaught ReferenceError: MAX is not defined

const命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。


if (true) {
  console.log(MAX); // ReferenceError
  const MAX = 5;
}

上面代码在常量MAX声明之前就调用,结果报错。

const声明的常量,也与let一样不可重复声明。

var message = "Hello!";
let age = 25;

// 以下两行都会报错
const message = "Goodbye!";
const age = 30;

常量foo储存的是一个地址,这个地址指向一个对象。不可变的只是这个地址,即不能把foo指向另一个地址,但对象本身是可变的,所以依然可以为其添加新属性。

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

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

下面是另一个例子。

const a = [];
a.push('Hello'); // 可执行
a.length = 0;    // 可执行
a = ['Dave'];    // 报错

上面代码中,常量a是一个数组,这个数组本身是可写的,但是如果将另一个数组赋值给a,就会报错。

如果真的想将对象冻结,应该使用Object.freeze方法。

const foo = Object.freeze({});
// 常规模式时,下面一行不起作用;
// 严格模式时,该行会报错
foo.prop = 123;

上面代码中,常量foo指向一个冻结的对象,所以添加新属性不起作用,严格模式时还会报错。

除了将对象本身冻结,对象的属性也应该冻结。下面是一个将对象彻底冻结的函数。

var constantize = (obj) => {
  Object.freeze(obj);
  Object.keys(obj).forEach( (key, i) => {
    if ( typeof obj[key] === 'object' ) {
      constantize( obj[key] );
    }
  });
};

ES6 声明变量的六种方法

ES5 只有两种声明变量的方法:var命令和function命令。ES6除了添加let和const命令,后面章节还会提到,另外两种声明变量的方法:import命令和class命令。所以,ES6 一共有6种声明变量的方法。


let arr = [1, 2, 3,4];
let {0 : first, 1:second, [arr.length - 1] : last} = arr;
console.log(first); // 1
console.log(second); //
console.log(last); // 3

上面代码对数组进行对象解构。数组arr的0键对应的值是1,[arr.length - 1]就是2键,对应的值是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

变量的解构赋值

用途

(1)交换变量的值

let x = 1;
let y = 2;
[x,y] = [y,x];
console.log(y);  //x = 2,y = 1;

这样的写法不仅简洁,而且易读,语义非常清晰。

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

// 返回一个数组
  function example() {
  return [1, 2, 3];
  }
  let [a, b, c] = example();
  console.table([a,b,c]);

结果:
这里写图片描述

(3)函数参数的定义
解构赋值可以方便地将一组参数与变量名对应起来。

// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);

// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});

(4)提取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]

上面代码可以快速提取 JSON 数据的值。

(5)函数参数的默认值

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’;这样的语句。

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

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

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

如果只想获取键名,或者只想获取键值,可以写成下面这样。

// 获取键名
for (let [key] of map) {
  // ...
}

// 获取键值
for (let [,value] of map) {
  // ...
}

这里说明下:Set 本身是一个构造函数,用来生成 Set 数据结构。它类似于数组,但是成员的值都是唯一的,没有重复的值。

例如:

var s = new Set();
[2,3,5,4,5,2,2].map(x => s.add(x))
for (i of s) {console.log(i)}
// 2 3 5 4

注:add()方法,向Set 数据结构数据结构添加元素。

(7)输入模块的指定方法
加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰。

const { SourceMapConsumer, SourceNode } = require("source-map");

字符串的扩展

codePointAt()

codePointAt方法是测试一个字符由两个字节还是由四个字节组成的最简单方法。

function is32Bit(c) {
  return c.codePointAt(0) > 0xFFFF;
}

is32Bit("") // true
is32Bit("a") // false

String.fromCodePoint()

ES5提供String.fromCharCode方法,用于从码点返回对应字符,但是这个方法不能识别32位的UTF-16字符(Unicode编号大于0xFFFF)。

String.fromCharCode(0x20BB7)
// "ஷ"

ES6提供了String.fromCodePoint方法,可以识别0xFFFF的字符,弥补了String.fromCharCode方法的不足。在作用上,正好与codePointAt方法相反。

String.fromCodePoint(0x20BB7)
// ""
String.fromCodePoint(0x78, 0x1f680, 0x79) === 'x\uD83D\uDE80y'
// true

注意,fromCodePoint方法定义在String对象上,而codePointAt方法定义在字符串的实例对象上。


字符串的遍历器接口

用for…of循环遍历

代码如下:

 for(let codePointAt of 'hicai'){
     console.log(codePointAt);
  }  

结果:
这里写图片描述

除了遍历字符串,这个遍历器最大的优点是可以识别大于0xFFFF的码点

var text = String.fromCodePoint(0x20BB7);

for (let i = 0; i < text.length; i++) {
  console.log(text[i]);
}
// " "
// " "

for (let i of text) {
  console.log(i);
}
// ""

上面代码中,字符串text只有一个字符,但是for循环会认为它包含两个字符(都不可打印),而
for…of循环会正确识别出这一个字符。


at()

ES5对字符串提供charAt方法,但不能识别码点大于0xFFFF的字符。
ES6的at方法,可以识别Unicode编号大于0xFFFF的字符,返回正确的字符。

'abc'.at(0) // "a"
''.at(0) // ""

normalize()

比如Ǒ(\u01D1)。另一种是提供合成符号(combining character),即原字符与重音符号的合成,两个字符合成一个字符,比如O(\u004F)和ˇ(\u030C)合成Ǒ(\u004F\u030C)。

这两种表示方法,在视觉和语义上都等价,但是JavaScript不能识别。

'\u01D1'==='\u004F\u030C' //false
'\u01D1'.length // 1
'\u004F\u030C'.length // 2

ES6提供字符串实例的normalize()方法,用来将字符的不同表示方法统一为同样的形式,这称为Unicode正规化。

'\u01D1'.normalize() === '\u004F\u030C'.normalize()
// true

includes(), startsWith(), endsWith()

ES5中的indexOf方法,用来检索字符串中指定字符串出现的位置 而ES6又提供了三种新方法。

includes():返回布尔值,表示是否找到了参数字符串。
startsWith():返回布尔值,表示参数字符串是否在源字符串的头部。
endsWith():返回布尔值,表示参数字符串是否在源字符串的尾部。

var s = 'Hello world!';
s.startsWith('Hello') // true
s.endsWith('!') // true
s.includes('o') // true

这三个方法都支持第二个参数,表示开始搜索的位置。

var s = 'Hello world!';
s.startsWith('world', 6) // true
s.endsWith('Hello', 5) // true
s.includes('Hello', 0) // true

注意:endsWith的行为与其他两个方法有所不同。它针对前n个字符,而其他两个方法针对从第n个位置直到字符串结束。


repeat()

repeat方法返回一个新字符串,表示将原字符串重复n次。

'x'.repeat(3) // "xxx"
'hello'.repeat(2) // "hellohello"
'na'.repeat(0) // ""

参数如果是小数,会被取整。

'na'.repeat(2.9) // "nana"

如果repeat的参数是负数或者Infinity,会报错。

'na'.repeat(Infinity)
// RangeError
'na'.repeat(-1)
// RangeError

但是,如果参数是0到-1之间的小数,则等同于0,这是因为会先进行取整运算。0到-1之间的小数,取整以后等于-0,repeat视同为0。

'na'.repeat(-0.9) // " "

参数NaN等同于0。

'na'.repeat(NaN) // ""

如果repeat的参数是字符串,则会先转换成数字。

'na'.repeat('na') // ""
'na'.repeat('3') // "nanana"

padStart(),padEnd()

字符串补全长度功能,padStart()用于头部补全,padEnd()用于尾部补全。

//padStart()
'x'.padStart(5, 'ab') // 'ababx'
'x'.padStart(4, 'ab') // 'abax'
//padEnd()
'x'.padEnd(5, 'ab') // 'xabab'
'x'.padEnd(4, 'ab') // 'xaba'

参数:第一个参数用来指定字符串的最小长度,第二个参数是用来补全的字符串。

如果原字符串的长度,等于或大于指定的最小长度,则返回原字符串。

'xxx'.padStart(2, 'ab') // 'xxx'
'xxx'.padEnd(2, 'ab') // 'xxx'

如果用来补全的字符串与原字符串,两者的长度之和超过了指定的最小长度,则会截去超出位数的补全字符串。

'abc'.padStart(10, '0123456789')
// '0123456abc'

如果省略第二个参数,默认使用空格补全长度。

'x'.padStart(4) // '   x'
'x'.padEnd(4) // 'x   '

padStart的常见用途是为数值补全指定位数。下面代码生成10位的数值字符串。

'1'.padStart(10, '0') // "0000000001"
'12'.padStart(10, '0') // "0000000012"
'123456'.padStart(10, '0') // "0000123456"

另一个用途是提示字符串格式。

'12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12"
'09-12'.padStart(10, 'YYYY-MM-DD') // "YYYY-09-12"

数值的扩展

Number.isFinite(), Number.isNaN()

Number.isFinite()用来检查一个数值是否为有限的(finite)。

Number.isFinite(15); // true
Number.isFinite(0.8); // true
Number.isFinite(NaN); // false
Number.isFinite(Infinity); // false
Number.isFinite(-Infinity); // false
Number.isFinite('foo'); // false
Number.isFinite('15'); // false
Number.isFinite(true); // false

Number.isNaN()用来检查一个值是否为NaN。

Number.isNaN(NaN) // true
Number.isNaN(15) // false
Number.isNaN('15') // false
Number.isNaN(true) // false
Number.isNaN(9/NaN) // true
Number.isNaN('true'/0) // true
Number.isNaN('true'/'true') // true

注意:它们与传统的全局方法isFinite()和isNaN()的区别在于,传统方法先调用Number()将非数值的值转为数值,再进行判断,而这两个新方法只对数值有效,Number.isFinite()对于非数值一律返回false, Number.isNaN()只有对于NaN才返回true,非NaN一律返回false。

例如:

isFinite(25) // true
isFinite("25") // true
Number.isFinite(25) // true
Number.isFinite("25") // false

isNaN(NaN) // true
isNaN("NaN") // true
Number.isNaN(NaN) // true
Number.isNaN("NaN") // false
Number.isNaN(1) // false

Number.parseInt(), Number.parseFloat()

ES6将全局方法parseInt()和parseFloat(),移植到Number对象上面,行为完全保持不变。


// ES5的写法
parseInt('12.34') // 12
parseFloat('123.45#') // 123.45

// ES6的写法
Number.parseInt('12.34') // 12
Number.parseFloat('123.45#') // 123.45

这样做的目的,是逐步减少全局性方法,使得语言逐步模块化。

Number.parseInt === parseInt // true
Number.parseFloat === parseFloat // true

Number.isInteger()

Number.isInteger()用来判断一个值是否为整数。

Number.isInteger(25) // true
Number.isInteger(25.0) // true
Number.isInteger(25.1) // false
Number.isInteger("15") // false
Number.isInteger(true) // false

Number.EPSILON

ES6在Number对象上面,新增一个极小的常量Number.EPSILON。

Number.EPSILON
// 2.220446049250313e-16
Number.EPSILON.toFixed(20)
// '0.00000000000000022204'

安全整数和Number.isSafeInteger()

JavaScript能够准确表示的整数范围在-2^53到2^53之间(不含两个端点),超过这个范围,无法精确表示这个值。

Math.pow(2, 53) // 9007199254740992

9007199254740992  // 9007199254740992
9007199254740993  // 9007199254740992

Math.pow(2, 53) === Math.pow(2, 53) + 1
// true

上面代码中,超出2的53次方之后,一个数就不精确了。

ES6引入了Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER这两个常量,用来表示这个范围的上下限。

Number.MAX_SAFE_INTEGER === Math.pow(2, 53) - 1
// true
Number.MAX_SAFE_INTEGER === 9007199254740991
// true

Number.MIN_SAFE_INTEGER === -Number.MAX_SAFE_INTEGER
// true
Number.MIN_SAFE_INTEGER === -9007199254740991
// true

Number.isSafeInteger()则是用来判断一个整数是否落在这个范围之内。

Number.isSafeInteger('a') // false
Number.isSafeInteger(null) // false
Number.isSafeInteger(NaN) // false
Number.isSafeInteger(Infinity) // false
Number.isSafeInteger(-Infinity) // false

Number.isSafeInteger(3) // true
Number.isSafeInteger(1.2) // false
Number.isSafeInteger(9007199254740990) // true
Number.isSafeInteger(9007199254740992) // false

Number.isSafeInteger(Number.MIN_SAFE_INTEGER - 1) // false
Number.isSafeInteger(Number.MIN_SAFE_INTEGER) // true
Number.isSafeInteger(Number.MAX_SAFE_INTEGER) // true
Number.isSafeInteger(Number.MAX_SAFE_INTEGER + 1) // false

Math对象的扩展

Math.trunc()

Math.trunc方法用于去除一个数的小数部分,返回整数部分。

Math.trunc(4.1) // 4
Math.trunc(4.9) // 4
Math.trunc(-4.1) // -4
Math.trunc(-4.9) // -4
Math.trunc(-0.1234) // -0

对于非数值,Math.trunc内部使用Number方法将其先转为数值。

Math.trunc('123.456')
// 123

对于空值和无法截取整数的值,返回NaN。

Math.trunc(NaN);      // NaN
Math.trunc('foo');    // NaN
Math.trunc();         // NaN

Math.sign()

Math.sign方法用来判断一个数到底是正数、负数、还是零。

它会返回五种值。
参数为正数,返回+1;
参数为负数,返回-1;
参数为0,返回0;
参数为-0,返回-0;
其他值,返回NaN。

Math.sign(-5) // -1
Math.sign(5) // +1
Math.sign(0) // +0
Math.sign(-0) // -0
Math.sign(NaN) // NaN
Math.sign('foo'); // NaN
Math.sign(); // NaN

Math.cbrt()

Math.cbrt方法用于计算一个数的立方根。( 3的立方)

Math.cbrt(-1) // -1
Math.cbrt(0)  // 0
Math.cbrt(1)  // 1
Math.cbrt(2)  // 1.2599210498948734

对于非数值,Math.cbrt方法内部也是先使用Number方法将其转为数值。

Math.cbrt('8') // 2 
Math.cbrt('hello') // NaN

Math.clz32()

Math.clz32方法返回一个数的32位无符号整数形式有多少个前导0。

Math.clz32(0) // 32
Math.clz32(1) // 31
Math.clz32(1000) // 22
Math.clz32(0b01000000000000000000000000000000) // 1
Math.clz32(0b00100000000000000000000000000000) // 2

上面代码中,0的二进制形式全为0,所以有32个前导0;1的二进制形式是0b1,只占1位,所以32位之中有31个前导0;1000的二进制形式是0b1111101000,一共有10位,所以32位之中有22个前导0。

Math.imul()

Math.imul方法返回两个数以32位带符号整数形式相乘的结果,返回的也是一个32位的带符号整数。

Math.imul(2, 4)   // 8
Math.imul(-1, 8)  // -8
Math.imul(-2, -2) // 4

Math.fround()

Math.fround方法返回一个数的单精度浮点数形式。

Math.fround(0)     // 0
Math.fround(1)     // 1
Math.fround(1.337) // 1.3370000123977661
Math.fround(1.5)   // 1.5
Math.fround(NaN)   // NaN

Math.hypot()

Math.hypot方法返回所有参数的平方和的平方根。

Math.hypot(3, 4);        // 5
Math.hypot(3, 4, 5);     // 7.0710678118654755
Math.hypot();            // 0
Math.hypot(NaN);         // NaN
Math.hypot(3, 4, 'foo'); // NaN
Math.hypot(3, 4, '5');   // 7.0710678118654755
Math.hypot(-3);          // 3

上面代码中,3的平方加上4的平方,等于5的平方。

如果参数不是数值,Math.hypot方法会将其转为数值。只要有一个参数无法转为数值,就会返回NaN。


对数方法

(1) Math.expm1()

Math.expm1(x)返回ex - 1,即Math.exp(x) - 1。

Math.expm1(-1) // -0.6321205588285577
Math.expm1(0)  // 0
Math.expm1(1)  // 1.718281828459045

(2)Math.log1p()
Math.log1p(x)方法返回1 + x的自然对数,即Math.log(1 + x)。如果x小于-1,返回NaN。

Math.log1p(1)  // 0.6931471805599453
Math.log1p(0)  // 0
Math.log1p(-1) // -Infinity
Math.log1p(-2) // NaN

(3)Math.log10()
Math.log10(x)返回以10为底的x的对数。如果x小于0,则返回NaN。

Math.log10(2)      // 0.3010299956639812
Math.log10(1)      // 0
Math.log10(0)      // -Infinity
Math.log10(-2)     // NaN
Math.log10(100000) // 5

( 4 ) Math.log2()
Math.log2(x)返回以2为底的x的对数。如果x小于0,则返回NaN。

Math.log2(3)       // 1.584962500721156
Math.log2(2)       // 1
Math.log2(1)       // 0
Math.log2(0)       // -Infinity
Math.log2(-2)      // NaN
Math.log2(1024)    // 10
Math.log2(1 << 29) // 29

三角函数方法

ES6新增了6个三角函数方法。
Math.sinh(x) 返回x的双曲正弦(hyperbolic sine)
Math.cosh(x) 返回x的双曲余弦(hyperbolic cosine)
Math.tanh(x) 返回x的双曲正切(hyperbolic tangent)
Math.asinh(x) 返回x的反双曲正弦(inverse hyperbolic sine)
Math.acosh(x) 返回x的反双曲余弦(inverse hyperbolic cosine)
Math.atanh(x) 返回x的反双曲正切(inverse hyperbolic tangent)

Math.signbit()

Math.sign()用来判断一个值的正负,但是如果参数是-0,它会返回-0。

Math.signbit(2) //false
Math.signbit(-2) //true
Math.signbit(0) //false
Math.signbit(-0) //true

该方法的算法如下。

如果参数是NaN,返回false
如果参数是-0,返回true
如果参数是负值,返回true
其他情况返回false

指数运算符

ES2016 新增了一个指数运算符()**。

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

指数运算符可以与等号结合,形成一个新的赋值运算符(=)**。

 let b = 4;
 b **= 3;  // 等同于 b = b * b * b;
 console.log(b)  //64

数组的扩展

Array.from()

Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括ES6新增的数据结构Set和Map)。
下面是一个类似数组的对象,Array.from将它转为真正的数组。

 let arrayLike = {
    '0':'a',
    '1':'b',
    '2':'c',
    length:3
  };
  //ES5写法
  var arr1 = [].slice.call(arrayLike);
  //ES6写法
  let arr2 = Array.from(arrayLike);
  console.log(arr1);
  console.log(arr2); 

控制台结果:
这里写图片描述

实际应用中,常见的类似数组的对象是DOM操作返回的NodeList集合,以及函数内部的arguments对象。Array.from都可以将它们转为真正的数组。

举个例子:

<p>1</p>
<p>2</p>
<p>3</p>
<p>4</p>
<p>5</p>
<script>
let ps = document.querySelectorAll('p');
  Array.from(ps).forEach(function(item){
    console.log(item)
  })
</script>

控制台结果:
这里写图片描述


// arguments对象
function foo() {
  var args = Array.from(arguments);
  // ...
}

只要是部署了Iterator接口的数据结构,Array.from都能将其转为数组。

Array.from('hello')
// ['h', 'e', 'l', 'l', 'o']

let namesSet = new Set(['a', 'b'])
Array.from(namesSet) // ['a', 'b']

如果参数是一个真正的数组,Array.from会返回一个一模一样的新数组。

let arr = Array.from([1,2,3])
console.log(arr) //[1, 2, 3]

扩展运算符(…)也可以将某些数据结构转为数组。

// arguments对象
function foo() {
  var args = [...arguments];
}

// NodeList对象
[...document.querySelectorAll('div')]

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]

利用Array.from获取一组DOM节点的文本内容
举个例子:

<span>苹果</span>
<span>香蕉</span>
<span></span>
<span>西瓜</span>
<span>桃子</span>
<script>
    var spans = document.querySelectorAll('span');
    //ES5写法
    var names1 = Array.prototype.map.call(spans,function(i){
        return i.textContent;
    })
    console.log(names1);

   //ES6写法
   let spans2 = document.querySelectorAll('span');
   let names2 = Array.from(spans2,i => i.textContent);
   console.log(names2);
</script>

控制台结果都相同:
这里写图片描述

Array.from可以将数组中布尔值为false的成员转为0。
例如:

Array.from([1, , 2, , 3], (n) => n || 0)
// [1, 0, 2, 0, 3]

经过测试:NaN、undefined转化也为0

let arr = Array.from([1,NaN, 2,undefined, 3], (n) => n || 0)
console.log(arr);

Array.from返回各种数据的类型
举个例子:

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方法没有参数、一个参数、三个参数时,返回结果都不一样。只有当参数个数不少于2个时,Array()才会返回由参数组成的新数组。参数个数只有一个时,实际上是指定数组的长度。

Array.of基本上可以用来替代Array()或new Array(),并且不存在由于参数不同而导致的重载。它的行为非常统一。

Array.of() // []
Array.of(undefined) // [undefined]
Array.of(1) // [1]
Array.of(1, 2) // [1, 2]

Array.of总是返回参数值组成的数组。如果没有参数,就返回一个空数组。

数组实例的copyWithin()

数组实例的copyWithin方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组

Array.prototype.copyWithin(target, start = 0, end = this.length)

三个参数:
target(必需):从该位置开始替换数据。
start(可选):从该位置开始读取数据,默认为0。如果为负值,表示倒数。
end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示倒数。*

举个例子:

let arr = Array.of(1,2,3,4,5);
console.log(arr.copyWithin(0,3)) //[4, 5, 3, 4, 5]

上面代码表示将从3号位直到数组结束的成员(4和5),复制到从0号位开始的位置,结果覆盖了原来的1和2。

注意:三个参数都应该是数值,如果不是,会自动转为数值。

例如刚才的例子,输出结果相同:

console.log(arr.copyWithin(0,'3')) //[4, 5, 3, 4, 5]

数组实例的find()和findIndex()

find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined。

[1, 4, -5, 10].find((n) => n < 0)
// -5

find方法的回调函数可以接受三个参数,依次为当前的值、当前的位置和原数组。
切记:是返回第一个符合条件的值,就不会找下一个了。例如:

  var arr = [1, 5, 10, 15].find(function(value, index, arr) {
  return value > 9;

}) 
 console.log(arr)  //10

findIndex方法的用法与find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。

切记:是返回第一个符合条件值的索引值

[1, 5, 10, 15].findIndex(function(value, index, arr) {
  return value > 9;
}) // 2

另外,这两个方法都可以发现NaN,弥补了数组的IndexOf方法的不足。

[NaN].indexOf(NaN)
// -1
[NaN].findIndex(y => Object.is(NaN, y))
// 0

注意:Object.is()是ES6新增的用来比较两个值是否严格相等的方法,与===的行为基本一致。

例如:

var a = 3; 
var b = "3"; 

a==b;    // true
a===b;   // false,因为*a*,*b*的类型不一样 
Object.is( a, b );  //false,因为*a*,*b*的类型不一样 

数组实例的fill()

fill方法使用给定值,填充一个数组。

  let arr = new Array('a','b','c');
  console.log(arr.fill(7)) //[7, 7, 7];

  //fill方法用于空数组的初始化非常方便。数组中已有的元素,会被全部抹去。
  let arr2 = new Array(3).fill(6);
  console.log(arr2)// [6, 6, 6]

fill方法还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置
例如:fill方法从1号位开始,向原数组填充7,到2号位之前结束。

['a', 'b', 'c'].fill(7, 1, 2)// ['a', 7, 'c']

entries(),keys()和values()用于遍历数组

用于遍历数组。它们都返回一个遍历器对象,可以用for…of循环进行遍历。

keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历。

//entries()是对键值对的遍历
 let arr = new Array('A','B','C');
   for(let [index,elem] of arr.entries()){
      console.log(index,elem);
   }

这里写图片描述

//keys()是对键名的遍历
for (let index of ['a', 'b'].keys()) {
  console.log(index);
}
// 0
// 1
//values()是对键值的遍历
let arr = [['姓名','阿蔡'],['年龄','23']];
   for(var elem of arr.values()){  
         console.log(elem); 
  }

控制台结果:
这里写图片描述


includes()

Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似。

[1, 2, 3].includes(2);     // true
[1, 2, 3].includes(4);     // false
[1, 2, NaN].includes(NaN); // true

函数的扩展

基本用法

,可以把参数默认值写在参数定义后面
例如:

//ES5写法
function log(x, y) {
 if (typeof y === 'undefined') {
  y = 'World';
  }
  console.log(x, y);
}

log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello World

//ES6写法
function log(x,y='World'){
  console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello

可以看到,ES6 的写法比 ES5 简洁许多。

注意:参数变量是默认声明的,所以不能用let或const再次声明。

function foo(x = 5) {
  let x = 1; // error
  const x = 2; // error
}

参数默认值的位置

*注意:如果非尾部的参数设置默认值,实际上这个参数是没法省略的。是非位数!!如果省略就报错*
举个例子:

    function foo(x,y=1){
        console.log(x,y);
    }
    foo(1,); //[1,undefined]

    //如果是非尾部参数设置默认值
    function foo(x=1,y){
        console.log(x,y);
    }
   foo(,2) //报错

再举个例子:

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。

function foo(x = 5, y = 6) {
  console.log(x, y);
}

foo(undefined, null)
// 5 null

如果传入undefined,将触发该参数等于默认值,null则没有这个效果。


函数的 length 属性

指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length属性将失真。

(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2

length属性的含义是,该函数预期传入的参数个数。某个参数指定默认值以后,预期传入的参数个数就不包括这个参数了。同理,rest参数也不会计入length属性。

(function(...args) {}).length // 0

如果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数了。

(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1

作用域

函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。

  var x = 8;
   function f(x,y = x){
     console.log(y)
   } 

   f();//undefined
   f(2); //2
   f(1,3) //3

参数y的默认值等于变量x。调用函数f时,参数形成一个单独的作用域。在这个作用域里面,默认值变量x指向第一个参数x,而不是全局变量x,所以输出是2。

再看下面的例子。


let x = 1;

function f(y = x) {
  let x = 2;
  console.log(y);
}

f() // 1

上面代码中,函数f调用时,参数y = x形成一个单独的作用域。这个作用域里面,变量x本身没有定义,所以指向外层的全局变量x。函数调用时,函数体内部的局部变量x影响不到默认值变量x。

如果此时,全局变量x不存在,就会报错。

function f(y = x) {
  let x = 2;
  console.log(y);
}

f() // ReferenceError: x is not defined

这样写,也会报错。

var x = 1;

function foo(x = x) {
  // ...
}

foo() // ReferenceError: x is not defined

参数x = x形成一个单独作用域。实际执行的是let x = x,由于暂时性死区的原因,这行代码会报错”x 未定义“。

再举个例子:

var x = 1;
function foo(x, y = function() { x = 2; }) {
  var x = 3;
  y();
  console.log(x);
}

foo() // 3
x // 1

上面代码中,函数foo的参数形成一个单独作用域。这个作用域里面,首先声明了变量x,然后声明了变量y,y的默认值是一个匿名函数。这个匿名函数内部的变量x,指向同一个作用域的第一个参数x。函数foo内部又声明了一个内部变量x,该变量与第一个参数x由于不是同一个作用域,所以不是同一个变量,因此执行y后,内部变量x和外部全局变量x的值都没变。


应用

利用参数默认值,可以指定某一个参数不得省略,如果省略就抛出一个错误。

function throwIfMissing() {
  throw new Error('Missing parameter');
}

function foo(mustBeProvided = throwIfMissing()) {
  return mustBeProvided;
}

foo()
// Error: Missing parameter

上面代码的foo函数,如果调用的时候没有参数,就会调用默认值throwIfMissing函数,从而抛出一个错误。


rest参数

ES6 引入 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) {
  // ...
}

扩展运算符

扩展运算符(spread)是三个点(…)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。

console.log(...[1, 2, 3])
// 1 2 3

console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5

扩展运算符可以用于函数的调用

function push(array, ...items) {
  array.push(...items);
}
  function add(x,y){
    return x + y;
  }
  var numbers = [3,7];
  console.log(add(...numbers)); //10

扩展运算符还可以与正常函数参数结合使用

  function f(a,b,c,d,e){
    console.log(a,b,c,d,e)
  }
  var number = [3,5];
  f(1,...number,...[7,9])  //1,3,5,7,9

替代数组的apply方法

由于扩展运算符可以展开数组,所以不再需要apply方法,将数组转为函数的参数了。

// ES5的写法
function f(x, y, z) {
  // ...
}
var args = [0, 1, 2];
f.apply(null, args);

// ES6的写法
function f(x, y, z) {
  // ...
}
var args = [0, 1, 2];
f(...args);

通过push函数,将一个数组添加到另一个数组的尾部

//ES5的写法
var arr1 = [0,1,2];
var arr2 = [3,4,5];
Array.prototype.push.apply(arr1,arr2);

//ES6的写法
var arr1 = [0,1,2];
var arr2 = [3,4,5];
arr1.push(...arr2)

扩展运算符的应用

1.合并数组

// ES5
[1, 2].concat(more)
// ES6
[1, 2, ...more]

再举个例子:


   var arr1 = ['a','b'];
   var arr2 = ['c'];
   var arr3 = ['b'];

   //ES5写法
   var array = arr1.concat(arr2,arr3);
   console.log(array);    

  //ES6写法
  var array2 = [...arr1,...arr2,...arr3];
   console.log(array2);
   //["a", "b", "c", "b"]

注意:将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。

2.与解构赋值结合
扩展运算符可以与解构赋值结合起来,用于生成数组。

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];
// 报错

3.函数的返回值
JavaScript的函数只能返回一个值,如果需要返回多个值,只能返回数组或对象。扩展运算符提供了解决这个问题的一种变通方法。

var dateFields = readDateFields(database);
var d = new Date(...dateFields);

上面代码从数据库取出一行数据,通过扩展运算符,直接将其传入构造函数Date。

4.字符串
扩展运算符还可以将字符串转为真正的数组。

[...'hello']
// [ "h", "e", "l", "l", "o" ]

正确返回字符串长度的函数,识别32位的Unicode字符。

function length(str) {
  return [...str].length;
}

length('x\uD83D\uDE80y') // 3

5.实现了Iterator接口的对象
任何Iterator接口的对象,都可以用扩展运算符转为真正的数组。

var nodeList = document.querySelectorAll('div');
var array = [...nodeList];

6.Map和Set结构,Generator函数
扩展运算符内部调用的是数据结构的Iterator接口,因此只要具有Iterator接口的对象,都可以使用扩展运算符,比如Map结构。

let map = new Map([ //Map是键值对结构
  [1,'one'],
  [2,'two'],
  [3,'three']
])
let arr = [...map.keys()];
console.log(arr); // [1, 2, 3]

注意:Set和Map类似,也是一组key的集合,但不存储value。由于key不能重复,所以,在Set中,没有重复的key。


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"

箭头函数

(箭头函数导致this总是指向函数定义生效时所在的对象)

基本用法

ES6允许使用“箭头”(=>)定义函数。

var f = v => v;

上面的箭头函数等同于:

var f = function(v) {
  return v;
};

如果箭头函数不需要参数或多个参数:

var f = () => 5;
//等同于
var f = function(){return 5};

var sum = (num1,num2) => num1 + num2;
//等同于
var sum = function(num1,num2){return num1 + num2}

如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。

var sum = (num1, num2) => { return num1 + num2; }

由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号。

var getTempItem = id => ({ id: id, name: "Temp" });

箭头函数可以与变量解构结合使用。

const full = ({first,last}) => first + ' ' + last;

//等同于
function full(person){
   return person.first + ' ' + person.last;
}

箭头函数的一个用处是简化回调函数。

//ES5写法
[1,2,3].map(function(x)){
    return x * x;
}

//ES6写法
[1,2,3].map(x => x * x);

rest参数与箭头函数结合

const numbers = (...nums) => nums;
numbers(1,2,3,4) //[1,2,3,4]

const headAndTail = (head, ...tail) => [head, tail];
headAndTail(1, 2, 3, 4, 5)
// [1,[2,3,4,5]]

使用注意点

箭头函数有几个使用注意点。

(1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。

(2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。

(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用Rest参数代替。

(4)不可以使用yield命令,因此箭头函数不能用作Generator函数。

箭头函数this指向问题
箭头函数里面的this,绑定定义时所在的作用域,而不是指向运行时所在的作用域。

this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。


对象的扩展

属性的整洁表达法

ES6允许直接写入变量和函数,作为对象的属性和方法。这样书写更简洁。

  var foo = 'bar';
  var obj = {foo};
  obj //{foo: "bar"}

ES6 允许在对象之中,直接写变量。这时,属性名为变量名, 属性值为变量的值

//函数参数名直接作为对象属性值
  function f(x,y){
    return {obj1: x , obj2: y};
  } 
  f(1,2) //{obj1: 1, obj2: 2}

除了属性简写,方法也可以简写

  //ES5写法 
   var o = {
      method: function(){
         return 'hello';
      }
   }

   //ES6写法
   var o2 = {
     method:() => 'hello'
   }

再举个例子:

  var birth = '1995/1/5'
  var Person = {
      name:'阿蔡',
      birth,
      hello(){
         return 'hello'
      }

  } 
 console.log(Person);

控制台结果:
这里写图片描述

还可以,用于函数的返回值

function getPoint() {
  var x = 1;
  var y = 10;
  return {x, y};
}

getPoint() // {x:1, y:10}

属性名表达式

JavaScript语言定义对象的属性,有两种方法。

// 方法一:直接用标识符作为属性名
obj.foo = true;

// 方法二:表达式作为属性名,这时要将表达式放在方括号之内
obj[‘a’ + ‘bc’] = 123;

ES5和ES6字面量定义对象写法:

  // ES5
  var obj = {
       foo: true,
       abc: 123
  }
   console.log(obj);  //{foo: true, abc: 123}

  //ES6
  let propKey = 'foo';
  let obj2 = {
      [propKey]: true,
      ['a' + 'bc']: 123 
  }; 
  console.log(obj2);  //{foo: true, abc: 123}

再举个例子:

var lastWord = 'last word';

var a = {
  'first word': 'hello',
  [lastWord]: 'world'
};

a['first word'] // "hello"
a[lastWord] // "world"
a['last word'] // "world"

表达式还可以用于定义方法名

   let obj = {
      ['h' + 'ello'](){
         return 'hello'
      }
   }
   console.log(obj.hello())    //hello
   //或者可以这么写  obj['h' + 'ello']()

注意,属性名表达式与简洁表示法,不能同时使用,会报错。

正确写法:

    let name = '姓名:';
    let obj = {[name]:'阿蔡'} //{姓名:: "阿蔡"} 

方法的 name 属性

1、函数的name属性,返回函数名。对象方法也是函数,因此也有name属性。

举个例子:name属性返回函数名

  const person = {
      sayName(){
        return 'hello';
      }
   }
   console.log(person.sayName.name); //sayName

2、name属性读取取值函数(getter)和存值函数(setter)上的方法

const obj = {
  get foo() {},
  set foo(x) {}
};

obj.foo.name
// TypeError: Cannot read property 'name' of undefined

const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo');

descriptor.get.name // "get foo"
descriptor.set.name // "set foo"

3、bind方法创造的函数,name属性返回bound加上原函数的名字;Function构造函数创造的函数,name属性返回anonymous。

(new Function()).name // "anonymous"

var doSomething = function() {
  // ...
};
doSomething.bind().name // "bound doSomething"

4.如果对象的方法是一个 Symbol 值,那么name属性返回的是这个 Symbol 值的描述。

const key1 = Symbol('description');  //新的原始数据类型Symbol,表示独一无二的值
const key2 = Symbol();
let obj = {
  [key1]() {},
  [key2]() {},
};
obj[key1].name // "[description]"
obj[key2].name // ""

Object.is()

在ES5中,比较两个值是否相等,只有两个运算符:相等运算符(==)和严格等于运算符(===)。

但是缺点:
(==)会自动转换数据类型。
(===)NaN不等于自身,+0等于-0。

在ES6中,Object.is()可以比较两个值是否相等,与严格相等运算符行为基本一致,不同处在于:NaN等于自身,+0不等于-0

//ES6写法
 Object.is('foo','foo') //true
 Object.is({},{})  //false
 Object.is('+0','-0') //false
 Object.is(NaN,NaN) //true

//ES5写法
+0 === -0 //true
NaN === NaN //false

Object.assign()

基本用法
Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。

var targeObject.assign方法的第一个参数是目标对象,后面的参数都是源对象t = { a: 1 };

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

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

Object.assign方法的第一个参数是目标对象,后面的参数都是源对象。

1.注意,如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。

var target = { a: 1, b: 1 };

var source1 = { b: 2, c: 2 };
var source2 = { c: 3 };

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

再举个例子:

var target = { a: { b: 'c', d: 'e' } }
var source = { a: { b: 'hello' } }
Object.assign(target, source)
// { a: { b: 'hello' } }

2.对于这种嵌套的对象,一旦遇到同名属性,Object.assign的处理方法是替换,而不是添加。

3.如果该参数不是对象,则会先转成对象,然后返回。

typeof Object.assign(2) // "object"

4.undefined和null无法转成对象,所以如果它们作为参数,就会报错。

Object.assign(undefined) // 报错
Object.assign(null) // 报错

5.非对象参数出现在源对象的位置(即非首参数),那么处理规则有所不同。首先,这些参数都会转成对象,如果无法转成对象,就会跳过。这意味着,如果undefined和null不在首参数,就不会报错。

let obj = {a: 1};
Object.assign(obj, undefined) === obj // true
Object.assign(obj, null) === obj // true

6.其他类型的值(即数值、字符串和布尔值)不在首参数,也不会报错。但是,除了字符串会以数组形式,拷贝入目标对象,其他值都不会产生效果。

var v1 = 'abc';
var v2 = true;
var v3 = 10;

var obj = Object.assign({}, v1, v2, v3);
console.log(obj); // { "0": "a", "1": "b", "2": "c" }

v1、v2、v3分别是字符串、布尔值和数值,结果只有字符串合入目标对象(以字符数组的形式),数值和布尔值都会被忽略。这是因为只有字符串的包装对象,会产生可枚举属性。

7.Object.assign拷贝的属性是有限制的,只拷贝源对象的自身属性(不拷贝继承属性),也不拷贝不可枚举的属性(enumerable: false)

Object.assign({b: 'c'},
  Object.defineProperty({}, 'invisible', {
    enumerable: false,
    value: 'hello'
  })
)
// { b: 'c' }

8.属性名为Symbol值的属性,也会被Object.assign拷贝。

Object.assign({ a: 'b' }, { [Symbol('c')]: 'd' })
// { a: 'b', Symbol(c): 'd' }

注意:Object.assign方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。

9.Object.assign可以用来处理数组,但是会把数组视为对象。

Object.assign([1,2,3],[4,5]) //[4,5,3]

上面代码,Object.assign把数组视为属性名为0、1、2的对象,因此源数组的0号属性4覆盖了目标数组的0号属性1。

常见用途

(1)为对象添加属性
通过Object.assign方法,将x属性和y属性添加到Point类的对象实例。

class Point {
  constructor(x, y) {
    Object.assign(this, {x, y});
  }
}

(2)为对象添加方法

Object.assign(SomeClass.prototype, {
  someMethod(arg1, arg2) {
    ···
  },
  anotherMethod() {
    ···
  }
});

(3)克隆对象

function clone(origin) {
  return Object.assign({}, origin);
}

上面代码将原始对象拷贝到一个空对象,就得到了原始对象的克隆。

如果想要保持继承链,可以采用下面的代码。

function clone(origin) {
  let originProto = Object.getPrototypeOf(origin);
  return Object.assign(Object.create(originProto), origin);
}

(4)合并多个对象

将多个对象合并到某个对象。

const merge =
  (target, ...sources) => Object.assign(target, ...sources);

如果希望合并后返回一个新对象,可以改写上面函数,对一个空对象合并。

const merge =
  (...sources) => Object.assign({}, ...sources);

(5)为属性指定默认值

const DEFAULTS = {
  logLevel: 0,
  outputFormat: 'html'
};

function processContent(options) {
  options = Object.assign({}, DEFAULTS, options);
  console.log(options);
  // ...
}

上面代码中,DEFAULTS对象是默认值,options对象是用户提供的参数。Object.assign方法将DEFAULTS和options合并成一个新对象,如果两者有同名属性,则option的属性值会覆盖DEFAULTS的属性值。


属性可枚举性

对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。Object.getOwnPropertyDescriptor方法可以获取该属性的描述对象。

let obj = {foo:123};
Object.getOwnPropertyDescripto(obj,'foo')
//  {
//    value: 123,
//    writable: true,
//    enumerable: true,  //描述对象enumerable属性,称为“可枚举性”,如果该属性为false,就表示某些操作会忽略当前属性。
//    configurable: true
//  }

ES5有三个操作会忽略enumerable为false的属性。
for…in循环:只遍历对象自身的和继承的可枚举的属性
Object.keys():返回对象自身的所有可枚举的属性的键名
JSON.stringify():只串行化对象自身的可枚举的属性

ES6新增了一个操作Object.assign(),会忽略enumerable为false的属性,只拷贝对象自身的可枚举的属性。

toString和length属性的enumerable都是false,因此for…in不会遍历到这两个继承自原型的属性。

Object.getOwnPropertyDescriptor(Object.prototype,'toString').enumerable //false
Object.getOwnPropertyDescriptor([],'length').enumerable //false

另外,ES6规定,所有Class的原型的方法都是不可枚举的。

Object.getOwnPropertyDescriptor(class {foo(){}}.prototype, 'foo').enumerable // false

属性的遍历

ES6一共有5种方法可以遍历对象的属性。

var obj = {a:'1',[Symbol('b')]:2,d:4};
    //添加新的属性
    Object.defineProperty(obj,'c',{
        value:3,
        enumerable:false
    });

    console.log(obj); //{a: "1", d: 4, c: 3, Symbol(b): 2}

     //属性遍历

     //1.for...in循环,遍历对象自身和继承可枚举性
    for(var i in obj){
        console.log(i) //a , b
    }

     //2.Object.keys返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含Symbol属性)。
    console.log(Object.keys(obj)); //["a", "d"]

    //3.Object.getOwnPropertyNames返回一个数组,包含对象自身的所有属性(不含Symbol属性,但是包括不可枚举属性)。
    console.log(Object.getOwnPropertyNames(obj)) //["a", "d", "c"]


    //4.Object.getOwnPropertySymbols(obj)返回一个数组,包含对象自身的所有Symbols属性
   console.log(Object.getOwnPropertySymbols(obj))   //[Symbol(b)]

   //5返回一个数组,包含对象自身的所有属性,不管是属性名是Symbol或字符串,也不管是否可枚举。
   console.log(Reflect.ownKeys(obj)) //["a", "d", "c", Symbol(b)]

以上的5种方法遍历对象的属性,都遵守同样的属性遍历的次序规则。

首先遍历所有属性名为数值的属性,按照数字排序。
其次遍历所有属性名为字符串的属性,按照生成时间排序。
最后遍历所有属性名为Symbol值的属性,按照生成时间排序。


proto属性,Object.setPrototypeOf(),Object.getPrototypeOf()

proto属性

proto属性(前后各两个下划线),用来读取或设置当前对象的prototype(原型)对象。目前,所有浏览器(包括 IE11)都部署了这个属性。

//es6的写法
var obj = {
  method:function(){...}
};
obj.__proto__ = someOtherObj;

//es5的写法
var obj = Object.create(someOtherObj);
obj.method = function() { ... };

注意:该属性没有写入 ES6 的正文,本质上是一个内部属性,而不是一个正式的对外的 API。无论从语义的角度,还是从兼容性的角度,都不要使用这个属性,而是使用下面的Object.setPrototypeOf()(写操作)、Object.getPrototypeOf()(读操作)、Object.create()(生成操作)代替。


Object.setPrototypeOf()

Object.setPrototypeOf方法的作用与_proto_相同,用来设置一个对象的prototype对象,返回参数对象本身。它是 ES6 正式推荐的设置原型对象的方法。

格式:

Object.setPrototypeOf(object, prototype)

用法:

var o = Object.setPrototypeOf({}, null)

举个例子:

let proto = {};
let obj = {x:10};
Object.setPrototypeOf(obj,proto); //proto对象设为obj对象的原型,所以从obj对象可以读取proto对象的属性。

proto.y = 20;
proto.z = 40;
obj.x //10
obj.y //20
obj.z //40

如果第一个参数不是对象,会自动转为对象。但是由于返回的还是第一个参数,所以这个操作不会产生任何效果。

Object.setPrototypeOf(1, {}) === 1 // true
Object.setPrototypeOf('foo', {}) === 'foo' // true
Object.setPrototypeOf(true, {}) === true // true

由于undefined和null无法转为对象,所以如果第一个参数是undefined或null,就会报错。


Object.getPrototypeOf()

该方法与Object.setPrototypeOf方法配套,用于读取一个对象的原型对象。

Object.getPrototypeOf(obj);

举个例子

 function foo() {
  // ...
}

var rec = new foo();

console.log(Object.getPrototypeOf(rec) === foo.prototype);// true

console.log(Object.setPrototypeOf(rec, Object.prototype)); //__proto__ :Object
console.log(Object.getPrototypeOf(rec) === foo.prototype);// false

如果参数不是对象,会被自动转为对象

Object.getPrototypeOf(1) // Number {[[PrimitiveValue]]: 0}
Object.getPrototypeOf('foo') //String {length: 0, [[PrimitiveValue]]: ""}
Object.getPrototypeOf(true) // Boolean {[[PrimitiveValue]]: false}

如果参数是undefined或null,则无法转化为对象。直接报错。

Object.getPrototypeOf(undefined)
Object.getPrototypeOf(null)
// TypeError: Cannot convert undefined or null to object

Object.keys()

ES5 引入了Object.keys方法,返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名。

var obj = {foo:'bar',baz:42};
Object.keys(obj) //['foo','baz'];

ES7 引入了跟Object.keys配套的Object.values和Object.entries,作为遍历一个对象的补充手段,供for…of循环使用。

let {keys,values,entries} = Object;
let obj = {a:1,b:2,c:3};
console.log(keys(obj))  //["a", "b", "c"]

for(let key of keys(obj)){
  console.log(key); //a,b,c
}

for(let value of values(obj)){
   console.log(value) // 1,2,3
}

for(let [key,value] of entries(obj)){
   console.log([key,value]);  // ['a', 1], ['b', 2], ['c', 3]
}

Object.values()

Object.values方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值。

var obj = {foo:'bar',baz:42};
Object.values(obj) // ["bar", 42]

属性名为数值的属性,是按照数值大小,从小到大遍历的。因此返回的顺序是b、c、a。

var obj = { 100: 'a', 2: 'b', 7: 'c' };
Object.values(obj) //["b", "c", "a"]

Object.values只返回对象自身的可遍历属性。

var obj = Object.create({}, {p:{value:1}});
Object.values(obj) //[]

上面代码中,用Object.create方法的第二参数添加对象属性,默认情况下不可遍历,因为p**属性描述对象enumerable默认是false**,Object.values不会返回这个属性。只要把enumerable改成true,Object.values就会返回属性p的值

var obj = Object.create({},{p:{
    value:1,
    enumerable:true
  }
});
Object.values(obj) // [42]

Object.values会过滤属性名为 Symbol 值的属性。

Object.values({ [Symbol()]: 123, foo: 'abc' });
// ['abc']

如果Object.values方法的参数是一个字符串,会返回各个字符组成的一个数组。

Object.values('foo')
// ['f', 'o', 'o']

如果参数不是对象,Object.values会先将其转为对象。由于数值和布尔值的包装对象,都不会为实例添加非继承的属性。所以,Object.values会返回空数组。

Object.values(42) // []
Object.values(true) // []

Object.entries

Object.entries方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对数组。

var obj = {foo:'bar',baz:'42'};
Object.entries(obj) //[foo:'bar',baz:'42']

除了返回值不一样,该方法的行为与Object.values基本一致。
如果原对象的属性名是一个Symbol值,该属性会被忽略。

var obj = {[Symbol()]:123,foo:'abc'};
Object.entries(obj) // ['foo','abc']

对象的扩展运算符

(1)解构赋值
对象的解构赋值用于从一个对象取值,相当于将所有可遍历的,但尚未被读取的属性,分配到指定的对象上面。所有的键和它们的值,都会拷贝到新对象上面。

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

解构赋值要求等会右边是一个对象,所以如果等会右边是undefined或null,就会报错。因为它们无法转化为对象。

let {x,y,...z} = null;
let {x,y,...z} = undefined
//运行时发生错误

解构赋值必须是最后一个参数,否则会报错

let {...x,y,z} = obj //句法错误
let {x,...y,...z} = obj //句法错误

只要解构赋值不是最后一个参数,就会报错。

注意,解构赋值的拷贝是浅拷贝,即如果一个键的值是复合类型的值(数组、对象、函数)、那么解构赋值拷贝的是这个值的引用,而不是这个值的副本。

let obj = {a:{b:1}};
let {...x} = obj;
obj.a.b = 2;
x.a.b //2

上面代码中,x是结构赋值所在的对象,拷贝了对象的a属性。a属性引用了一个对象,修改了这个对象的值,会影响到解构赋值对它的引用。

另外,解构赋值不会拷贝继承自原型对象的属性

let o1 = {a:1};
let o2 = {b:2};
o2.__proto__ = o1;
let o3 = {...o2};
o3 //{b:2}

上面代码中,对象o3是o2的拷贝,但是只复制了o2自身的属性,没有复制它的原型对象o1的属性。

解构赋值的一个用处,是扩展某个函数的参数,引入其他操作。

function foo({a,b}){
 //...
}
function wrapFoo({x,y,...restConfig}){
   // 使用x和y参数进行操作
  // 其余参数传给原始函数
  return foo(restConfig);
}

原始函数foo接受a和b作为参数,函数wrapFoo在foo的基础上进行了扩展,能够接受多余的参数,并且保留原始函数的行为。

(2)扩展运算符
扩展运算符(…)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中。

let z = {a:1,b:2};
let n = {...z};
n // {a:1, b:2}

这等同于使用Object.assign方法。

let aClone = {...a};
//等同于
let aClone = Object.assign({},a)

扩展运算符可以用于合并两个对象

let ab = { ...a, ...b };
// 等同于
let ab = Object.assign({}, a, b);

扩展运算符的参数对象之中,如果有取值函数get,这个函数是会执行的。

// 并不会抛出错误,因为x属性只是被定义,但没执行
let aWithXGetter = {
  ...a,
  get x() {
    throws new Error('not thrown yet');
  }
};

// 会抛出错误,因为x属性被执行了
let runtimeError = {
  ...a,
  ...{
    get x() {
      throws new Error('thrown now');
    }
  }
};

如果扩展运算符的参数是null或undefined,这两个值会被忽略,不会报错。

let obj = {...null,...undefined}; //不报错

Object.getOwnPropertyDescriptors()

ES5有一个Object.getOwnPropertyDescriptor方法,返回某个对象属性的描述对象(descriptor)。

var obj = {p:'a'};
Object.getOwnPropertyDescriptors(obj,'p') //切记描述对象要加上''
//{value: "a", writable: true, enumerable: true, configurable: true}

ES2017 引入了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: bar],
//      set: undefined,
//      enumerable: true,
//      configurable: true } }

Object.getOwnPropertyDescriptors方法返回一个对象,所有原对象的属性名都是该对象的属性名,对应的属性值就是该属性的描述对象。


Null 传导运算符

const firstName = message?.body?.user?.firstName || 'default';

上面代码有三个?.运算符,只要其中一个返回null或undefined,就不再往下运算,而是返回undefined。

“Null 传导运算符”有四种用法。
obj?.prop // 读取对象属性
obj?.[expr] // 同上
func?.(…args) // 函数或对象方法的调用
new C?.(…args) // 构造函数的调用

传导运算符之所以写成obj?.prop,而不是obj?prop,是为了方便编译器能够区分三元运算符?:(比如obj?prop:123)。

例子。

// 如果 anull 或 undefined, 返回 undefined
// 否则返回 a.b.c().d
a?.b.c().d

// 如果 a 是 null 或 undefined,下面的语句不产生任何效果
// 否则执行 a.b = 42
a?.b = 42

// 如果 a 是 null 或 undefined,下面的语句不产生任何效果
delete a?.b

猜你喜欢

转载自blog.csdn.net/qq_35036255/article/details/80344748
今日推荐