ECMAScript新特性-Chapter1

190 阅读4分钟

概要

ECMAScript的新特性语法较多,部分需要了解,部分需要掌握,更有一部分应该烂记于心,这篇专题就描述这部分应该熟记于心的内容。

字符串的扩展

  • 模板字符串:模板字符串是字符串的扩展中最值得称道的功能了,可以在模板字符串内实现换行以及使用变量。
const job = 'front-end developer'
const str = `I am a ${job}!
And you ?`

console.log(str);
// I am a front-end developer!
// And you ?
  • includes方法:判断字符串是否包含某个指定字符
const str= 'Error: foo is not defined!'

console.log(str.includes('foo')); // true
console.log(str.includes('bar')); // false

  • startsWith方法:判断字符串是否以指定字符开头
const str= 'Error: foo is not defined!'

console.log(str.startsWith('Error')); // true
console.log(str.startsWith('bar')); // false
  • endsWith方法:判断字符串是否以指定字符结尾
const str= 'Error: foo is not defined!'

console.log(str.endsWith('!')); // true
console.log(str.endsWith('defined')); // false

数组的扩展

ES2015针对数组增加了许多扩展方法以及新增API,下面列举几个需要熟记的扩展方法或API

数组的解构

在ES5.1中,获取数组单个元素或者获取数组部分集合的时候,利用下标、splice、slice等方法或许是我们下意识的选择。

ES2015诞生后,这一意识应当被颠覆。数组的解构让开发者更方便获取数组中的特定元素或者某一部分的元素。

数组的解构遵循模式匹配原则,简而言之,即一一对应。

下面展示一些数组解构实际使用场景:

  • 为多个变量同时赋值 & 取出数组中的多个变量
const [a, b, c] = ["foo", 9, new Date()]

console.log(a); // foo
console.log(b); // 9
console.log(c); // Sat Aug 29 2020 00:42:19 GMT+0800 (中国标准时间)
  • 取出数组中除第一个元素外其余元素的集合
const [a, ...b] = [1,2,3,4,5,6]

console.log(a); // 1
console.log(b); // [2,3,4,5,6]
  • 思考:数组解构赋值,等式两侧必须都是数组吗?
  • 答案:实际上,数组的解构赋值依赖的是Iterator接口,只要等式右侧的数据结构具有Iterator接口,解构便可以进行。
// 如下面的解构,等式右侧是一个Set数据结构,解构成功执行
let [x, y, z] = new Set(['a', 'b', 'c']);
console.log(x); // "a"
console.log(y); // "b"
console.log(z); // "c"

Array.from方法

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

下面是一个类数组对象,Array.from将它转为真正的数组

const arrayLike = {
    0: '1',
    1: '2',
    2: '3',
    'length': '3'
}

const arr = Array.from(arrayLike);

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

Array.from同样可以将具有Iterable接口的数据结构转为数组,如原生的Set结构

const set = new Set();
set.add('a');
set.add('b');
set.add('c');

console.log(Array.from(set)); // ["a", "b", "c"]

Array.includes方法

ES5的时代,想要判断数组是是否包含某个元素,可能会使用Array.indexOf方法判断,该方法返回一个索引,表示元素在数组中的位置,如果没找到则返回-1

const arr = [1,2,3,4,NaN];

console.log(arr.indexOf(3)); // 2
console.log(arr.indexOf(NaN)); // -1

上面代码中,有一个问题就是indexOf方法内部使用严格相等运算算法,所以该方法认为NaN !== NaN,因此也就没法找到数组中的NaN元素

ES2015针对indexOf方法的不足和语义不明确问题,重新给出了一个API Array.includes,专门用来判断数组中是否包含某个元素,返回值是布尔类型

const arr = [1,2,3,4,NaN];

console.log(arr.includes(3)); // true
console.log(arr.includes(NaN)); // true
console.log(arr.includes('not')); // false

对象的解构赋值

类似于数组的解构,对象的解构在程序开发时同样能带来很多方便。

对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。

  • 获取对象中特定变量:有了对象解构,再也不需要ES5.1那种 var name = res.name; var age = res.age;的写法了
// 假定obj对象包含name、age以及job属性,现在需要同时获取这三个属性

