let/const

var/函数

我们都知道var和函数存在变量提升的问题,它的声明会被提升到作用域的顶端。

  1. if(true) {
  2. var value = 1
  3. }
  4. console.log(value) //1
  5. //代码相当于
  6. var value
  7. if(true) {
  8. value = 1
  9. }
  10. console.log(value) //1

为了加强对变量生命周期的控制,ES 2015引入了块级作用域,let 和const。
块级作用域在于:

  • 函数内部
  • 块中的()字符和{}之间的区域

    临时死区(Temporal Dead Zone)

    简称TDZ,因为 JavaScript 引擎在扫描代码发现变量声明时,要么将它们提升到作用域顶部(遇到 var 声明),要么将声明放在 TDZ 中(遇到 let 和 const 声明)。访问 TDZ 中的变量会触发运行时错误。只有执行过变量声明语句后,变量才会从 TDZ 中移出,然后方可访问。
    1. console.log(typeof value); // Uncaught ReferenceError: value is not defined
    2. let value = 1;
    所以其实let 和const 也是有提升只是存放到了暂时性死区里,在没有变量声明之前访问报错。
    看一个老生常谈的问题 for循环
    1. for(let i=0; i<3; i++) {
    2. let i = 'foo'
    3. console.log(i)
    4. }
    5. // 拆分就是
    6. let i = 0;
    7. if(i<3) {
    8. let i = 'foo'
    9. console.log(i)
    10. }
    11. i++
    12. if(i<3) {
    13. let i = 'foo'
    14. console.log(i)
    15. }
    16. i++
    17. if(i<3) {
    18. let i = 'foo'
    19. console.log(i)
    20. }
    21. // 所以说for是双层作用域
    22. // for(let ...)这是一层作用域
    23. // 里面又是内层作用域
    相当于在 for(let i=0; i<3; i++) 有一个隐藏的作用域。

    const

    再来说下const,const 用于声明常量,其值一旦被设定不能再被修改,否则会报错。不允许声明之后修改内存地址 就是说,不允许修改绑定,但允许修改值这就意味着当const声明对象时
    1. const obj = {}
    2. obj.name = 'tom'
    3. //完全没有问题

    建议

    在我们开发中主用const,辅助用let,不用var。

变量的解构赋值

从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。

数组的结构赋值

原来我们数组赋值必须指定值。

  1. const arr = [1,2,3]
  2. const f = arr[0]
  3. const b = arr[1]
  4. const c = arr[2]

ES2015 允许这样写

  1. const arr = [1,2,3]
  2. const [f, b, c] = arr
  3. console.log(f,b,c)
  4. //1 2 3

上面代码表示,可以从数组中提取值,按照对应位置,对变量赋值。写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。

解构不成功
  1. let [f] = [];
  2. let [f, b] = [1];

如果解构不成功,变量的值就等于undefined。以上两种情况都属于解构不成功,f/b的值都会等于undefined.

不完全解构

另一种情况是不完全解构,即等号左边的模式,只匹配一部分的等号右边的数组。

  1. const arr = [1,2,3]
  2. const [f, b] = arr
  3. console.log(f,b)
  4. //1 2

这种情况下,解构依然可以成功。

默认值

解构赋值允许指定默认值。

  1. const arr = [1,2,3]
  2. const [f, b, c, d='more'] = arr
  3. console.log(f,b,c,d)

这种数组的解构赋值很大程度上帮助缩短我们写代码逻辑,比如分割字符串获取字段

  1. const path = 'foo/part/ccc'
  2. const tmp = path.split('/')
  3. const root = tmp[1]
  4. console.log(root)
  5. //之后
  6. const [,root] = path.split('/')
  7. console.log(root)

对象的解构赋值

解构不仅可以用于数组,还可以用于对象,

  1. let { foo, bar } = { foo: 'aaa', bar: 'bbb' };
  2. foo // "aaa"
  3. bar // "bbb"

对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。

  1. let { bar, foo } = { foo: 'aaa', bar: 'bbb' };
  2. foo // "aaa"
  3. bar // "bbb"
  4. let { baz } = { foo: 'aaa', bar: 'bbb' };
  5. baz // undefined

