ES6 笔记

240 阅读9分钟

EMAScript定义了语言的标准。

我们的node环境是由ECMAScript+NodeAPI组成的。

image.png 浏览器环境是由ECMAScript+DOM+BOM组成的。

image.png

let和const

ES6之前,只有一种定义变量的方式,即var。通过var定义的变量会遇到很多奇怪的问题,为了解决该问题,ES6定义了块级作用域并引入了letconst关键字。

var

简单举几个var会产生的问题。

if (true) {
  var foo = 'xhh'
}
console.log(foo) // xhh`
for (var i = 0; i < 3; i++) {
  for (var i = 0; i < 3; i++) {
    console.log(i)
  }
  // 打印了三次,两个for循环中使用了相同的i,当内层for循环执行结束之后,i的值是3,则不会执行外部的for循环
  console.log('内层结束 i = ' + i) 
}
var elements = [{}, {}, {}]
for (var i = 0; i < elements.length; i++) {
  elements[i].onclick = function () {
    console.log(i)
  }
}
// 打印的值为2,变量i绑定到了全局对象上 可以用let或者闭包来解决该问题
elements[0].onclick()

简单举一个变量提升的例子

console.log(foo);
var foo = 'xhh';

等价于下面的代码

var foo = undefined;
console.log(foo); // undefined
foo = 'xhh';

let

let引入了块级作用域的概念,通过let声明的变量仅在该代码块内有效。针对上面的几个例子,可以将var更改为let,则可以正常执行。

{
    let foo = 'xhh'
}
console.log(foo); // ReferenceError: foo is not defined
// for 循环会产生两层作用域
for (let i = 0; i < 3; i++) {
  let i = 'foo'
  console.log(i) // foo
}

const

const也是块级作用域的,与let的区别是一旦使用const声明后,就不可以更改变量的指向了。

const foo = 'foo';
foo = 'bar'; // TypeError: Assignment to constant variable.
const obj = {}
// 对于数据成员的修改是没有问题的 因为声明后obj指向的是一个对象的地址,而操作obj对象时相当于对地址上的内容做操作,本质上地址是没有变化的
obj.name = 'xhh'

obj = {} // TypeError: Assignment to constant variable. 更改地址指向则会报错

Array destructuring

在日常工作中,曾经review过其他小伙伴的代码,其中充满了arr[0]arr[1]类似的代码,看上去满头雾水。

其实可以通过数组结构的方式来使代码更语义化一些。举一个简单的例子。

const arr = ['X', 'HH', 'XHH'];
// 常规的取值方式是使用arr[0], arr[1], arr[2]的方式来取值,ES6版本之后我们可以使用如下方式来结构
const [lastName, firstName, fullName] = arr;
// 当不需要前面两个变量时,也可以通过 ','来分割,避免产生多余变量
const [, , name] = arr;
// 同样的对于数组我们也可以指定默认值
const [, , , sex = 'MALE'] = arr;
console.log(sex); //MALE 可以看到即使我们在数组中没有声明 也是可以获取到对应值的

Object destructuring

  • 解构
const obj = { name: 'XHH', age: 18 }

let objName = obj.name;
let objAge = obj.age;

const { name, age } = obj
console.log(name)  // 可以看到代码已经变得相当简洁了,如果要取的属性更多,那么效果就会更明显
  • 变量重命名
// 当要解构的变量名与已定义变量名相同时,可以给待解构的变量名起别名
const name = 'tom'
const { name: objName } = obj
console.log(objName)
  • 变量默认值
const user = {firstname: 'X'};
const {firstname:fullName = 'XHH', role='Admin'} = user;
console.log(role); // Admin

Template string

模板字符串

const name = 'tom'
// 可以通过 ${} 插入表达式,表达式的执行结果将会输出到对应位置
const msg = `hey, ${name} --- ${1 + 2} ---- ${Math.random()}`
console.log(msg)
// 带标签的模板字符串

