ES6常用但被忽略的方法(第二弹函数、数组和对象)

5,203 阅读14分钟

写在开头

  • ES6常用但被忽略的方法 系列文章,整理作者认为一些日常开发可能会用到的一些方法、使用技巧和一些应用场景,细节深入请查看相关内容连接,欢迎补充交流。

相关文章

函数扩展

函数默认值

  1. 函数可以通过定义时给对应的参数赋一个默认值,默认值当函数调用时如果没有传入对应的值便会使用默认值。设置默认值还可以搭配解构赋值一起使用。
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
  • 不完全解构时,即使后面的对象中有对应的键值也不会被赋值。
  1. 函数有默认值的参数在参数个数中间,并且后面的参数不实用默认值,这时要使用默认值的参数可以传入undefined
function detanx(x =  null, y = 6) {
  console.log(x, y);
}
detanx(undefined, null); // null null
  1. 默认值会改变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
  • 使用扩展运算符表示接收的参数,length0.
(function(...args) {}).length // 0
  1. 设置默认值后作用域改变
  • 设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(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

函数默认值应用

  1. 指定某一个参数不得省略,如果省略就抛出一个错误。
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;
  };
}());

箭头函数

  • 使用注意点
    1. 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。
    2. 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
    3. 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
    4. 不可以使用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

数组扩展

扩展运算符

  • 应用
    1. 接收不确定参数数量
    function add(...values) {
      let sum = 0;
      for (var val of values) {
        sum += val;
      }
      return sum;
    }
    add(2, 5, 3) // 10
    
    1. 替代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);
    
    1. 复制、合并数组、转换字符串为数组
    const a = [1]
    const b = [2];
    // 复制数组
    const c = [...a]; // [1]
    // 合并数组
    [...a, ...b, ...c] // [1, 2, 1]
    // 转换字符串为数组
    [...'detanx'] // ['d', 'e', 't', 'a', 'n', 'x']
    
    1. 实现了 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没有找到返回的是undefinedfindIndex返回的是-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) // [, , ,]
  1. 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
  1. map()会跳过空位,但会保留这个值。
// map方法
[,'a'].map(x => 1) // [,1]
  1. join()toString()会将空位视为undefined,而undefinednull会被处理成空字符串。
// join方法
[,'a',undefined,null].join('#') // "#a##"

// toString方法
[,'a',undefined,null].toString() // ",a,,"
  1. 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
  1. 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,就表示某些操作会忽略当前属性。

  • 目前,有四个操作会忽略enumerablefalse的属性。

    1. for...in循环:只遍历对象自身的和继承的可枚举的属性。
    2. Object.keys():返回对象自身的所有可枚举的属性的键名。
    3. JSON.stringify():只串行化对象自身的可枚举的属性。
    4. Object.assign(): 忽略enumerablefalse的属性,只拷贝对象自身的可枚举的属性。
  • 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';
  • 链判断运算符有三种用法。
    1. obj?.prop // 对象属性
    2. obj?.[expr] // 对象属性
    3. 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()
  • 注意点
    1. 短路机制
    • ?.运算符相当于一种短路机制,只要不满足条件,就不再往下执行。
    // 当name不存在时,后面的就不再执行
    user?.name?.firstName
    
    1. delete 运算符
    delete a?.b
    // 等同于
    a == null ? undefined : delete a.b
    
    • 上面代码中,如果aundefinednull,会直接返回undefined,而不会进行delete运算。
    1. 括号的影响
    • 如果属性链有圆括号,链判断运算符对圆括号外部没有影响,只对圆括号内部有影响。
    (a?.b).c
    // 等价于
    (a == null ? undefined : a.b).c
    
    1. 报错场合
    • 以下写法是禁止的,会报错。
    // 构造函数
    new a?.()
    new a?.b()
    
    // 链判断运算符的右侧有模板字符串
    a?.`{b}`
    a?.b`{c}`
    
    // 链判断运算符的左侧是 super
    super?.()
    super?.foo
    
    // 链运算符用于赋值运算符左侧
    a?.b = c
    
    1. 右侧不得为十进制数值
    • 为了保证兼容以前的代码,允许foo?.3:0被解析成foo ? .3 : 0,因此规定如果?.后面紧跟一个十进制数字,那么?.不再被看成是一个完整的运算符,而会按照三元运算符进行处理,也就是说,那个小数点会归属于后面的十进制数字,形成一个小数。

Null运算判断符??

  • 读取对象属性的时候,如果某个属性的值是nullundefined,有时候需要为它们指定默认值。常见做法是通过||运算符指定默认值。
  • ||运算符当左边为空字符串或者false的时候也会设置默认值,??运算符只有当左边是nullundefined才会设置默认值。
// || 运算
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值属性和可枚举属性(属性的enumerabletrue),复制到目标对象(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' }
  • 传入nullundefined会报错。
Object.assign(undefined) // 报错
Object.assign(null) // 报错
  • 注意点
    1. Object.assign方法实行的是浅拷贝,而不是深拷贝。
    2. 遇到同名属性,Object.assign的处理方法是替换,而不是添加。
    3. 数组的处理
      • Object.assign可以用来处理数组,但是会把数组视为对象
      Object.assign([1, 2, 3], [4, 5])
      // [4, 5, 3]
      
    4. 取值函数的处理
    • Object.assign只能进行值的复制,如果要复制的值是一个取值函数,那么将求值后再复制。
    const source = {
      get foo() { return 1 }
    };
    const target = {};
    Object.assign(target, source)
    // { foo: 1 }
    

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

  1. 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
}
  1. 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"]
  • 属性名为数值的属性,是按照数值大小,从小到大遍历的,因此返回的顺序是bca
  • Object.values会过滤属性名为 Symbol 值的属性。
  1. 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 }