上面代码的第一个例子,等号左边的两个变量的次序,与等号右边两个同名属性的次序不一致,但是对取值完全没有影响。第二个例子的变量没有对应的同名属性,导致取不到值,最后等于undefined。相当于解构失败,变量的值等于undefined
其实,对象的解构赋值是下面形式的简写。

  1. let { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' };

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

  1. let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
  2. baz // "aaa"
  3. foo // error: foo is not defined

上面代码中,foo是匹配的模式,baz才是变量。真正被赋值的是变量baz,而不是模式foo。
等以后深入的使用过程中,可以更加理解解构赋值,看个例子

  1. let {log} = console
  2. log('foo')
  3. log('foo1')
  4. log('foo3')
  5. //foo
  6. //foo1
  7. //foo3
  8. //大大简化了代码的编写 整体体积也减少了很多
  9. 相当于:
  10. const {log:lo} = console //log:相当于模式 后面的lo才是变量
  11. lo('fooo')
  12. lo('foo1')
  13. lo('foo3')
  14. //foo
  15. //foo1
  16. //foo3

对象的解构也可以指定默认值,默认值生效的条件是,对象的属性值严格等于undefined。

  1. var {x = 3} = {};
  2. x // 3
  3. var {x, y = 5} = {x: 1};
  4. x // 1
  5. y // 5

字符串的解构赋值

字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。

  1. const [a, b, c, d, e] = 'hello';
  2. a // "h"
  3. b // "e"
  4. c // "l"
  5. d // "l"
  6. e // "o"

类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值。

  1. let {length : len} = 'hello';
  2. len // 5

函数参数的解构赋值

函数的参数也可以使用解构赋值。

  1. function add([x, y]) {
  2. console.log(x+y)
  3. }
  4. add([1,2])
  5. //3

另一个例子

  1. function move ({x=0, y=0}) {
  2. return [x, y]
  3. }
  4. move({x:3, y:8})

函数move的参数是一个对象,通过对这个对象进行解构,得到变量x和y的值。如果解构失败,x和y等于默认值。

一些用途

  1. 交换变量的值

    1. let a = 1
    2. let y = 2
    3. [x, y] = [y, x]
  2. 从函数返回多个值

函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便。

  1. / 返回一个数组
  2. function example() {
  3. return [1, 2, 3];
  4. }
  5. let [a, b, c] = example();
  6. // 返回一个对象
  7. function example() {
  8. return {
  9. foo: 1,
  10. bar: 2
  11. };
  12. }
  13. let { foo, bar } = example();
  1. 提取 JSON 数据 ```javascript let jsonData = { id: 42, status: “OK”, data: [867, 5309] };

let { id, status, data: number } = jsonData;

console.log(id, status, number); // 42, “OK”, [867, 5309]

  1. 上面代码可以快速提取 JSON 数据的值。<br />4.输入模块的指定方法
  2. ```javascript
  3. const { SourceMapConsumer, SourceNode } = require("source-map");

模板字符串

定义

模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。

基本使用

  1. // 普通字符串
  2. `In JavaScript '\n' is a line-feed.`
  3. // 多行字符串
  4. `In JavaScript this is
  5. not legal.`
  6. console.log(`string text line 1
  7. string text line 2`);
  8. // 字符串中嵌入变量
  9. let name = "Bob", time = "today";
  10. `Hello ${name}, how are you ${time}?`

大括号内部可以放入任意的 JavaScript 表达式,可以进行运算,以及引用对象属性。

  1. const {log} = console
  2. const name = 'tom'
  3. const msg = `hey,${name}---${Math.random()}------${1+2}`
  4. log(msg)
  5. //hey,tom---0.8797044709433715------3

模板字符串之中还能调用函数。

  1. function fn() {
  2. return "Hello World";
  3. }
  4. `foo ${fn()} bar`
  5. // foo Hello World bar

标签模板

它可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。这被称为“标签模板”功能(tagged template),就是对字符串进行加工。

  1. const {log} = console
  2. const str = myTagFunc`hey,${name}is a ${gender}.`
  3. function myTagFunc(string, name, gender) {
  4. const sex = gender? 'man' : 'woman';
  5. return string[0]+ name + string[1] + sex + string[2]
  6. }
  7. log(str)
  8. //hey,tom is a man.

字符串的扩展方法

传统上,JavaScript 只有indexOf方法,可以用来确定一个字符串是否包含在另一个字符串中。ES6 又提供了三种新方法。

  1. includes():返回布尔值,表示是否找到了参数字符串。
  2. startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
  3. endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。

    1. //字符串的查找方法
    2. const message = 'Error:foo is not defined.'
    3. const {log} = console
    4. log(
    5. message.startsWith('Error'), //是否是以 Error开头
    6. message.endsWith('.'), //是否是以 .结尾
    7. message.includes('foo') //字符串中间是否包含 foo
    8. )
  4. repeat()方法,返回一个字符串,表示将原字符串重复n次

    1. 'x'.repeat(5)
    2. //xxxxx
  5. padStart(),padEnd()

如果某个字符串不够指定长度,会在头部或尾部补全。padStart()用于头部补全,padEnd()用于尾部补全。

  1. 'x'.padStart(5, 'ab') // 'ababx'
  2. 'x'.padStart(4, 'ab') // 'abax'
  3. 'x'.padEnd(5, 'ab') // 'xabab'
  4. 'x'.padEnd(4, 'ab') // 'xaba'

padStart()和padEnd()一共接受两个参数,第一个参数是字符串补全生效的最大长度,第二个参数是用来补全的字符串。

  1. trimStart(),trimEnd()

它们的行为与trim()一致,trimStart()消除字符串头部的空格,trimEnd()消除尾部的空格。它们返回的都是新字符串,不会修改原始字符串。

  1. replaceAll()

字符串的实例方法replace()只能替换第一个匹配

  1. 'aabbcc'.replace('b', '_')
  2. // 'aa_bcc

上面例子中,replace()只将第一个b替换成了下划线。
如果要替换所有的匹配,不得不使用正则表达式的g修饰符。

  1. 'aabbcc'.replace(/b/g, '_')
  2. // 'aa__cc'

正则表达式毕竟不是那么方便和直观, 引入了replaceAll()方法,可以一次性替换所有匹配。

  1. 'aabbcc'.replaceAll('b', '_')
  2. // 'aa__cc'

剩余参数/展开数组

剩余参数

…只可出现在最后并且只有一个

  1. const {log} = console
  2. //...只可出现在最后并且只有一个
  3. function foo(first, ...args) {
  4. // log(Array.from(arguments))
  5. log(args)
  6. }
  7. foo(1,2,3,4)

展开数组

  1. //...展开数组
  2. const arr = ['foo', 'bar', 'baz']
  3. console.log.apply(console, arr)
  4. // 同
  5. console.log(...arr)

箭头函数

基本用法

让函数的书写变得很简洁,可读性很好。
我们看一个简单的例子:

  1. function inc (number) {
  2. return number + 1
  3. }
  4. const inc = (n,m)=> {
  5. return n + 1
  6. }

还有循环中能简化很多代码

  1. const arr = [1,2,3,4,5]
  2. const newArr = arr.filter(function(i) {
  3. return i % 2
  4. })
  5. const newArr = arr.filter(i => i % 2 )
  6. //[ 1, 3, 5 ]

我们注意一点 如果箭头函数的圆括号后面你省略了{},是默认return 内容得。加上{} 你就得写上return关键词。

箭头函数与this

前言this

在“use strict”严格模式下,没有直接的挂载者(或称调用者)的函数中this是指向window,这是约定俗成的。在“use strict”严格模式下,没有直接的挂载者的话,this默认为undefined。下面讨论的例子都是在非严格模式下。
this具有运行期绑定的特性,是基于函数的执行环境绑定的,在全局函数中,this指向的是window,当函数被作为某个对象调用时,this就等于那个对象。

箭头函数的this

箭头函数的this定义:箭头函数的this是在定义函数时绑定的,不是在执行过程中绑定的。简单的说,函数在定义时,this就继承了定义函数的对象。
例子:

  1. var name = '333'
  2. var person = {
  3. name: 'tom',
  4. sayHi:function() {
  5. return function() {
  6. return console.log(this.name)
  7. }
  8. }
  9. }
  10. person.sayHi()()
  11. //333
  12. //--------------------------------------
  13. var name = '333'
  14. var person = {
  15. name: 'tom',
  16. sayHi:function() {
  17. setTimeout(function() {
  18. console.log(this.name)
  19. },1000)
  20. }
  21. }
  22. person.sayHi()
  23. //333

原因是,匿名函数的执行环境是全局的。this只在函数内部起作用。此时的this.name在匿名函数中找不到,所以就从全局中找,找到后打印出来。

原来我们常常通过闭包的特性缓存上下文this

  1. // 箭头函数 与 this
  2. var name = '333'
  3. var person = {
  4. name: 'tom',
  5. syaHIAsync:function() {
  6. const _this = this //通过闭包的特性
  7. setTimeout(function() {
  8. console.log(_this.name)
  9. },1000)
  10. }
  11. }
  12. person.syaHIAsync()
  13. //tom

现在可以用箭头函数

  1. // 箭头函数 与 this
  2. var name = '333'
  3. var person = {
  4. name: 'tom',
  5. syaHIAsync:function() {
  6. setTimeout(()=> {
  7. console.log(this.name)
  8. },1000)
  9. }
  10. }
  11. person.syaHIAsync()
  12. //tom

箭头函数的this始终和它最近的外层相同的指向,这样就是箭头函数中的this。

对象字面量

ES6 新推出的新简写法,用来初始化对象并向对象添加方法。
我们经常这样写

  1. // 年货糖果按斤计费
  2. let type = 'candy';
  3. let weight = '5';
  4. let price = '8';
  5. const goods = {
  6. type: type,
  7. weight: weight,
  8. price: price
  9. }

键值对出现了重复,ES2015 中,如果属性名和和所分配的变量名一样,就可以从对象属性中删掉这些重复的变量名称。

  1. let type = 'candy';
  2. let weight = '5';
  3. let price = '8';
  4. const goods = {
  5. type,
  6. weight,
  7. price, // 如果属性名和字面量一致的话 可以省略
  8. total: function(){
  9. // ...
  10. }
  11. }

计算属性名

在之前ES5中,如果属性名是个变量或者需要动态计算,则只能通过 对象.[变量名] 的方式去访问。
而在字面量中是无法使用的。

  1. const p = {
  2. name : '李四',
  3. age : 20
  4. }
  5. const attName = 'name';
  6. console.log(p[attName]) //这里 attName表示的是一个变量名。
  7. //p[attName] === p.name
  8. //-----------------------------------------------
  9. const p = {
  10. attName : '李四', // 这里的attName是属性名,相当于各级p定义了属性名叫 attName的属性。
  11. age : 20
  12. }
  13. console.log(p[attName]) // undefined

在ES2015中,计算属性名 [] 里面可以使用任意的表达式 作为这个属性的属性名称。
上述例子中就可以写为:

  1. const p = {
  2. [attName] : '李四', // 引用了变量attName。相当于添加了一个属性名为name的属性
  3. age : 20
  4. }
  5. console.log(p[attName]) // 李四

计算属性名 [] 里面可以使用任意的表达式

  1. // 对象字面量
  2. const bar = '123'
  3. const obj = {
  4. foo: 12,
  5. // bar: bar, //1
  6. bar, //2
  7. }
  8. // 如果属性名和字面量一致的话 可以省略 如同 1 2 行一样
  9. //计算属性名 [] 里面可以使用任意的表达式 作为这个属性的属性名称
  10. obj[Math.random()] = 124
  11. obj[1+2] = 3
  12. console.log(obj)
  13. //{ '3': 3, foo: 12, bar: '123', '0.40770536872805674': 124 }

对象新增方法

Object.assign()

基本用法:
Object.assign()方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。
第一个参数是目标参数,把剩下的参数都合并到目标参数里,可以接收多个参数 同名会被覆盖。

  1. //object.assign 方法
  2. const source1 = {
  3. a:function() {
  4. return true
  5. },
  6. b:5,
  7. c:4
  8. }
  9. const source2 = {
  10. d:10,
  11. e:11
  12. }
  13. const target = {
  14. a: function() {
  15. return false
  16. },
  17. f: 222
  18. }
  19. const result = Object.assign(target, source1, source2)
  20. console.log(target.a()) //后面的属性会覆盖前面的属性
  21. console.log(result === target)
  22. //true
  23. //true

注意:非对象参数出现在源对象的位置(即非首参数),首先,这些参数都会转成对象,如果无法转成对象,就会跳过。这意味着,如果undefined和null不在首参数,就不会报错。在首参的话就会报错。

浅拷贝
Object.assign()方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。

  1. const obj1 = {a: {b: 1}};
  2. const obj2 = Object.assign({}, obj1);
  3. obj1.a.b = 2;
  4. obj2.a.b // 2

Object.is()

ES5 比较两个值是否相等,只有两个运算符:相等运算符(==)和严格相等运算符(===)。它们都有缺点,前者会自动转换数据类型,后者的NaN不等于自身,以及+0等于-0。
Object.is就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致,不同之处只有两个:一是+0不等于-0,二是NaN等于自身。

  1. +0 === -0 //true
  2. NaN === NaN // false
  3. Object.is(+0, -0) // false
  4. Object.is(NaN, NaN) // true

Object.keys()

Object.values()

Object.entries()

Object.fromEntries()

proxy

前言:ES2015之前我们要监视对象属性的读写可以用Object.defineProperty(),但是有一些局限性,而ES2015提供了更好用的更为强大的方法proxy。下面我们来对比下。
1.object.definedProperty 只能监视到属性的读写,proxy 能监听到很多对象的操作。拿对象的删除举例

  1. const person = {
  2. name: 'tom',
  3. age: 12
  4. }
  5. const personProxy = new Proxy (person, {
  6. deleteProperty(target, property) { //目标对象、属性名
  7. console.log(`delete: ${property}`)
  8. delete target[property]
  9. }
  10. })
  11. delete personProxy.age
  12. console.log(person)
  13. // delete: age
  14. //{ name: 'tom' }

get方法用于拦截某个属性的读取操作,可以接受三个参数,依次为目标对象、属性名和 proxy 实例本身(严格地说,是操作行为所针对的对象),其中最后一个参数可选。

2.object.definedProperty 是重写数组方法 像push等,而proxy是拦截对数组的操作,对数组的监视

  1. const arr = []
  2. const arrProxy = new Proxy(arr, {
  3. set(target, property, value) { //目标对象、属性名、属性值
  4. console.log('set', property, value)
  5. target[property] = value
  6. return true
  7. }
  8. })
  9. arrProxy.push(100)

set方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身,其中最后一个参数可选。

Proxy的拦截操作

下面是 Proxy 支持的拦截操作一览,一共 13 种。
get(target, propKey, receiver):拦截对象属性的读取,比如proxy.foo和proxy[‘foo’]。
set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = v或proxy[‘foo’] = v,返回一个布尔值。
has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。
deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。
ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for…in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(…args)、proxy.call(object, …args)、proxy.apply(…)。
construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(…args)。

小总结

还是在工作中多用才能记住,相比较于Object.definedProperty(),proxy是以非侵入的方式监管了对对象的读写。

实例

get应用实例

  1. const person = {
  2. name: 'tom'
  3. }
  4. const personProxy = new Proxy(person, {
  5. get:function(target, propKey) {
  6. if(propKey in target) {
  7. return target[propKey]
  8. }else {
  9. throw new ReferenceError("Prop name \"" + propKey + "\" does not exist.");
  10. }
  11. }
  12. })
  13. proxy.name // "tom"
  14. proxy.age // 抛出一个错误

set 应用实例
假定Person对象有一个age属性,该属性应该是一个不大于 200 的整数,那么可以使用Proxy保证age的属性值符合要求。

  1. let validator = {
  2. set: function(obj, prop, value) {
  3. if (prop === 'age') {
  4. if (!Number.isInteger(value)) {
  5. throw new TypeError('The age is not an integer');
  6. }
  7. if (value > 200) {
  8. throw new RangeError('The age seems invalid');
  9. }
  10. }
  11. // 对于满足条件的 age 属性以及其他属性,直接保存
  12. obj[prop] = value;
  13. }
  14. };
  15. let person = new Proxy({}, validator);
  16. person.age = 100;
  17. person.age // 100
  18. person.age = 'young' // 报错
  19. person.age = 300 // 报错

其他的不再一一举例了。

Class

基本用法

JavaScript 语言中,生成实例对象的传统方法是通过构造函数。下面是一个例子。

  1. //旧的定义原型对象
  2. function Person(name) {
  3. this.name = name
  4. }
  5. Person.prototype.say = function() {
  6. console.log(`hi, my name is ${this.name}`)
  7. }
  8. const person1 = new Person('tom')
  9. person1.say()

在ES6 es2015中引入了 Class(类)这个概念,作为对象的模板 ,通过class关键字,可以定义类。
上面代码改写

  1. class Person {
  2. // 构造函数
  3. constructor(name) {
  4. this.name = name
  5. }
  6. say() {
  7. console.log(`hi, my name is ${this.name}`)
  8. }
  9. }
  10. const person1 = new Person('tom')
  11. person1.say()

可以看到里面有一个constructor方法,这就是构造方法,而this关键字则代表实例对象。也就是说,ES5 的构造函数Person,对应 ES6 的Person类的构造方法。

class的继承

class 可以通过extends关键字实现继承,这比es5的通过修改原型链实现继承,要清晰方便。下面我们来看个例子:

  1. // extends 继承
  2. class Person {
  3. // 构造函数
  4. constructor(name) {
  5. this.name = name
  6. }
  7. say() { //实例方法
  8. console.log(`hi, my name is ${this.name}`)
  9. }
  10. static create(name) { //静态方法 注意静态方法是通过构造函数直接调用的this指向没有改变
  11. return new Person(name)
  12. }
  13. }
  14. class Student extends Person {
  15. constructor(name, number) {
  16. super(name) //super永远指向父类,调用父类的constructor(name)
  17. this.number = number
  18. }
  19. hello() {
  20. super.say()
  21. console.log(`my school number is ${this.number}`)
  22. }
  23. }
  24. const student1 = new Student('rose', '111')
  25. student1.hello()
  26. //hi, my name is rose
  27. //my school number is 111

上面代码中,constructor方法和hello方法之中,都出现了super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。

  • 子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,如果不调用super方法,子类就得不到this对象
  • 在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。
  • 父类的静态方法,也会被子类继承。 ```javascript class A { static hello() { console.log(‘hello world’); } }

class B extends A { }

B.hello() // hello world

  1. <a name="cHhDt"></a>
  2. ### Reflect
  3. <a name="eCJTy"></a>
  4. #### 概述
  5. Reflect对象与Proxy对象一样,也是 ES6 为了操作对象而提供的新 API。Reflect对象的设计目的有这样几个。
  6. 1. **将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上**。现阶段,某些方法同时在Object和Reflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法。
  7. 2. **修改某些Object方法的返回结果,让其变得更合理**。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false。
  8. ```javascript
  9. // 老写法
  10. try {
  11. Object.defineProperty(target, property, attributes);
  12. // success
  13. } catch (e) {
  14. // failure
  15. }
  16. // 新写法
  17. if (Reflect.defineProperty(target, property, attributes)) {
  18. // success
  19. } else {
  20. // failure
  21. }
  1. 让Object操作都变成函数行为。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)让它们变成了函数行为。 ```javascript // 老写法 ‘assign’ in Object // true

// 新写法 Reflect.has(Object, ‘assign’) // true

// 老写法 const person = { name : ‘tom’ } delete person.name // 新写法 reflect.deleteProperty(person, name) //—————————————————————————— const {log} = console const obj = { name: ‘tom’, age: 12 } const reflect = Reflect.get(obj, ‘name’) const reflect2 = Reflect.deleteProperty(obj, ‘age’) const reflect3 = Reflect.has(obj, ‘name’) log(reflect,reflect2,reflect3)

  1. 4. **Reflect对象的方法与Proxy对象的方法一一对应**,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。
  2. ```javascript
  3. Proxy(target, {
  4. set:function(target, propKey, value) {
  5. var success = Reflect.set(target, propKey, value);
  6. if(success) {
  7. console.log('property ' + name + ' on ' + target + ' set to ' + value);
  8. }else {
  9. return success;
  10. }
  11. }
  12. })