// 模板字符串的标签就是一个特殊的函数,
// 使用这个标签就是调用这个函数
// const str = console.log`hello world`

const name = 'tom'
const gender = false

function myTagFunc (strings, name, gender) {
  // console.log(strings, name, gender)
  // return '123'
  const sex = gender ? 'man' : 'woman'
  return strings[0] + name + strings[1] + sex + strings[2]
}

const result = myTagFunc`hey, ${name} is a ${gender}.`

console.log(result)

String ext methods

// 字符串的扩展方法

const message = "Error: foo is not defined.";

message.startsWith("Error"); // true
message.endsWith("."); // true
message.includes("foo"); // true

Parameter defaults

举一个简单的例子

function map(arr) {
  // arr = arr || [];
  arr.map((item) => item + 1);
}

为了避免arr为空报错的情况,我们会用||添加一个默认值。

现在我们可以直接在声明函数的时候为arr指定默认值,如下。

function map(arr = []) {
  arr.map((item) => item + 1);
}

Rest parameter

function foo (first, ...args) {
  console.log(first); // 1
  console.log(args) // [2,3,3]
}

foo(1, 2, 3, 4)

Spread parameter

// 展开数组参数

const arr = ['foo', 'bar', 'baz']

// console.log(
//   arr[0],
//   arr[1],
//   arr[2],
// )

// console.log.apply(console, arr)

console.log(...arr)

Arrow

使用箭头函数可以让函数写起来更简单

function inc (number) {
   return number + 1
}

// 最简方式
const inc = n => n + 1

// arr.filter(function (item) {
//   return item % 2
// })

// 常用场景,回调函数
arr.filter(i => i % 2)

arrow处理this。我们知道箭头函数定义的函数中是没有自己的this的,它会去最近的作用域内寻找依附的this

const test = () => this // 浏览器环境中执行
console.log(test()); // window
// 箭头函数与 this
// 箭头函数不会改变 this 指向

const person = {
  name: 'tom',
  // sayHi: function () {
  //   console.log(`hi, my name is ${this.name}`)
  // }
  sayHi: () => {
    console.log(`hi, my name is ${this.name}`)
  },
  sayHiAsync: function () {
    const _this = this
    setTimeout(function () {
      console.log(_this.name)
      // console.log(this); // Timeout
    }, 1000)

    // console.log(this) // person
    setTimeout(() => {
      console.log(this.name)
      // console.log(this) // person
    }, 1000)
  }
}

person.sayHiAsync()

Tips: 常规的react component中,定义的类属性函数是需要绑定作用域的,此时我们可以使用箭头函数来简化代码。

Object

对象字面量, 当变量名与对象的属性名相同时,可以使用如下方式进行简写。

// 对象字面量

const bar = "345";

const obj = { bar };
// 等价于 const obj = {bar:bar}

Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。

const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };

const returnedTarget = Object.assign(target, source);

console.log(target);
// expected output: Object { a: 1, b: 4, c: 5 }

console.log(returnedTarget);
// expected output: Object { a: 1, b: 4, c: 5 }
console.log(target === returnedTarget);
// true

Object.is()方法判断两个值是否为同一个值

// 一般我们的比较会使用 == 或者 === , == 一般是值的比较 ===是值与类型的比较
// 但是 === 无法判断 +0与-0是否相等,两个NaN也判断为不相等
console.log(
  // 0 == false              // => true
  // 0 === false             // => false
  // +0 === -0               // => true
  // NaN === NaN             // => false
  // Object.is(+0, -0)       // => false
  // Object.is(NaN, NaN)     // => true
)

简单看一下Object.isPolyfill

if (!Object.is) {
  Object.is = function(x, y) {
    // SameValue algorithm
    if (x === y) { // Steps 1-5, 7-10
      // Steps 6.b-6.e: +0 != -0
      return x !== 0 || 1 / x === 1 / y;
    } else {
      // Step 6.a: NaN == NaN
      return x !== x && y !== y;
    }
  };
}

