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('p')]
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('p');
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)。
例子。
// 如果 a 是 null 或 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
相关阅读
Promise.resolve()方法 有时需要将现有对象转为 Promise 对象,Promise.resolve方法就起到这个作用。const jsPromise = Promise.re
一、简介 ECMAScript 和 JavaScript 的关系是,前者是后者的规格,后者是前者的一种实现 各大浏览器的最新版本,对 ES6 的支持可以查
new Set(),用来去重数组。let arr = [1, 2, 2, 3];let set = new Set(arr);let newArr = Array.from(set);console.log(newArr); /