const { name, age, job } = obj;
  • 将对象的某个方法赋值给指定变量:假定某个文件中共计需要执行1000次console.log方法,如果使用解构,可以大量减少代码的冗余
const { log } = console;

log('123') // 123
  • 对象解构时的重命名和默认值
const name = 'foo'
const obj = { name: 'tomas', age: 43 }

const { name: objNmae, read = true } = obj;

console.log(objNmae); // tomas
console.log(read); // true
  • 对象解构时使用扩展运算符
const { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 }

console.log(x); // 1
console.log(y); // 2
console.log(z); // { a: 3, b: 4 }

const foo = {...z, h: 5};

console.log(foo); // {a: 3, b: 4, h: 5}

函数默认值

在ES5.1的时代,函数的形参不能显式地设置默认值,只能在函数体中手动判断,ES2015增加了函数默认值功能,方便函数调用

  • ES5时代函数默认值
function foo(name, age){
    var name = name === undefined ? 'tom' : name;
    console.log(name);
}
foo() // tom
foo('jake') // jake 
  • ES2015函数参数默认值
function foo(name = 'tom', age = '52'){
    console.log(name);
}

foo() // tom
foo('hash') // hash
  • 函数参数默认值的位置:通常情况下,定义了默认值的参数,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是没法省略的。
// 例一
function f(x = 1, y) {
  return [x, y];
}

f() // [1, undefined]
f(2) // [2, undefined]
f(, 1) // 报错
f(undefined, 1) // [1, 1]

// 例二
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]
  • 函数参数默认值结合对象解构:对象的解构语法本身支持默认值,函数参数也同样支持默认值,那么二者结合会有什么有趣的事呢?
function foo({x = 0, y = 0} = {x:6,y:8}) {
  return [x, y];
}

foo() // [6,8]
foo({x: 'are',y: 'you'}) // ["are","you"]
foo({x: 'are'}) // ["are", 0]
foo({y: 'you'}) // [0, "you"]

函数参数默认值结合对象解构赋值可以在使用函数时,将多个参数放到一个对象中,并在定义函数时针对该参数对象的特定几个属性设置默认值。

对象的计算属性

ES5中,如果希望给某个对象设置动态属性,不能在字面量定义时设置,而只能针对已定义的对象添加动态属性,如下:

var abj = { name: 'tom' };

obj[Math.random()] = 'teenage';

而在ES2015中,可以使用字面量的方式去定义动态属性,如下:

const foo = 'name';

const obj = {
    [foo]: 'tom',
    [1 + 2]: 'number'
};

console.log(obj); // {3: "number", name: "tom"}

Proxy

Proxy可以理解成,在目标对象之前架设一层拦截,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy这个词的原意是代理,用在这里表示由它来代理某些操作,可以译为代理器

下面展示Proxy最基础的使用:

拦截读取行为:以下代码拦截person对象的读取行为,无论读取什么属性,都返回数字100

const person = {
    name: 'tom',
    age: 20
}

const personProxy = new Proxy(person, {
    get(target, property){
        return 100;
    }
})

console.log(personProxy.name); // 100

拦截属性赋值行为:以下代码拦截perosn对象的age属性设置操作,如果给age属性赋值非整数,则会抛出一个错误

const person = {
    name: 'tom',
    age: 20
};

const personProxy = new Proxy(person, {
    set(target, property, value){
        if (!Number.isInteger(value) && property === 'age'){
            throw new TypeError(`${value} is not an int`);
        }
    }
})

personProxy.age = 21.1 // Uncaught TypeError: 21.1 is not an int

Note:拦截操作只在代理对象上触发,如上面示例中的personProxy对象,而不是源对象。

当然,除了这两种最基础的拦截行为以外,ES2015提供了更多丰富的拦截行为,详情可参阅ES6标准入门Proxy章节

Proxy相较于Object.defineProperty的优势

  • 更多的监视操作或者说拦截操作
  • 对数组的更好支持,defineProperty方法针对数组的push、shift等方法无能为力,因此vue2.x版本只能采取重写Array.push等方法来解决这个问题,并且对于采用数组下标的赋值行为无能为力
  • Proxy是以非侵入的方式监管了对象的读写,也就是说,一个已经定义好的对象,我们不需要针对对象本身进行任何的操作就可以实现对象任意属性的监听;而defineProperty方法却需要我们针对那些需要监听的属性进行单独的设置,这造成了很多额外的操作。