Proxy

// Proxy 对象

// const person = {
//   name: 'zce',
//   age: 20
// }

// const personProxy = new Proxy(person, {
//   // 监视属性读取
//   get (target, property) {
//     return property in target ? target[property] : 'default'
//     // console.log(target, property)
//     // return 100
//   },
//   // 监视属性设置
//   set (target, property, value) {
//     if (property === 'age') {
//       if (!Number.isInteger(value)) {
//         throw new TypeError(`${value} is not an int`)
//       }
//     }

//     target[property] = value
//     // console.log(target, property, value)
//   }
// })

// personProxy.age = 100

// personProxy.gender = true

// console.log(personProxy.name)
// console.log(personProxy.xxx)

// Proxy 对比 Object.defineProperty() ===============

// 优势1:Proxy 可以监视读写以外的操作 --------------------------

// const person = {
//   name: 'zce',
//   age: 20
// }

// const personProxy = new Proxy(person, {
//   deleteProperty (target, property) {
//     console.log('delete', property)
//     delete target[property]
//   }
// })

// delete personProxy.age
// console.log(person)

// 优势2:Proxy 可以很方便的监视数组操作 --------------------------

// const list = []

// const listProxy = new Proxy(list, {
//   set (target, property, value) {
//     console.log('set', property, value)
//     target[property] = value
//     return true // 表示设置成功
//   }
// })

// listProxy.push(100)
// listProxy.push(100)

// 优势3:Proxy 不需要侵入对象 --------------------------

// const person = {}

// Object.defineProperty(person, 'name', {
//   get () {
//     console.log('name 被访问')
//     return person._name
//   },
//   set (value) {
//     console.log('name 被设置')
//     person._name = value
//   }
// })
// Object.defineProperty(person, 'age', {
//   get () {
//     console.log('age 被访问')
//     return person._age
//   },
//   set (value) {
//     console.log('age 被设置')
//     person._age = value
//   }
// })

// person.name = 'jack'

// console.log(person.name)

// Proxy 方式更为合理
const person2 = {
  name: 'zce',
  age: 20
}

const personProxy = new Proxy(person2, {
  get (target, property) {
    console.log('get', property)
    return target[property]
  },
  set (target, property, value) {
    console.log('set', property, value)
    target[property] = value
  }
})

personProxy.name = 'jack'

console.log(personProxy.name)
handler 方法触发方式
get读取某个属性
set写入某个属性
hasin 操作符
deletePropertydelete 操作符
getPropertyObject.getPropertypeOf()
setPropertyObject.setPrototypeOf()
isExtensibleObject.isExtensible()
preventExtensionsObject.preventExtensions()
getOwnPropertyDescriptorObject.getOwnPropertyDescriptor()
definePropertyObject.defineProperty()
ownKeysObject.keys() 、Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()
apply调用一个函数
construct用 new 调用一个函数

如有有没有用过的API,可以去MDN查找一下。

Reflect

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与proxy handlers的方法相同。Reflect不是一个函数对象,因此它是不可构造的。

Class

// class 关键词

// function Person (name) {
//   this.name = name
// }

// Person.prototype.say = function () {
//   console.log(`hi, my name is ${this.name}`)
// }

class Person {
  constructor (name) {
    this.name = name
  }

  say () {
    console.log(`hi, my name is ${this.name}`)
  }
}

const p = new Person('tom')
p.say()

Static method

// static 方法

class Person {
  constructor (name) {
    this.name = name
  }

  say () {
    console.log(`hi, my name is ${this.name}`)
  }

  static create (name) {
    return new Person(name)
  }
}
// 有点像函数式编程里面函子的构造方式
const tom = Person.create('tom')
tom.say()

class extends

// extends 继承

class Person {
  constructor (name) {
    this.name = name
  }