上面代码中,Proxy方法拦截target对象的属性赋值行为。它采用Reflect.set方法将值赋值给对象的属性,确保完成原有的行为,然后再部署额外的功能。

下面是另一个例子

  1. var loggedObj = new Proxy(obj, {
  2. get(target, name) {
  3. console.log('get', target, name);
  4. return Reflect.get(target, name);
  5. },
  6. deleteProperty(target, name) {
  7. console.log('delete' + name);
  8. return Reflect.deleteProperty(target, name);
  9. },
  10. has(target, name) {
  11. console.log('has' + name);
  12. return Reflect.has(target, name);
  13. }
  14. });

上面代码中,每一个Proxy对象的拦截操作(get、delete、has),内部都调用对应的Reflect方法,保证原生行为能够正常执行。添加的工作,就是将每一个操作输出一行日志。

Set/Map数据结构

set

基本用法ES2015 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。
Set本身是一个构造函数,用来生成 Set 数据结构。

  1. const s = new Set();
  2. [2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));
  3. // Set(4) {2, 3, 5, 4}
  4. for (let i of s) {
  5. console.log(i);
  6. }
  7. // 2 3 5 4

Set函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。
add 会返回集合方法本身 所以可以链式调用, 添加了存在的值 就会被忽略。

  1. const s = new Set()
  2. s.add(1).add(2).add(3).add(1)
  3. //Set { 1, 2, 3 }