Reflect

Reflect类可以看做是一个静态类,对它不能使用new操作符,只能调用它下面的十三个底层操作方法。如get函数、set函数、has函数等

Reflect存在的目的是统一对象的操作API,ES5中的delete操作符、in操作符、以及最典型的.操作符,这些都可以使用Reflect中的方法去替代

下面展示Reflect最基础的几个方法

Reflect.get

const person = {
    name: 'tom',
    age: 20
};

console.log(Reflect.get(person, 'name')); // tom

Reflect.has

const person = {
    name: 'tom',
    age: 20
};

console.log(Reflect.has(person, 'age')); // true

Object的一些扩展方法

ES2017和ES2016一般,仅仅只是增加了几个API,并没有大的语法变化,但是这些增加的API也需要熟记于心

Object.values方法

该方法类似于Object.keys方法,它可以获取到对象的所有属性值,并将其作为数组返回

const s = Symbol();
const person = {
    name: 'tom',
    age: 20,
    [s]: 'I am an Symbol type of value'
};

console.log(Object.values(person)); // ['tom',20]

Note:对于属性名为Symbol类型的值,values()方法不会返回它的值,这和keys方法的特性是一致的。

Object.entries方法

这个方法以数组的形式返回对象当中所有的键值对,返回值可以被很好地用于for-of循环

const person = {
    name: 'tom',
    age: 20
};

console.log(Object.entries(person)) 
// [ ["name", "tom"], ["age", 20] ]

for (const [key, value] of Object.entries(person)) {
    console.log(key, value);
}
// name tom
// age 20

Object.getOwnPropertyDescriptors方法

Object.assign方法可以用于复制对象的属性,但是对于get和set却无法复制,这个方法可以解决这个问题

const person = {
    firstName: 'tom',
    lastName: 'jack',
    get fullName(){
        return `${this.firstName}-${this.lastName}`
    }
}

console.log(person.fullName); // tom-jack

下面我们使用Object.assign复制这个对象,并且修改复制后对象的firstName

const foo = Object.assign({}, person);
foo.firstName = 'hash';

console.log(foo.fullName); // tom-jack

打印结果依然是tom-jack,这并不是我们期望的结果,原因在于使用Object.assign并不能将get和set函数原封不动的复制到新对象中

那么解决方法就是使用Object.getOwnPropertyDescriptors方法复制get和set函数

const descriptor = Object.getOwnPropertyDescriptors(person);

const bar = Object.defineProperties({}, descriptor);

bar.firstName = 'hash'

console.log(bar.fullName); // hash-jack

实现Iterable接口

无论何种数据结构,只要它的[Symbol.iterator]属性是一个方法,并且该方法能返回一个迭代器对象,该种数据结构便是可遍历的,便可以通过for of循环实现遍历

下面代码展示给普通对象添加可迭代(Iterable)接口

const foo = [1,2,3,4,5]
let index = 0
const person = {
    [Symbol.iterator](){
         return {
            next(){
                return {
                    value: foo[index],
                    done: index++ >= foo.length
                }
            }
        }
    }
}

for (const item of person){
    console.log(item);
}

// 1
// 2
// 3
// 4
// 5

可以看到,使用for-of循环成功遍历了person对象并返回了我们期望返回的结果。

除了手动控制迭代器对象的每一次迭代结果,还可以通过Generator函数自动生成迭代器对象,从而简化为自定义数据结构实现Iterable接口的过程。

const person = {
    man: ['man1','man2','man3'],
    women: ['women1', 'women2', 'women3'],
    god: ['god1', 'god2', 'god3'],
    *[Symbol.iterator](){
        const all = [...person['man'], ...person['women'], ...person['god']]
    
        for (const item of all){
            yield item
        }
    }
}

for (const item of person){
    console.log(item);
}
//  man1
//  man2
//  man3
//  women1
//  women2
//  women3
//  god1
//  god3
//  god2

上面代码中的person对象借助Generator函数实现了Iterable接口,同样,针对其他不同的数据结构以及期望的返回结果,可以根据自己的需求实现不同的返回内容