  say () {
    console.log(`hi, my name is ${this.name}`)
  }
}

class Student extends Person {
  constructor (name, number) {
    super(name) // 父类构造函数
    this.number = number
  }

  hello () {
    super.say() // 调用父类成员
    console.log(`my school number is ${this.number}`)
  }
}

const s = new Student('jack', '100')
s.hello()

// TODO 补充一下js的继承方式

Set

// Set 数据结构

const s = new Set()

s.add(1).add(2).add(3).add(4).add(2)

// console.log(s)

// s.forEach(i => console.log(i))

// for (let i of s) {
//   console.log(i)
// }

// console.log(s.size)

// console.log(s.has(100))

// console.log(s.delete(3))
// console.log(s)

// s.clear()
// console.log(s)

// 应用场景:数组去重

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

// const result = Array.from(new Set(arr))
const result = [...new Set(arr)]

console.log(result)

// 弱引用版本 WeakSet
// 差异就是 Set 中会对所使用到的数据产生引用
// 即便这个数据在外面被消耗,但是由于 Set 引用了这个数据,所以依然不会回收
// 而 WeakSet 的特点就是不会产生引用,
// 一旦数据销毁,就可以被回收,所以不会产生内存泄漏问题。

const fn = () => {};
const obj = {};
const set = new Set([fn, fn, obj, obj]);
console.log(set.size); // 2

Map

一直很好奇,既然对象是就是键值对的形式,为什么还会有map的产生呢,简单看一下下面的代码

const obj = {}
obj[true] = 'value'
obj[123] = 'value'
obj[{ a: 1 }] = 'value'
// 可以看到对象的所有键值在存储的时候都被转换成了字符串,这种情况如果键值是对象,那么存储很容易产生漏数据的情况
console.log(Object.keys(obj)) // [ '123', 'true', '[object Object]' ]
console.log(obj['[object Object]']) // value
const m = new Map();

const tom = { name: "tom" };
const tom2 = { name: "tom" };
m.set(tom, 90);
m.set(tom2, 100);
// 可以看到map的键值是没有限制的
console.log(m); // Map(2) { { name: 'tom' } => 90, { name: 'tom' } => 100 }

console.log(m.get(tom)); // 90

// // m.has()
// // m.delete()
// // m.clear()

// m.forEach((value, key) => {
//   console.log(value, key)
// })

// 弱引用版本 WeakMap
// 差异就是 Map 中会对所使用到的数据产生引用
// 即便这个数据在外面被消耗,但是由于 Map 引用了这个数据,所以依然不会回收
// 而 WeakMap 的特点就是不会产生引用,
// 一旦数据销毁,就可以被回收,所以不会产生内存泄漏问题。

Symbol

symbol是一个基本类型

typeof Symbol() === 'symbol' // true

它的出现是为了解决可拓展对象属性命名重复的问题, 下面是一个常见的例子,可以看到不同的文件中对相同的对象属性做了覆盖,这样就很容易出现bug

// common.js
const A = {};

// A.js
A.foo = 1;
// B.js
A.foo = 2;

下面我们看一下Symbol是如何解决该问题的。

// 两个 Symbol 永远不会相等

console.log(
  Symbol() === Symbol() 
)   // false

// Symbol 描述文本

console.log(Symbol('foo'))
console.log(Symbol('bar'))
console.log(Symbol('baz'))

// 使用 Symbol 为对象添加用不重复的键

const obj = {}
obj[Symbol()] = '123'
obj[Symbol()] = '456'
console.log(obj) // { [Symbol()]: '123', [Symbol()]: '456' }

// 案例2:Symbol 模拟实现私有成员

// a.js ======================================

const name = Symbol()
const person = {
  [name]: 'zce',
  say () {
    console.log(this[name])
  }
}
// 只对外暴露 person

// b.js =======================================

// 由于无法创建出一样的 Symbol 值,
// 所以无法直接访问到 person 中的「私有」成员
// person[Symbol()]
person.say()