用来去重数组

  1. const arr = [1,2,3,4,5,5,3]
  2. const newArr = [...new Set(arr)]
  3. console.log(newArr)

去重字符串

  1. [...new Set('abckdeedsww')].join('')

Set 内部判断两个值是否不同,使用的算法叫做“Same-value-zero equality”,它类似于精确相等运算符(===),主要的区别是向 Set 加入值时认为NaN等于自身,而精确相等运算符认为NaN不等于自身。
另外,两个对象总是不相等的。

set的属性和方法

  1. s.add(1).add(2).add(2);
  2. // 注意2被加入了两次
  3. s.size // 2
  4. s.has(1) // true
  5. s.has(2) // true
  6. s.has(3) // false
  7. s.delete(2);
  8. s.has(2) // false

Array.from方法可以将 Set 结构转为数组。这就又提供了一个数组去重的方法

  1. const newArr = Array.from(new Set([1,2,3,4,5,6,3,5]))
  2. console.log(newArr)

set遍历操作

Set 结构的键名就是键值(两者是同一个值)
keys(),values(),entries()

  1. let set = new Set(['red', 'green', 'blue']);
  2. for (let item of set.keys()) {
  3. console.log(item);
  4. }
  5. // red
  6. // green
  7. // blue
  8. for (let item of set.values()) {
  9. console.log(item);
  10. }
  11. // red
  12. // green
  13. // blue
  14. for (let item of set.entries()) {
  15. console.log(item);
  16. }
  17. // ["red", "red"]
  18. // ["green", "green"]
  19. // ["blue", "blue"]
  20. //其实可以省略values直接遍历,Set 结构的实例默认可遍历
  21. for (let item of set) {
  22. console.log(item);
  23. }
  24. // ["red", "red"]
  25. // ["green", "green"]
  26. // ["blue", "blue"]

