写在开头
ES6
常用但被忽略的方法 系列文章,整理作者认为一些日常开发可能会用到的一些方法、使用技巧和一些应用场景,细节深入请查看相关内容连接,欢迎补充交流。
相关文章
- ES6常用但被忽略的方法(第一弹解构赋值和数值)
- ES6常用但被忽略的方法(第三弹Symbol、Set 和 Map )
- ES6常用但被忽略的方法(第四弹Proxy和Reflect)
- ES6常用但被忽略的方法(第五弹Promise和Iterator)
- ES6常用但被忽略的方法(第六弹Generator )
- ES6常用但被忽略的方法(第七弹async)
- ES6常用但被忽略的方法(第八弹Class)
- ES6常用但被忽略的方法(第九弹Module)
- ES6常用但被忽略的方法(第十弹项目开发规范)
- ES6常用但被忽略的方法(第十一弹Decorator)
- ES6常用但被忽略的方法(终弹-最新提案)
函数扩展
函数默认值
- 函数可以通过定义时给对应的参数赋一个默认值,默认值当函数调用时如果没有传入对应的值便会使用默认值。设置默认值还可以搭配解构赋值一起使用。
function detanx(x = 1) { ... }
// 搭配解构赋值
function detanx({name, age = 10} = {name: 'detanx', age: 10}) {...}
// 不完全解构,
function detanx({name, age = 10} = {name: 'detanx'}) {...}
detanx({age: 12}) // undefined 12
- 不完全解构时,即使后面的对象中有对应的键值也不会被赋值。
- 函数有默认值的参数在参数个数中间,并且后面的参数不实用默认值,这时要使用默认值的参数可以传入
undefined
。
function detanx(x = null, y = 6) {
console.log(x, y);
}
detanx(undefined, null); // null null
- 默认值会改变
length
获取函数参数返回的长度。
- 指定了默认值以后,函数的
length
属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length
属性将失真。
(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
- 默认值的参数不是尾参数,那么
length
属性也不再计入后面的参数。
(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1
- 使用扩展运算符表示接收的参数,
length
为0
.
(function(...args) {}).length // 0
- 设置默认值后作用域改变
- 设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(
context
)。
var x = 1;
function detanx(x, y = x) {
console.log(y);
}
f(2) // 2
function detanx(x = x, y = x) {
console.log(x, y);
}
f(undefined,2) // ReferenceError: Cannot access 'x' before initialization
函数默认值应用
- 指定某一个参数不得省略,如果省略就抛出一个错误。
function throwIfMissing() {
throw new Error('Missing parameter');
}
function foo(mustBeProvided = throwIfMissing()) {
return mustBeProvided;
}
foo()
// Error: Missing parameter
- 参数
mustBeProvided
的默认值等于throwIfMissing
函数的运行结果(注意函数名throwIfMissing
之后有一对圆括号),这表明参数的默认值不是在定义时执行,而是在运行时执行。如果参数已经赋值,默认值中的函数就不会运行。
严格模式变化
ES5
开始,函数内部可以设定为严格模式。ES2016
做了一点修改,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。
// 报错
function doSomething(a, b = a) {
'use strict';
// code
}
// 报错
const doSomething = function ({a, b}) {
'use strict';
};
// 报错
const doSomething = (...a) => {
'use strict';
};
const obj = {
// 报错
doSomething({a, b}) {
'use strict';
}
};
- 这种限制有个问题就是在声明时都是先执行参数再执行函数体,只有在进入函数体发现用了严格模式才会报错。
- 规避这种限制的方法。
// 第一种是设定全局性的严格模式,这是合法的。
'use strict';
function doSomething(a, b = a) {
// code
}
// 第二种是把函数包在一个无参数的立即执行函数里面。
const doSomething = (function () {
'use strict';
return function(value = 42) {
return value;
};
}());
箭头函数
- 使用注意点
- 函数体内的
this
对象,就是定义时所在的对象,而不是使用时所在的对象。 - 不可以当作构造函数,也就是说,不可以使用
new
命令,否则会抛出一个错误。 - 不可以使用
arguments
对象,该对象在函数体内不存在。如果要用,可以用rest
参数代替。 - 不可以使用
yield
命令,因此箭头函数不能用作Generator
函数。
- 函数体内的
- 箭头函数也可以像普通函数一样嵌套使用。
- 简化编写代码,但会降低代码的可读性。
// 柯里化
const curry = (fn, arr = []) => (...args) => (
arg => arg.length === fn.length
? fn(...arg)
: curry(fn, arg)
)([...arr, ...args]);
let curryTest = curry( (a, b) => a + b );
curryTest(1, 2) //返回3
curryTest(1)(2) //返回3
数组扩展
扩展运算符
- 应用
- 接收不确定参数数量
function add(...values) { let sum = 0; for (var val of values) { sum += val; } return sum; } add(2, 5, 3) // 10
- 替代
apply
方法
- 之前在某个函数需要接收数组参数时需要这样写
fn.apply(this, arr)
。
// ES5 的写法 const args = [0, 1, 2]; function f(x, y, z) { // ... } f.apply(null, args); // ES6的写法 function f(x, y, z) { // ... } f(...args);
- 复制、合并数组、转换字符串为数组
const a = [1] const b = [2]; // 复制数组 const c = [...a]; // [1] // 合并数组 [...a, ...b, ...c] // [1, 2, 1] // 转换字符串为数组 [...'detanx'] // ['d', 'e', 't', 'a', 'n', 'x']
- 实现了
Iterator
接口的对象
- 任何定义了遍历器(
Iterator
)接口的对象(参阅Iterator
一章),都可以用扩展运算符转为真正的数组。
Number.prototype[Symbol.iterator] = function*() { let i = 0; let num = this.valueOf(); while (i < num) { yield i++; } } console.log([...5]) // [0, 1, 2, 3, 4]
- 例如函数的
arguments
对象是一个类数组;querySelectorAll
方法返回的是一个NodeList
对象。它也是一个类似数组的对象。
let nodeList = document.querySelectorAll('div'); let array = [...nodeList];
Array.from
Array.from
的作用和扩展运算符类似,都可以将类数组转为数组,但它还可以接收第二参数,作用类似于数组的map
方法,用来对每个元素进行处理,将处理后的值放入返回的数组。
// 生成0-99的数组
Array.from(new Array(100), (x, i) => i);
copyWithin()
- 数组实例的
copyWithin()
方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。Array.prototype.copyWithin(target, start = 0, end = this.length)
target
(必需):从该位置开始替换数据。如果为负值,表示倒数。start
(可选):从该位置开始读取数据,默认为0
。如果为负值,表示从末尾开始计算。end
(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示从末尾开始计算。
- 这三个参数都应该是数值,如果不是,会自动转为数值。
// 将3号位复制到0号位
[1, 2, 3, 4, 5].copyWithin(0, 3, 4)
// [4, 2, 3, 4, 5]
// -2相当于3号位,-1相当于4号位
[1, 2, 3, 4, 5].copyWithin(0, -2, -1)
// [4, 2, 3, 4, 5]
// 将3号位复制到0号位
[].copyWithin.call({length: 5, 3: 1}, 0, 3)
// {0: 1, 3: 1, length: 5}
// 将2号位到数组结束,复制到0号位
let i32a = new Int32Array([1, 2, 3, 4, 5]);
i32a.copyWithin(0, 2);
// Int32Array [3, 4, 5, 4, 5]
// 对于没有部署 TypedArray 的 copyWithin 方法的平台
// 需要采用下面的写法
[].copyWithin.call(new Int32Array([1, 2, 3, 4, 5]), 0, 3, 4);
// Int32Array [4, 2, 3, 4, 5]
find() 和 findIndex()
- 两个方法都可以接收两个参数,第一个参数是回调函数(可以接收三个参数,分别是当前值,当前位置,当前的原数组),第二个参数用来绑定回调函数的
this
。区别是find
没有找到返回的是undefined
,findIndex
返回的是-1
。这两个方法都可以发现NaN
,弥补了数组的indexOf
方法的不足。
[1, 4, -5, 10].find((n) => n > 10) // undefined
[1, 5, 10, 15].findIndex(function(value, index, arr) {
return value > 15;
}) // -1
// 绑定this
function f(v){
return v > this.age;
}
let person = {name: 'John', age: 20};
[10, 12, 26, 15].find(f, person); // 26
// 找出NaN
[NaN].indexOf(NaN)
// -1
[NaN].findIndex(y => Object.is(NaN, y))
// 0
fill
fill
方法使用给定值,填充一个数组。
['a', 'b', 'c'].fill(7)
// [7, 7, 7]
new Array(3).fill(7)
// [7, 7, 7]
-
上面代码表明,
fill
方法用于空数组的初始化非常方便。数组中已有的元素,会被全部抹去。 -
fill
方法还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置。
- ['a', 'b', 'c'].fill(7, 1, 2)
// ['a', 7, 'c']
- 上面代码表示,
fill
方法从1
号位开始,向原数组填充7
,到2
号位之前结束。
注意,如果填充的类型为对象,那么被赋值的是同一个内存地址的对象,而不是深拷贝对象。
let arr = new Array(3).fill({name: "Mike"});
arr[0].name = "Ben";
arr
// [{name: "Ben"}, {name: "Ben"}, {name: "Ben"}]
let arr = new Array(3).fill([]);
arr[0].push(5);
arr
// [[5], [5], [5]]
includes
- 某个数组是否包含给定的值,与字符串的
includes
方法类似。与indexOf
的区别是includes
直接返回一个布尔值,indexOf
需要再判断返回值是否为-1
。当我们需要一个变量是否为多个枚举值之一时就可以使用includes
。
const name = 'detanx';
const nameArr = ['detanx', 'det', 'tanx']
// 使用includes做判断
if(nameArr.indexOf(name) !== -1) { ... }
=>
if(nameArr.includes(name)) { ... }
// 判断name是否是 detanx,tanx,det
if(name === 'detanx' || name === 'det' || name === 'tanx') {...}
=>
nameArr.includes(name);
数组空位
- 数组的空位指:数组的某一个位置没有任何值(空位不是
undefined
,一个位置的值等于undefined
,依然是有值的。)。比如,Array
构造函数返回的数组都是空位。
Array(3) // [, , ,]
forEach()
、filter()
、reduce()
、every()
和some()
都会跳过空位。
// 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()
会跳过空位,但会保留这个值。
// map方法
[,'a'].map(x => 1) // [,1]
join()
和toString()
会将空位视为undefined
,而undefined
和null
会被处理成空字符串。
// join方法
[,'a',undefined,null].join('#') // "#a##"
// toString方法
[,'a',undefined,null].toString() // ",a,,"
Array.from()
、...
、entries()
、keys()
、values()
、find()
和findIndex()
会将空位处理成undefined
。
Array.from(['a',,'b'])
// [ "a", undefined, "b" ]
[...['a',,'b']]
// [ "a", undefined, "b" ]
// 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
copyWithin()
、fill()
、for...of
会将空位视为正常的数组位置。
[,'a','b',,].copyWithin(2,0) // [,"a",,"a"]
new Array(3).fill('a') // ["a","a","a"]
let arr = [, ,];
for (let i of arr) {
console.log(1);
}
// 1
// 1
- 空位的处理规则非常不统一,所以建议避免出现空位。
对象扩展
对象属性枚举
- 对象的每个属性都有一个描述对象(
Descriptor
),用来控制该属性的行为。Object.getOwnPropertyDescriptor
方法可以获取该属性的描述对象。
let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, 'foo')
// {
// value: 123,
// writable: true,
// enumerable: true,
// configurable: true
// }
-
描述对象的
enumerable
属性,称为“可枚举性”,如果该属性为false
,就表示某些操作会忽略当前属性。 -
目前,有四个操作会忽略
enumerable
为false
的属性。for...in
循环:只遍历对象自身的和继承的可枚举的属性。Object.keys()
:返回对象自身的所有可枚举的属性的键名。JSON.stringify()
:只串行化对象自身的可枚举的属性。Object.assign()
: 忽略enumerable
为false
的属性,只拷贝对象自身的可枚举的属性。
-
Symbol
类型的属性只能通过getOwnPropertySymbols
方法获取。
链判断运算符
ES5
我们判断一个深层级的对象是否有某一个key
需要一层一层去判断,现在我们可以通过?.
的方式去获取。
// es5
// 错误的写法(当某一个key不存在undefined.key就会代码报错)
const firstName = message.body.user.firstName;
// 正确的写法
const firstName = (message
&& message.body
&& message.body.user
&& message.body.user.firstName) || 'default';
// es6
const firstName = message?.body?.user?.firstName || 'default';
- 链判断运算符有三种用法。
obj?.prop
// 对象属性obj?.[expr]
// 对象属性func?.(...args)
// 函数或对象方法的调用
a?.b
// 等同于
a == null ? undefined : a.b
a?.[x]
// 等同于
a == null ? undefined : a[x]
a?.b()
// 等同于
a == null ? undefined : a.b()
a?.()
// 等同于
a == null ? undefined : a()
- 注意点
- 短路机制
?.
运算符相当于一种短路机制,只要不满足条件,就不再往下执行。
// 当name不存在时,后面的就不再执行 user?.name?.firstName
delete
运算符
delete a?.b // 等同于 a == null ? undefined : delete a.b
- 上面代码中,如果
a
是undefined
或null
,会直接返回undefined
,而不会进行delete
运算。
- 括号的影响
- 如果属性链有圆括号,链判断运算符对圆括号外部没有影响,只对圆括号内部有影响。
(a?.b).c // 等价于 (a == null ? undefined : a.b).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
运算判断符??
- 读取对象属性的时候,如果某个属性的值是
null
或undefined
,有时候需要为它们指定默认值。常见做法是通过||
运算符指定默认值。 ||
运算符当左边为空字符串或者false
的时候也会设置默认值,??
运算符只有当左边是null
或undefined
才会设置默认值。
// || 运算
const a = '';
b = a || 1;
console.log(b) // 1
// ?? 运算
b = a ?? 1;
console.log(b) // ''
??
有一个运算优先级问题,它与&&
和||
的优先级孰高孰低,所以在一起使用时必须用括号表明优先级,否则会报错。
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
)的所有自身属性(不拷贝继承属性)、自身的Symbol
值属性和可枚举属性(属性的enumerable
为true
),复制到目标对象(target
)。第一个参数为目标对象,后面的都是源对象。
const target = { a: 1, b: 1 };
const source1 = { b: 2, c: 2 };
const source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
// 拷贝Symbol值属性
Object.assign({ a: 'b' }, { [Symbol('c')]: 'd' })
// { a: 'b', Symbol(c): 'd' }
- 传入
null
和undefined
会报错。
Object.assign(undefined) // 报错
Object.assign(null) // 报错
- 注意点
Object.assign
方法实行的是浅拷贝,而不是深拷贝。- 遇到同名属性,
Object.assign
的处理方法是替换,而不是添加。 - 数组的处理
Object.assign
可以用来处理数组,但是会把数组视为对象。
Object.assign([1, 2, 3], [4, 5]) // [4, 5, 3]
- 取值函数的处理
Object.assign
只能进行值的复制,如果要复制的值是一个取值函数,那么将求值后再复制。
const source = { get foo() { return 1 } }; const target = {}; Object.assign(target, source) // { foo: 1 }
Object.keys(),Object.values(),Object.entries()
Object.keys()
ES5
引入了Object.keys
方法,返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable
)属性的键名。我们如果只需要拿对象的键名就可以使用这个放。
let obj = { a: 1, b: 2, c: 3 };
for (let key of Object.keys(obj)) {
console.log(key); // 'a', 'b', 'c'
console.log(obj[key]) // 1, 2, 3
}
Object.values()
- 同
Object.keys
方法,不同的是一个返回键一个返回值。想通过这个方法拿键名就没有办法了。
let obj = { a: 1, b: 2, c: 3 };
for (let value of Object.values(obj)) {
console.log(value); // 1, 2, 3
}
// 数值的键名
const obj = { 100: 'a', 2: 'b', 7: 'c' };
Object.values(obj)
// ["b", "c", "a"]
- 属性名为数值的属性,是按照数值大小,从小到大遍历的,因此返回的顺序是
b
、c
、a
。 Object.values
会过滤属性名为Symbol
值的属性。
Object.entries()
Object.entries()
方法返回一个数,除了返回值类型不一样之外,其他的行为和Object.values
基本一致。
for (let [key, value] of Object.entries(obj)) {
console.log([key, value]); // ['a', 1], ['b', 2], ['c', 3]
}
- 对于遍历对象我现在常用以下写法
Object.entries(obj).forEach(([key, value]) => {
console.log([key, value]); // ['a', 1], ['b', 2], ['c', 3]
})
Object.fromEntries()
Object.fromEntries()
方法是Object.entries()
的逆操作,用于将一个键值对数组转为对象。主要目的,是将键值对的数据结构还原为对象。
Object.fromEntries([ ['foo', 'bar'],
['baz', 42]
])
// { foo: "bar", baz: 42 }
Map
结构就是一个键值对的数组,所以很适用于Map
结构转为对象。
// 例一
const entries = new Map([
['foo', 'bar'],
['baz', 42]
]);
Object.fromEntries(entries)
// { foo: "bar", baz: 42 }
// 例二
const map = new Map().set('foo', true).set('bar', false);
Object.fromEntries(map)
// { foo: true, bar: false }