// Symbol 补充

console.log(
  Symbol("foo") === Symbol("foo") // false
);

// Symbol 全局注册表 ----------------------------------------------------
// 如果有可能会被重用,那么可以使用Symbol.for来声明, 声明后会转换成字符串存储在全局注册表中

const s1 = Symbol.for("foo");
const s2 = Symbol.for("foo");
console.log(s1 === s2);

console.log(Symbol.for(true) === Symbol.for("true")); // true
// 此处同样会涉及到键值转换成字符串重复的问题,需要注意
console.log(Symbol.for({ a: 1 }) === Symbol.for({ b: 1 })); // true

// 内置 Symbol 常量 ---------------------------------------------------

console.log(Symbol.iterator) // Symbol(Symbol.iterator)
console.log(Symbol.hasInstance) // Symbol(Symbol.hasInstance)

const obj = {
  [Symbol.toStringTag]: 'XObject'  
}
console.log(obj.toString()) // [object XObject]

// Symbol 属性名获取 ---------------------------------------------------
// 需要注意 for循环遍历、Object.keys()和JSON.stringify是没有办法拿到Symbol定义的属性的
const obj = {
  [Symbol()]: "symbol value",
  foo: "normal value",
};

for (var key in obj) {
  console.log(key); //foo
}
console.log(Object.keys(obj)); // [ 'foo' ]
console.log(JSON.stringify(obj)); // {"foo":"normal value"}

console.log(Object.getOwnPropertySymbols(obj)); // [ Symbol() ]

For of

我们知道常规的遍历方式比较多。

  • for(let i = 0; i < arr.length; i++)
  • forEach
  • for(let key in obj)
  • 普通for循环代码比较啰嗦,常常会有一些临时变量
  • forEach没有办法终止循环,一般要使用some和every来配置使用
  • for in只可以遍历对象

es6引入了for of的遍历方式,请看如下案例。

// for...of 循环

const arr = [100, 200, 300, 400]

// for (const item of arr) {
//   console.log(item)
// }

// for...of 循环可以替代 数组对象的 forEach 方法

// arr.forEach(item => {
//   console.log(item)
// })

// for (const item of arr) {
//   console.log(item)
//   if (item > 100) {
//     break
//   }
// }

// forEach 无法跳出循环,必须使用 some 或者 every 方法

// arr.forEach() // 不能跳出循环
// arr.some()
// arr.every()

// 遍历 Set 与遍历数组相同

// const s = new Set(['foo', 'bar'])

// for (const item of s) {
//   console.log(item)
// }

// 遍历 Map 可以配合数组解构语法,直接获取键值

// const m = new Map()
// m.set('foo', '123')
// m.set('bar', '345')

// for (const [key, value] of m) {
//   console.log(key, value)
// }

// 普通对象不能被直接 for...of 遍历

const obj = { foo: 123, bar: 456 }

for (const item of obj) { // TypeError: obj is not iterable
  console.log(item) 
}

此处抛出了一个问题,为什么对象没有办法被for of 遍历,可以看到报错信息是 obj is not iterable。我们可以去简单看一下Array,Set,Map他们的原型上面都有一个Symbol(Symbol.iterator)属性,该属性是一个函数,提供了对for of的支持,如果对象实现了该方法,我们认为该对象是一个可迭代对象,即iterable

下面我们来看一下下面的例子,去除迭代器函数,每次调用next方法会返回{value: any, done: boolean} 的一个对象,当无内容可遍历时, done的属性值为truevalueundefined

// 迭代器(Iterator)

const set = new Set(['foo', 'bar', 'baz'])

const iterator = set[Symbol.iterator]() // [Set Iterator] { 'foo', 'bar', 'baz' }