Set 可以很容易地实现并集(Union)、交集(Intersect)和差集(Difference)。

  1. let a = new Set([1, 2, 3]);
  2. let b = new Set([4, 3, 2]);
  3. //并集
  4. let union = new Set([...a],[...b])
  5. // Set {1, 2, 3, 4}
  6. //交集
  7. let intersect = new Set([...a].filter(x=> b.has(x)))
  8. // set {2, 3}
  9. //差集
  10. // (a 相对于 b 的)差集
  11. let difference = new Set([...a].filter(x => !b.has(x)));
  12. // Set {1}

map

JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。
ES2015 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。

  1. //对象
  2. const obj = {}
  3. obj[true] = 'value'
  4. obj[123] = 'value2'
  5. obj[{a:1}] = 'value3'
  6. const {log} = console
  7. log(obj)
  8. //{ '123': 'value2', true: 'value', '[object Object]': 'value3' }
  9. //可以看出 对象是调用键值的tostring.转化为string值
  10. //---------------------------------------------------------------
  11. const m = new Map()
  12. const tom = {name: 'tom'}
  13. m.set(tom, 100)
  14. m.set(Math.random, '随机数')
  15. log(m)
  16. // Map { { name: 'tom' } => 100, 0.4260789016377904 => '随机数' }
  17. log(m.get(tom))
  18. // 100
  19. for (const [key, value] of m) {
  20. console.log(key, value)
  21. }
  22. //{ name: 'tom' } 100
  23. //0.5114862235627582 随机数

其他具体的就不在这里赘述了。

Symbol

比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法(mixin 模式),新方法的名字就有可能与现有方法产生冲突。ES2015就提供了Symbol,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。

  1. let s = Symbol();
  2. console.log(s)

变量s就是一个独一无二的值。typeof运算符的结果,表明变量s是 Symbol 数据类型。

  1. Symbol 是一个原始类型的值,不能用new
  2. Symbol 值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。
  3. 可以接受一个字符串作为参数,为新创建的 Symbol 提供描述,用来显示在控制台或者作为字符串的时候使用,便于区分。 ```javascript const s = Symbol() console.log(typeof s) console.log( Symbol() === Symbol() ) //false //——————————————————-

console.log(Symbol(‘foo’)) console.log(Symbol(‘g’)) console.log(Symbol(‘tom’))

//Symbol(foo) //Symbol(g) //Symbol(tom)

//——————————————————-

let s1 = Symbol(‘foo’); let s2 = Symbol(‘foo’);

s1 === s2 // false

//——————————————————

//Symbol 值不能与其他类型的值进行运算,会报错 let sym = Symbol(‘My symbol’);

“your symbol is “ + sym // TypeError: can’t convert symbol to string

//——————————————————-

//Symbol 值可以显式转为字符串 let sym = Symbol(‘My symbol’);

String(sym) // ‘Symbol(My symbol)’ sym.toString() // ‘Symbol(My symbol)’

//———————————————————-

//Symbol 值也可以转为布尔值,但是不能转为数值 let sym = Symbol(); Boolean(sym) // true !sym // false

if (sym) { // … }

Number(sym) // TypeError sym + 2 // TypeError

  1. <a name="Um2g6"></a>
  2. #### description 描述
  3. ```javascript
  4. const sym = Symbol('foo');
  5. sym.description // "foo"

作为属性名Symbol

  1. let mySymbol = Symbol();
  2. // 第一种写法
  3. let a = {};
  4. a[mySymbol] = 'Hello!';
  5. // 第二种写法
  6. let a = {
  7. [mySymbol]: 'Hello!' //在对象的内部,使用 Symbol 值定义属性时,Symbol 值必须放在方括号之中。
  8. };
  9. // 第三种写法
  10. let a = {};
  11. Object.defineProperty(a, mySymbol, { value: 'Hello!' });
  12. // 以上写法都得到同样结果
  13. a[mySymbol] // "Hello!"
  14. //--------------------------------------------------------------
  15. const log = {};
  16. log.levels = {
  17. DEBUG: Symbol('debug'),
  18. INFO: Symbol('info'),
  19. WARN: Symbol('warn')
  20. };
  21. console.log(log.levels.DEBUG.description, 'debug message');
  22. console.log(log.levels.INFO.description, 'info message');
  23. //debug debug message
  24. //info info message