console.log(iterator.next()) // { value: 'foo', done: false }
console.log(iterator.next()) // { value: 'bar', done: false }
console.log(iterator.next()) // { value: 'baz', done: false }
console.log(iterator.next()) // { value: undefined, done: true }
console.log(iterator.next()) // { value: undefined, done: true }

while (true) {
  const current = iterator.next()
  if (current.done) {
    break // 迭代已经结束了,没必要继续了
  }
  console.log(current.value)
}

我们使用对象模拟一下该实现,代码如下:

const obj = {
  store: ['foo', 'bar', 'baz'],

  [Symbol.iterator]: function () {
    let index = 0
    const self = this

    return {
      next: function () {
        const result = {
          value: self.store[index],
          done: index >= self.store.length
        }
        index++
        return result
      }
    }
  }
}

for (const item of obj) {
  console.log('循环体', item)
}

可以看到此时已经不在报错了,这里我们使用的是设计模式中的迭代器模式,迭代器模式的好处是使用者不需要知道可迭代对象内部的实现,可迭代的内容完全由可迭代对象来指定。

一个简单的todo list小应用

// 迭代器设计模式

// 场景:你我协同开发一个任务清单应用

// 我的代码 ===============================

const todos = {
  life: ['吃饭', '睡觉', '打豆豆'],
  learn: ['语文', '数学', '外语'],
  work: ['喝茶'],

  // 提供统一遍历访问接口
  each: function (callback) {
    const all = [].concat(this.life, this.learn, this.work)
    for (const item of all) {
      callback(item)
    }
  },

  // 提供迭代器(ES2015 统一遍历访问接口)
  [Symbol.iterator]: function () {
    const all = [...this.life, ...this.learn, ...this.work]
    let index = 0
    return {
      next: function () {
        return {
          value: all[index],
          done: index++ >= all.length
        }
      }
    }
  }
}

// 你的代码 ===============================

// for (const item of todos.life) {
//   console.log(item)
// }
// for (const item of todos.learn) {
//   console.log(item)
// }
// for (const item of todos.work) {
//   console.log(item)
// }

todos.each(function (item) {
  console.log(item)
})

console.log('-------------------------------')

for (const item of todos) {
  console.log(item)
}

Generator

生成器函数,这不是一个新鲜的东西,在Python中也有类似的概念,简单看一下下面的代码。生成器函数相当于是惰性执行的,调用之后不会立刻执行,需要有人去调用next方法才会将代码向下执行,每次调用next会停留在yield关键字处,等待下次调用。

function * foo () {
  console.log('1111')
  yield 100
  console.log('2222')
  yield 200
  console.log('3333')
  yield 300
}

const generator = foo()


console.log(generator.next()) // 第一次调用,函数体开始执行,遇到第一个 yield 暂停 { value: 100, done: false }
console.log(generator.next()) // 第二次调用,从暂停位置继续,直到遇到下一个 yield 再次暂停 { value: 200, done: false }
console.log(generator.next()) // 。。。 { value: 300, done: false } 
console.log(generator.next()) // 第四次调用,已经没有需要执行的内容了,所以直接得到 undefined { value: undefined, done: true }

一个常见的小应用就是银行的发号器

// Generator 应用

// 案例1:发号器

function * createIdMaker () {
  let id = 1
  while (true) {
    yield id++
  }
}

const idMaker = createIdMaker()

console.log(idMaker.next().value)
console.log(idMaker.next().value)
console.log(idMaker.next().value)
console.log(idMaker.next().value)

基于这种思想,我们可以优化一下上面的对象遍历代码。

const todos = {
  life: ['吃饭', '睡觉', '打豆豆'],
  learn: ['语文', '数学', '外语'],
  work: ['喝茶'],
  [Symbol.iterator]: function * () {
    const all = [...this.life, ...this.learn, ...this.work]
    for (const item of all) {
      yield item
    }
  }
}

for (const item of todos) {
  console.log(item)
}

Promise

Promise是一个很大的概念,我会在后面的异步编程的笔记里面补充。