Symbol 值作为属性名时,该属性是公有属性不是私有属性,可以在类的外部访问。但是不会出现在 for…in 、 for…of 的循环中,也不会被 Object.keys() 、 Object.getOwnPropertyNames() 返回。如果要读取到一个对象的 Symbol 属性,可以通过 Object.getOwnPropertySymbols() 和 Reflect.ownKeys() 取到。

  1. const obj = {}
  2. let a = Symbol('a');
  3. let b = Symbol('b');
  4. obj[a] = 'hello'
  5. obj[b] = 'world'
  6. const objectSymbols = Object.getOwnPropertySymbols(obj)
  7. console.log(objectSymbols)
  8. //[ Symbol(a), Symbol(b) ]

同一个Symbol值

重新使用同一个 Symbol 值,Symbol.for()方法可以做到这一点

  1. let s1 = Symbol.for('foo');
  2. let s2 = Symbol.for('foo');
  3. s1 === s2 // true

Symbol.for()与Symbol()这两种写法,都会生成新的 Symbol。Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。

for …of

全新的遍历方式 for…of 遍历 可以遍历所有所有数据结构。
一个数据结构只要部署了Symbol.iterator属性,就被视为具有 iterator 接口,就可以用for…of循环遍历它的成员。也就是说,for…of循环内部调用的是数据结构的Symbol.iterator方法。
for…of循环可以使用的范围包括:

  1. 数组
  2. Set
  3. Map 结构
  4. 某些类似数组的对象(比如arguments对象、DOM NodeList 对象)、 Generator 对象
  5. 字符串。 ```javascript const arr = [1,2,3,4] for (const i of arr) { console.log(i) if(i>3) {
    1. break //可以用break随时终止循环 优于forEach
    } } //—————————————————————————————————— const arr = [‘a’,’b’,’c’, ‘d’] for(let i in arr) { console.log(i) } //0 1 2 3 4 for(let i of arr) { console.log(i) } //a b c d //JavaScript 原有的for…in循环,只能获得对象的键名,不能直接获取键值。ES6 提供for…of循环,允许遍历获得键值。

//for…of循环调用遍历器接口,数组的遍历器接口只返回具有数字索引的属性。这一点跟for…in循环也不一样。 let arr1 = [3, 5, 7]; arr1.foo = ‘hello’;

for (let i in arr1) { console.log(i); // “0”, “1”, “2”, “foo” }

for (let i of arr1) { console.log(i); // “3”, “5”, “7” } //———————————————————————————————————- // 可以遍历map set对象 const s = new Set([‘foo’,’bar’]) for (const item of s) { console.log(s) } // 跟数组差不多也是拿到的每一项

const m = new Map() m.set(‘foo’,’124’) m.set(‘foo1’,’4456’) for (const [value, key] of m) { console.log(value, key) }

  1. 下面我们具体说说iterator接口。
  2. <a name="rwbhy"></a>
  3. ### iterator 迭代器(遍历器)
  4. <a name="o5zQD"></a>
  5. #### 前言
  6. 原来表示“集合”的数据结构,数组(Array)和对象(Object),ES6 又添加了MapSet。这样就有了四种数据集合,用户还可以组合使用它们,定义自己的数据结构,比如数组的成员是MapMap的成员是对象。这样就需要一种统一的接口机制,来处理所有不同的数据结构。
  7. <a name="b9PEs"></a>
  8. #### 概念
  9. 遍历器(Iterator)就是这样一种机制。**它是一种接口,为各种不同的数据结构提供统一的访问机制**。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作。<br />其实Iterator 接口主要供for...of使用(**一个数据结构只要部署了Symbol.iterator属性,就被视为具有 iterator 接口,就可以用for...of循环遍历**)。<br />我们来看个例子
  10. ```javascript
  11. // iterator 迭代器
  12. const set = new Set(['foo','bar','baz'])
  13. console.log(set)
  14. const iterator = set[Symbol.iterator]()
  15. console.log(iterator.next())
  16. console.log(iterator.next())
  17. console.log(iterator.next())
  18. console.log(iterator.next())
  19. console.log(iterator.next())
  20. //Set { 'foo', 'bar', 'baz' }
  21. //{ value: 'foo', done: false }
  22. //{ value: 'bar', done: false }
  23. //{ value: 'baz', done: false }
  24. //{ value: undefined, done: true }
  25. //{ value: undefined, done: true }
  26. //所以我们能看出来Iterator 的遍历过程

从代码里我们能看出来Iterator的遍历过程

  1. 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
  2. 第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
  3. 第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
  4. 不断调用指针对象的next方法,直到它指向数据结构的结束位置。

返回一个包含value和done两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。

  1. //模拟实现Iterator
  2. function makeIterator(array) {
  3. var nextIndex = 0
  4. return function() {
  5. return nextIndex< array.length?
  6. {value: array[nextIndex++],done:false}:
  7. {value:undefined,done:true}
  8. }
  9. }
  10. var it makeIterator(['a', 'b'])
  11. it.next() // { value: "a", done: false }
  12. it.next() // { value: "b", done: false }
  13. it.next() // { value: undefined, done: true }

所以只要数据有Symbol.iterator这个属性就可以

  1. //现在是for of 无法循环普通对象 是因为它内部没有实现iterable 接口
  2. const obj = {}
  3. for( const itme of obj) {
  4. console.log(item)
  5. }
  6. // TypeError: obj is not iterable
  7. //假如我们内部实现iterable接口 那这个对象就可以被for of 循环
  8. const obj = {
  9. store:['foo','bar','baz'],
  10. [Symbol.iterator] : function() {
  11. let index = 0
  12. const self = this
  13. return { //从内部再返回一个迭代器
  14. next: function() {
  15. const result = {
  16. value: self.store[index],
  17. done: index>=self.store.length //表示迭代有没有结束
  18. }
  19. index ++
  20. return result
  21. }
  22. }
  23. }
  24. }
  25. for( const item of obj) {
  26. console.log(item)
  27. }

原生具备 Iterator 接口的数据结构如下。
-array
-map
-set
-map
-string
-TypedArray
-函数的arguments对象
-nodeList 对象

generator

基本概念:

Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。

特征:
  1. function关键字与函数名之间有一个星号
  2. 函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)

看一个例子

  1. //基本使用
  2. function* helloWorldGenerator() {
  3. yield 'hello';
  4. yield 'world';
  5. return 'ending';
  6. }
  7. var hw = helloWorldGenerator(); //调用
  8. //-------------------------------------------
  9. hw.next() //调用遍历器对象的next方法,使得指针移向下一个状态
  10. // { value: 'hello', done: false }
  11. hw.next()
  12. // { value: 'world', done: false }
  13. hw.next()
  14. // { value: 'ending', done: true }
  15. hw.next()
  16. // { value: undefined, done: true }
  1. 上面代码定义了一个 Generator 函数helloWorldGenerator。
  2. 调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是上一章介绍的遍历器对象(Iterator Object)
  3. 每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。或者说(yield表暂停执行,next恢复执行)
  4. 每次调用遍历器对象的next方法,返回的value和done。value是yield表达式后面那个表达式的值;done表示是否遍历结束。

感觉像是利用Iterator实现对代码的分段执行能暂停能再开始。

使用案例
  1. // generator 案例
  2. //案例1 : 实现一个发号器
  3. function *createIdMaker() {
  4. let id = 1;
  5. while(true) {
  6. yield id ++
  7. }
  8. }
  9. const idMaker = createIdMaker()
  10. console.log(idMaker.next().value)
  11. console.log(idMaker.next().value)
  12. console.log(idMaker.next().value)
  13. console.log(idMaker.next().value)
  14. console.log("---------------------------")
  15. //案例二 : 实现iterator 迭代器
  16. const todos = {
  17. left:['吃饭','睡觉','打豆豆'],
  18. learn: ['语文','数学','英语'],
  19. work: ['喝茶'],
  20. // 实现迭代器
  21. [Symbol.iterator]: function *() { //因为它本身就返回Iterator迭代器
  22. const arr = [].concat(this.left, this.learn, this.work)
  23. for (const item of arr) {
  24. yield item
  25. }
  26. }
  27. }
  28. for (const item of todos) {
  29. console.log(item)
  30. }
  31. // 1
  32. // 2
  33. // 3
  34. // 4
  35. // ---------------------------
  36. // 吃饭
  37. // 睡觉
  38. // 打豆豆
  39. // 语文
  40. // 数学
  41. // 英语
  42. // 喝茶

再再具体的功能再查阅相关文档的使用吧。
promise已有相关文档介绍,下面介绍下async函数

async函数

含义

ES2017 标准引入了 async 函数,使得异步操作变得更加方便。
async 函数是什么?一句话,它就是 Generator 函数的语法糖。
看个例子:

  1. const fs = require('fs');
  2. // 依次读取两个文件
  3. const readFile = function (fileName) {
  4. return new Promise(function (resolve, reject) {
  5. fs.readFile(fileName, function(error, data) {
  6. if (error) return reject(error);
  7. resolve(data);
  8. });
  9. });
  10. };
  11. //generator写法
  12. const gen = function* () {
  13. const f1 = yield readFile('/etc/fstab');
  14. const f2 = yield readFile('/etc/shells');
  15. console.log(f1.toString());
  16. console.log(f2.toString());
  17. };
  18. //---------------------------------------------------------
  19. //async写法
  20. const asyncGen = async function() {
  21. const f1 = await readFile('/etc/fastab')
  22. const f2 = await readFile('etc/shells')
  23. console.log(f1.toString());
  24. console.log(f2.toString());
  25. }

一比较就会发现,async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await。
async对generator改进具体是:

  • 内置执行器。

Generator 函数的执行必须靠执行器执行得用next() 方法,而async自带执行器,执行就一行就可以

  1. asyncGen()
  • 更好的语义。

async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。

  • 更广的适用性

yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值

  • 返回值是 Promise

async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。
进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。

基本用法

指定多少毫秒后输出一个值

  1. function timeout(ms) {
  2. return new Promise((resolve)=> {
  3. setTimeout(resolve, ms)
  4. })
  5. }
  6. async function asyncPrint(value, ms) {
  7. await timeout(ms)
  8. console.log(value)
  9. }
  10. asyncPrint('hello world', 1000)

上面代码指定 1000 毫秒以后,输出hello world。

在我们实际使用上,因为await后面的promise对象,有可能失败,最好把await放到try..catch里

  1. function timeout(ms) {
  2. return new Promise((resolve)=> {
  3. setTimeout(resolve, ms)
  4. })
  5. }
  6. async function asyncPrint(value, ms) {
  7. try {
  8. await timeout(ms)
  9. console.log(value)
  10. }catch(err) {
  11. console.log(err)
  12. }
  13. }
  14. asyncPrint('hello world', 1000)

更多详细的使用具体使用的时候再看下。

实现异步的方式 generator promise async,都说async是解决异步的终极方案,可以多多使用。加油下一章我们介绍ES2016和ES2017新增的东西。