ECMAScript 6 (二)

1. String扩展

模板字符串

在 ES6 之前对字符串的处理是相当的麻烦,看如下场景:

1. 字符串很长要换行

字符串很长包括几种情形一个是开发时输入的文本内容,一个是接口数据返回的文本内容。如果对换行符处理不当,就会带来异常。

2. 字符串中有变量或者表达式

如果字符串不是静态内容,往往是需要加载变量或者表达式,这个也是很常见的需求。之前的做法是字符串拼接:

  var a = 20
  var b = 10
  var c = 'JavaScript'
  var str = 'My age is ' + (a + b) + ' and I love ' + c
  console.log(str)

如果字符串有大量的变量和表达式,这个拼接简直是噩梦。

3. 字符串中有逻辑运算

我们通常写代码都是有逻辑运算的,对于字符串也是一样,它包含的内容不是静态的,通常是根据一定的规则在动态变化。

  var retailPrice = 20
  var wholesalePrice = 16
  var type = 'retail'

  var showTxt = ''

  if (type === 'retail') {
      showTxt += '您此次的购买单价是:' + retailPrice
  } else {
      showTxt += '您此次的批发价是:' + wholesalePrice
  }

看到这样的代码一定会感到很熟悉,通常大家的做法是使用上述的字符串拼接+逻辑判断,或者采用字符串模板类库来操作。

从 ES6 开始可以用模板字符串定义字符串来解决拼接问题了。

`string text` 

`string text line 1
 string text line 2`

`string text ${expression} string text`

在这里你可以任意插入变量或者表达式,只要用 ${}包起来就好。

注意

这里的符号是反引号,也就是数字键 1 左边的键,不是单引号或者双引号

这样就可以轻松解决字符串包含变量或者表达式的问题了,对于多行的字符串,之前是这样处理

console.log('string text line 1\n' +
    'string text line 2')

现在可以这样做了

console.log(`string text line 1
string text line 2`)

模板字符串相当于加强版的字符串,除了作为普通字符串,还可以用来定义多行字符串,还可以在字符串中加入变量和表达式。完全不需要 \n 来参与。

扩展方法

1.子串的识别

ES6 之前判断字符串是否包含子串,用 indexOf 方法,ES6 新增了子串的识别方法。

  • includes():返回布尔值,判断是否找到参数字符串。
  • startsWith():返回布尔值,判断参数字符串是否在原字符串的头部。
  • endsWith():返回布尔值,判断参数字符串是否在原字符串的尾部。

以上三个方法都可以接受两个参数,需要搜索的字符串,和可选的搜索起始位置索引。

let string = "apple,banana,orange"; 

string.includes("banana");    

string.startsWith("apple");    

string.endsWith("apple");      

string.startsWith("banana",6);  //索引为6的位置起,判断banana是否在头部

2.字符串重复

repeat()方法返回一个新字符串,参数为需要重复的次数,表示将原字符串重复n次。

const str = 'ES6'

const newStr = str.repeat(3)

console.log(newStr)

如果参数是小数,向下取整

console.log("Hello,".repeat(3.2));  // "Hello,Hello,Hello,"

如果参数是 0 至 -1 之间的小数,会进行取整运算,0 至 -1 之间的小数取整得到 -0 ,等同于 repeat 零次

console.log("Hello,".repeat(-0.5));  // "" 

如果参数是负数,会报错:

console.log("Hello,".repeat(-1));  
// RangeError: Invalid count value

3.字符串补全

  • padStart:返回新的字符串,表示用参数字符串从头部(左侧)补全原字符串。
  • padEnd:返回新的字符串,表示用参数字符串从尾部(右侧)补全原字符串。

以上两个方法接受两个参数,第一个参数是指定生成的字符串的最小长度,第二个参数是用来补全的字符串。如果没有指定第二个参数,默认用空格填充。

console.log("h".padStart(5,"o")); 
console.log("h".padEnd(5,"o"));    
console.log("h".padStart(5));      

如果指定的长度小于或者等于原字符串的长度,则返回原字符串:

console.log("hello".padStart(5,"A"));  // "hello"

如果原字符串加上补全字符串长度大于指定长度,则截去超出位数的补全字符串:

console.log("hello".padEnd(10,",world!"));  // "hello,worl"

常用于补全位数:

console.log("123".padStart(10,"0"));  // "0000000123"

2. ES6对象(Object)

属性简洁表示法

在 ES6 之前 Object 的属性必须是 key-value 形式,如下:

  let name = '张三'
  let age = 24
  let obj = {
      name: name,
      age: age,
      study: function() {
          console.log(this.name + '正在学习')
      }
  }

在 ES6 之后允许对象的属性直接写变量,这时候属性名是变量名,属性值是变量值:

  let name = '张三'
  let age = 24
  let obj = {
      name,
      age,
  }
属性名表达式

在 ES6 可以直接用变量或者表达式来定义Object的 key属性名,但是一定要将表达式放在方括号内。

  let s = 'school'
  let obj = {
      foo: 'bar',
      [s]: 'sicnu'
  }
  
  const obj = {
 	  ["he"+"llo"](){
   		  return "Hi";
  	  }
  }
obj.hello();  //"Hi"

注意:

属性的简洁表示法和属性名表达式不能同时使用,否则会报错。

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

    基本语法:

Object.assign(target, …sources),从语法上可以看出源对象的个数是不限制的(零个或多个),如果是零个直接返回目的对象

参数含义必选
target目标对象Y
sources源对象N

如果是多个相同属性的会被后边的源对象的属相覆盖。

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

const returnedTarget = Object.assign(target, source)

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

如果没有源对象:

let s = Object.assign({
    a: 1
})
// {a: 1}

如果目的对象不是对象,则会自动转换为对象:

let t = Object.assign(2)
// Number {2}
let s = Object.assign(2, {
    a: 2
})
// Number {2, a: 2}

如果对象属性具有多层嵌套,这时使用Object.assign()合并对象会怎么样呢?

let target = {
    a: {
        b: {
            c: 1
        },
        e: 4,
        f: 5,
        g: 6
    }
}
let source = {
    a: {
        b: {
            c: 1
        },
        e: 2,
        f: 3
    }
}
Object.assign(target, source);
console.log(target);

我们惊奇的发现, g 属性消失了…

注意

Object.assign()对于引用数据类型属于浅拷贝。

TIP

对象的浅拷贝:浅拷贝是对象共用的一个内存地址,对象的变化相互印象。
对象的深拷贝:简单理解深拷贝是将对象放到新的内存中,两个对象的改变不会相互影响。

对象的遍历方式

如何能够遍历出对象中每个key和value的值呢?

let obj = {
    name: 'zhangsan',
    age: 24,
    school: 'sicnu'
}

for...in的作用是循环遍历对象自身的和继承的可枚举属性。

for (let key in obj) {
    console.log(key, obj[key])
}

Object.keys():用于返回对象所有key组成的数组。

Object.keys(obj).forEach(key => {
    console.log(key, obj[key])
})

Object.getOwnPropertyNames():用于返回对象所有key组成的数组。

Object.getOwnPropertyNames(obj).forEach(key => {
    console.log(key, obj[key])
})

Reflect.ownKeys():用于返回对象所有key组成的数组。

Reflect.ownKeys(obj).forEach(key => {
    console.log(key, obj[key])
})

3. Symbol

ES6 引入了一种新的原始数据类型 Symbol ,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。

Symbol 值通过Symbol函数生成。这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。

声明方式
let s = Symbol()

typeof s
// "symbol"

变量s就是一个独一无二的值。typeof的结果说明s是 Symbol 数据类型。

既然是独一无二的,那么两个Symbol()就一定是不相等的:

let s1 = Symbol()
let s2 = Symbol()
console.log(s1)
console.log(s2)
console.log(s1 === s2) // false

注意

Symbol函数前不能使用new命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象。也就是说,由于 Symbol 值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。

Symbol函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。

let s1 = Symbol('foo')
let s2 = Symbol('foo')
console.log(s1)
console.log(s2)
console.log(s1 === s2) // false
Symbol.for()

Symbol.for() 接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建一个以该字符串为名称的 Symbol 值,并将其注册到全局。

let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');
console.log(s1 === s2) // true

注意

Symbol.for()与Symbol()这两种写法,都会生成新的 Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。

作为属性名

由于每一个 Symbol 值都是不相等的,这意味着 Symbol 值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖。

比如在一个班级中,可能会有同学名字相同的情况,这时候使用对象来描述学生信息的时候,如果直接使用学生姓名作为key会有有问题。

const grade = {
    张三: {
        address: 'xxx',
        tel: '111'
    },
    李四: {
        address: 'yyy',
        tel: '222'
    },
    李四: {
        address: 'zzz',
        tel: '333'
    },
}
console.log(grade)
// 只会保留最后一个李四

如果使用Symbol,同名的学生信息就不会被覆盖:

const stu1 = Symbol('李四')
const stu2 = Symbol('李四')
const grade = {
    [stu1]: {
        address: 'yyy',
        tel: '222'
    },
    [stu2]: {
        address: 'zzz',
        tel: '333'
    },
}
console.log(grade)
console.log(grade[stu1])
console.log(grade[stu2])
属性遍历
const symbol = Symbol('aaa');
const obj = {
	a:'apple',
	b:'banana',
    [symbol]:'sss'
}
for(let item in obj){
	console.log(item);
}

Symbol 作为属性名,该属性不会出现在for...in循环中,也不会被Object.keys()Object.getOwnPropertyNames()返回。但是,它也不是私有属性,有一个Object.getOwnPropertySymbols方法,可以获取指定对象的所有 Symbol 属性名。

const a = Symbol('a');
const b = Symbol('b');

let obj = {
    [a]:'Hello',
    [b]:'world'
}

const objectSymbols = Object.getOwnPropertySymbols(obj);

4. Number

二进制与八进制

请大家思考在JS中如何把十进制和二进制如何相互转化?

十进制转换为二进制toString()方法可把一个 Number 对象转换为一个字符串,并返回结果。

语法:

NumberObject.toString(radix);

其中,radix为可选。规定表示数字的基数,使 2 ~ 36 之间的整数。若省略该参数,则使用基数 10。

const a = 5 // 101
console.log(a.toString(2))

二进制转换为十进制parseInt() 函数可解析一个字符串,并返回一个整数。

语法:

parseInt(string, radix);

其中,string为必需。要被解析的字符串。radix为可选。表示要解析的数字的基数。该值介于 2 ~ 36 之间。如果省略该参数或其值为 0,则数字将以 10 为基础来解析。

const b = 101
console.log(parseInt(b, 2))

ES6 提供了二进制和八进制数值的新的写法,分别用前缀0b(或0B)和0o(或0O)表示。

const a = 0B0101
console.log(a)

const b = 0O777
console.log(b)

如果要将0b0o前缀的字符串数值转为十进制,要使用Number方法。

Number('0b111')  // 7
Number('0o10')  // 8
新增方法
  • Number.isFinite()

用来检查一个数值是否为有限的(finite),即不是Infinity。Number.isFinate 没有隐式的 Number() 类型转换,所有非数值都返回 false。

Number.isFinite(15) // true
Number.isFinite(0.8) // true
Number.isFinite('foo') // false
Number.isFinite('15') // false
Number.isFinite(true) // false
  • Number.isNaN()

    说明

    NaNNot a Number,非数)是计算机科学中数值数据类型的一类值,表示未定义或不可表示的值。,说明某些算术运算(如求负数的平方根)的结果不是数字。方法 parseInt() 和 parseFloat() 在不能解析指定的字符串时就返回这个值。对于一些常规情况下返回有效数字的函数,也可以采用这种方法,用 Number.NaN 说明它的错误情况。

    在全局的 isNaN() 中,以下皆返回 true,因为在判断前会将非数值向数值转换 ,而 Number.isNaN() 不存在隐式的 Number() 类型转换,非 NaN 全部返回 false。因此,不能与 Number.NaN 比较来检测一个值是不是数字,而只能调用 isNaN() 来比较。

    NaN 与其他数值进行比较的结果总是不相等的,包括它自身在内。

用来检查一个值是否为NaN。

Number.isNaN(NaN) // true
Number.isNaN(15) // false
Number.isNaN('15') // false
Number.isNaN(true) // false
Number.isNaN(9 / NaN) // true
Number.isNaN('true' / 0) // true
  • Number.isInteger()

用来判断一个数值是否为整数。

Number.isInteger(25) // true
Number.isInteger(25.1) // false

Number.isInteger() // false
Number.isInteger(null) // false
Number.isInteger('15') // false
Number.isInteger(true) // false
  • 安全整数

JavaScript 能够准确表示的整数范围在-2^532^53之间(不含两个端点),超过这个范围,无法精确表示这个值。ES6 引入了Number.MAX_SAFE_INTEGERNumber.MIN_SAFE_INTEGER这两个常量,用来表示这个范围的上下限。

console.log(Number.MAX_SAFE_INTEGER);

console.log(Number.MIN_SAFE_INTEGER);
  • Number.isSafeInteger()

Number.isSafeInteger()则是用来判断一个整数是否落在这个安全整数范围之内。

const a = 2**53-1;
console.log(Number.isSafeInteger(a));
Math扩展

ES6在Math对象上新增了一些数学相关的方法。所有这些方法都是静态方法,只能在Math对象上调用。

  • Math.trunc()

方法用于去除一个数的小数部分,返回整数部分。

console.log(Math.trunc(5.5))
console.log(Math.trunc(-5.5))
console.log(Math.trunc(true)) // 1
console.log(Math.trunc(false)) // 0
console.log(Math.trunc(NaN)) // NaN
console.log(Math.trunc(undefined)) // NaN
console.log(Math.trunc()) // NaN
  • Math.sign()

方法用来判断一个数到底是正数、负数、还是零。对于非数值,会先将其转换为数值。

它会返回五种值。

    • 参数为正数,返回+1
    • 参数为负数,返回-1
    • 参数为 0,返回0
    • 参数为-0,返回-0
    • 其他值,返回NaN
console.log(Math.sign(5)) // 1
console.log(Math.sign(-5)) // -1
console.log(Math.sign(0)) // 0
console.log(Math.sign(NaN)) // NaN
console.log(Math.sign(true)) // 1
console.log(Math.sign(false)) // 0
  • Math.cbrt()

方法用于计算一个数的立方根。对于非数值,Math.cbrt方法内部也是先使用Number方法将其转为数值。

console.log(Math.cbrt(8)) // 2

console.log(Math.cbrt('sss')) // NaN

5.Class

Javascript是一种基于对象(object-based)的语言,你遇到的所有东西几乎都是对象。但是,它又不是一种真正的面向对象编程(OOP)语言,因为它的语法中没有class(类)。

这句话放在 ES5 可以说不为过,然而到了 ES6 这么说就已经不严谨了。因为 ES6 中已经有了专属的 class 语法了。

声明类

首先我们要先来说明在 JavaScript 世界里如何声明一个 “类”。在 ES6 之前大家都是这么做的:

let Animal = function(type) {
    this.type = type
    this.walk = function() {
        console.log( `I am walking` )
    }
}

let dog = new Animal('dog')
let monkey = new Animal('monkey')

在上述代码中,我们定义了一个叫 Animal 的类,类中声明了一个属性 type、一个方法 walk;然后通过 new Animal 这个类生成实例,完成了类的定义和实例化。上面这种写法跟传统的面向对象语言(比如C++和Java)差异很大,很容易让新学习这门语言的程序员感到困惑。

在 ES6 中把类的声明专业化了,不在用 function 的方式了,请看:

class Animal {
    constructor(type) {
        this.type = type
    }
    walk() {
        console.log( `I am walking` )
    }
}
let dog = new Animal('dog')
let monkey = new Animal('monkey')

constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。

很明显,从定义上就很专业了,有构造函数、方法,但是 ES6 增加了新的数据类型 class 吗?

console.log(typeof Animal) //function

可以发现 class 的类型还是 function,所以得出一个结论:class 的方式是 function 方式的语法糖,它的绝大部分功能,ES5都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。

Setters & Getters

对于类中的属性,可以直接在 constructor 中通过 this 直接定义,还可以直接在类的顶层来定义:

class Animal {
    constructor() {
      
    }
    get addr() {
        return '北京动物园'
    }
    set addr(value) {
        console.log('setter: '+value);
    }
}

上面代码中,addr属性有对应的存值函数和取值函数,因此赋值和读取行为都被自定义了。

在“类”的内部可以使用getset关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。再来看下如下的应用场景:

class CustomHTMLElement {
    constructor(element) {
        this.element = element
    }
    get html() {
        return this.element.innerHTML
    }
    set html(value) {
        this.element.innerHTML = value
    }
}

利用 set/get 实现了对 element.innerHTML 的简单封装。

静态方法

类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。

class Animal {
    constructor(type) {
        this.type = type
    }
    walk() {
        console.log( `I am walking` )
    }
    static eat() {
        console.log( `I am eating` )
    }
}

有没有很清爽,代码可读性一下子就上来了。上面代码中,Animal类的eat方法前有static关键字,表明该方法是一个静态方法,可以直接在Animal类上调用(Animal.eat()),而不是在Animal类的实例上调用。如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法。

注意,如果静态方法包含this关键字,这个this指的是类,而不是实例。

继承

面向对象只所以可以应对复杂的项目实现,很大程度上要归功于继承。如果对继承概念不熟悉的同学,可以自行查询。在 ES5 中怎么实现继承呢?

// 定义父类
let Animal = function(type) {
    this.type = type
}
// 定义方法
Animal.prototype.walk = function() {
    console.log( `I am walking` )
}
// 定义静态方法
Animal.eat = function(food) {
    console.log( `I am eating` )
}
// 定义子类
let Dog = function() {
    // 初始化父类
    Animal.call(this, 'dog')
    this.run = function() {
        console.log('I can run')
    }
}
// 继承
Dog.prototype = Animal.prototype

从代码上看,是不是很繁琐?而且阅读性也较差。再看看 ES6 是怎么解决这些问题的:Class 可以通过extends关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

class Animal {
    constructor(type) {
        this.type = type
    }
    walk() {
        console.log( `I am walking` )
    }
    static eat() {
        console.log( `I am eating` )
    }
}

class Dog extends Animal {
  constructor () {
    super('dog')// 调用父类的constructor(type)
  }
  run () {
    console.log('I can run')
  }
}

上面代码中,constructor方法中,都出现了super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。子类必须在constructor方法中调用super方法,否则新建实例时会报错。

6.Proxy

​ 在ES6标准中新增的一个非常强大的功能是 Proxy,它用于修改某些操作的默认行为(如查找、赋值、枚举、函数调用等),等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。

​ 简单地说,Proxy在目标对象之前架设一层“拦截”,外界访问该对象时,都必须先通过这层拦截。通过Proxy可以对外界的访问进行过滤和改写。

​ 换言之,Proxy对象可以对JavaScript中的一切合法对象的基本操作进行自定义,然后用自定义的操作去覆盖其对象的基本操作。

1.基本语法

​ ES6提供Proxy构造函数,用来生成Proxy 实例。

let p = new Proxy(target, handler)

​ 第一个参数target就是用来代理的“对象”,第二个参数handler参数用来定制拦截行为。

参数含义必选
target用Proxy包装的目标对象(可以是任何类型的对象,甚至是另一个代理)Y
handler执行一个操作时定义代理的行为的函数Y

2.常用拦截操作

  • get(target, propKey, receiver)

    拦截对象属性的读取,比如proxy.foo和proxy[‘foo’]操作。

let arr = [7, 8, 9]
arr = new Proxy(arr, {
    get(target, prop) {
        return prop in target ? target[prop] : '访问对象不存在该属性'
    }
})
console.log(arr[1]) // 8
console.log(arr[10]) // 访问对象不存在该属性

​ 上面代码表示,对数组arr访问时进行了代理,在读取arr元素时,如果访问不存在的元素,则返回“访问对象不存在该属性”。

  • set(target, propKey, value, receiver)

    拦截对象属性的设置,比如proxy.foo = v或proxy[‘foo’] = v,返回一个布尔值。如果这个方法抛出错误或者返回false,当前属性就无法被赋值。

let arr = [];
arr = new Proxy(arr, {
    set(target, prop, val) {
        if (typeof val === 'number') {
            target[prop] = val;
            return true;
        } else {
            return false;
        }
    }
})
arr.push(5);
arr.push(6);
//arr.push("string");
console.log(arr[0], arr[1], arr.length); //5 6 2

​ 上面代码中,对数组的添加元素操作进行了拦截处理,若添加元素的类型为number,则直接添加,否则添加失败。如果把arr.push("string")注释打开,会抛出异常Uncaught TypeError: proxy set handler returned false for property ‘2’。

  • has(target, propKey)

    拦截propKey in proxy(即判断对象是否具有某个属性)的操作,返回一个布尔值。

let range = {
    start: 1,
    end: 5
}

range = new Proxy(range, {
    has(target, propKey) {
        return propKey >= target.start && prop <= target.end
    }
})
console.log(2 in range) // true
console.log(9 in range) // false

​ 上面代码表示,对判断对象range是否具有某个属性操作进行拦截处理,如果参数值介于属性start和end之间,则返回true,否则返回false。

  • deleteProperty(target, propKey)

    拦截对象属性的删除操作,返回一个布尔值。如果这个方法抛出错误或者返回false,当前属性就无法被delete命令删除。

let user = {
    name: 'zhangsan',
    age: 24,
    _password: '***'
}
user = new Proxy(user, {
    get(target, prop) {
        if (prop.startsWith('_')) {
            throw new Error('不可访问')
        } else {
            return target[prop]
        }
    },
    set(target, prop, val) {
        if (prop.startsWith('_')) {
            throw new Error('不可访问')
        } else {
            target[prop] = val
            return true
        }
    },
    deleteProperty(target, prop) { // 拦截删除
        if (prop.startsWith('_')) {
            throw new Error('不可删除')
        } else {
            delete target[prop]
            return true
        }
    },
})
console.log(user.age) //24
user.age = 18
console.log(user.age) //18
try {
    user._password = 'xxx'
} catch (e) {
    console.log(e.message) //不可访问
}
console.log(user) //<target>: Object { name: "zhangsan", age: 18, _password: "***" }

try {
    // delete user.age
    delete user._password
} catch (e) {
    console.log(e.message) //不可删除
}
console.log(user) //<target>: Object { name: "zhangsan", _password: "***" }

​ 上面代码中,对定义了一个对象user,并对user的访问做了拦截处理:访问user属性时,若属性名以下划线开头,则该属性不允许访问、赋值和删除。

  • construct(target, args)

    construct方法用于拦截new命令,拦截Proxy实例作为构造函数调用的操作。construct方法返回的必须是一个对象,否则会报错。

let User = class {
    constructor(name) {
        this.name = name;
    }
}
User = new Proxy(User, {
    construct(target, args) {
        return new target(...args);
    }
})
console.log(new User('ES6')); //Object { name: "ES6" }
  • apply(target, object, args)

    拦截函数的调用操作。apply方法可以接受三个参数,分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组。

let sum = (...args) => {
    let num = 0
    args.forEach(item => {
        num += item
    })
    return num
}

sum = new Proxy(sum, {
    apply(target, ctx, args) {
        return target(...args) * 2
    }
})
console.log(sum(1, 2)); //6

​ 上面代码中,对函数sum的调用进行了拦截处理,将sum函数的执行结果进行乘2的操作再返回。

7.Promise

基本用法

ES6规定,Promise对象是一个构造函数,用来生成Promise实例。

下面代码创造了一个Promise实例。

var promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject。它们是两个函数,由JavaScript引擎提供,不用自己部署。

resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从Pending变为Resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从Pending变为Rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

Promise实例生成以后,可以用then方法分别指定Resolved状态和Reject状态的回调函数。

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为Resolved时调用,第二个回调函数是Promise对象的状态变为Reject时调用。其中,第二个函数是可选的,不一定要提供。这两个函数都接受Promise对象传出的值作为参数。

下面是一个Promise对象的简单例子。

function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms, 'done');
  });
}

timeout(100).then((value) => {
  console.log(value);
});

上面代码中,timeout方法返回一个Promise实例,表示一段时间以后才会发生的结果。过了指定的时间(ms参数)以后,Promise实例的状态变为Resolved,就会触发then方法绑定的回调函数。

Promise新建后就会立即执行。

let promise = new Promise(function(resolve, reject) {
  console.log('Promise');
  resolve();
});

promise.then(function() {
  console.log('Resolved.');
});

console.log('Hi!');

// Promise
// Hi!
// Resolved

上面代码中,Promise新建后立即执行,所以首先输出的是“Promise”。然后,then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以“Resolved”最后输出。

下面是异步加载图片的例子。

function loadImageAsync(url) {
  return new Promise(function(resolve, reject) {
    var image = new Image();

    image.onload = function() {
      resolve(image);
    };

    image.onerror = function() {
      reject(new Error('Could not load image at ' + url));
    };

    image.src = url;
  });
}

上面代码中,使用Promise包装了一个图片加载的异步操作。如果加载成功,就调用resolve方法,否则就调用reject方法。

下面是一个用Promise对象实现的Ajax操作的例子。

var getJSON = function(url) {
  var promise = new Promise(function(resolve, reject){
    var client = new XMLHttpRequest();
    client.open("GET", url);
    client.onreadystatechange = handler;
    client.responseType = "json";
    client.setRequestHeader("Accept", "application/json");
    client.send();

    function handler() {
      if (this.readyState !== 4) {
        return;
      }
      if (this.status === 200) {
        resolve(this.response);
      } else {
        reject(new Error(this.statusText));
      }
    };
  });

  return promise;
};

getJSON("/posts.json").then(function(json) {
  console.log('Contents: ' + json);
}, function(error) {
  console.error('出错了', error);
});

上面代码中,getJSON是对XMLHttpRequest对象的封装,用于发出一个针对JSON数据的HTTP请求,并且返回一个Promise对象。需要注意的是,在getJSON内部,resolve函数和reject函数调用时,都带有参数。

如果调用resolve函数和reject函数时带有参数,那么它们的参数会被传递给回调函数。reject函数的参数通常是Error对象的实例,表示抛出的错误;resolve函数的参数除了正常的值以外,还可能是另一个Promise实例,表示异步操作的结果有可能是一个值,也有可能是另一个异步操作,比如像下面这样。

var p1 = new Promise(function (resolve, reject) {
  // ...
});

var p2 = new Promise(function (resolve, reject) {
  // ...
  resolve(p1);
})

上面代码中,p1p2都是Promise的实例,但是p2resolve方法将p1作为参数,即一个异步操作的结果是返回另一个异步操作。

注意,这时p1的状态就会传递给p2,也就是说,p1的状态决定了p2的状态。如果p1的状态是Pending,那么p2的回调函数就会等待p1的状态改变;如果p1的状态已经是Resolved或者Rejected,那么p2的回调函数将会立刻执行。

var p1 = new Promise(function (resolve, reject) {
  setTimeout(() => reject(new Error('fail')), 3000)
})

var p2 = new Promise(function (resolve, reject) {
  setTimeout(() => resolve(p1), 1000)
})

p2
  .then(result => console.log(result))
  .catch(error => console.log(error))
// Error: fail

上面代码中,p1是一个Promise,3秒之后变为rejectedp2的状态在1秒之后改变,resolve方法返回的是p1。由于p2返回的是另一个 Promise,导致p2自己的状态无效了,由p1的状态决定p2的状态。所以,后面的then语句都变成针对后者(p1)。又过了2秒,p1变为rejected,导致触发catch方法指定的回调函数。

Promise.prototype.then()

Promise实例具有then方法,也就是说,then方法是定义在原型对象Promise.prototype上的。它的作用是为Promise实例添加状态改变时的回调函数。前面说过,then方法的第一个参数是Resolved状态的回调函数,第二个参数(可选)是Rejected状态的回调函数。

then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。

getJSON("/posts.json").then(function(json) {
  return json.post;
}).then(function(post) {
  // ...
});

上面的代码使用then方法,依次指定了两个回调函数。第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。

采用链式的then,可以指定一组按照次序调用的回调函数。这时,前一个回调函数,有可能返回的还是一个Promise对象(即有异步操作),这时后一个回调函数,就会等待该Promise对象的状态发生变化,才会被调用。

getJSON("/post/1.json").then(function(post) {
  return getJSON(post.commentURL);
}).then(function funcA(comments) {
  console.log("Resolved: ", comments);
}, function funcB(err){
  console.log("Rejected: ", err);
});

上面代码中,第一个then方法指定的回调函数,返回的是另一个Promise对象。这时,第二个then方法指定的回调函数,就会等待这个新的Promise对象状态发生变化。如果变为Resolved,就调用funcA,如果状态变为Rejected,就调用funcB

如果采用箭头函数,上面的代码可以写得更简洁。

getJSON("/post/1.json").then(
  post => getJSON(post.commentURL)
).then(
  comments => console.log("Resolved: ", comments),
  err => console.log("Rejected: ", err)
);

8 Generators

Generator函数是ES6提供的一种异步编程解决方案,语法行为与传统函数完全不同。通俗的讲Generators是可以用来控制迭代器的函数,它们可以暂停并在指定时间恢复。

​ 常规循环:

for (let i = 0; i < 3; i += 1) {
    console.log(i)
}
// 0 -> 1 -> 2 

​ 利用Generator:

function* generatorForLoop() {
    for (let i = 0; i < 3; i += 1) {
        yield console.log(i)
    }
}
const genForLoop = generatorForLoop()

console.log(genForLoop.next()) // 0
console.log(genForLoop.next()) // 1
console.log(genForLoop.next()) // 2

​ 常规的循环只能一次遍历完所有值,Generator可以通过yield关键字和next方法拿到依次遍历的值,让遍历的执行变得可控。

1.基本语法

​ Generator函数有几个值得注意的点:一是,function关键字与函数名之间有一个星号*;二是,函数体内部使用yield语句定义不同的内部状态,控制程序的执行的“暂停”,;三是,通过调用next方法来恢复程序的执行。

function* myGenerator() {
    yield "first";
    yield "second";
    return "third";
}

let mg = myGenerator()

​ 上面代码定义了一个Generator函数myGenerator,它内部有两个yield语句"first"和"second",即该函数有三个状态:first,second和return语句(结束执行)。

​ 调用Generator函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield语句(或return语句)为止。

​ 换言之,Generator函数是分段执行的,yield语句是暂停执行的标记,而next方法可以恢复执行。next方法返回一个对象,它的value属性就是当前yield语句后面表达式的值,done属性是一个布尔值,表示遍历是否结束。

mg.next()
// { value: 'first', done: false }

mg.next()
// { value: 'second', done: false }

mg.next()
// { value: 'third', done: true }

mg.next()
// { value: undefined, done: true }

注意

Generator 函数的定义不能使用箭头函数,否则会触发 SyntaxError 错误

let generator = * () => {} // SyntaxError
let generator = () * => {} // SyntaxError
let generator = ( * ) => {} // SyntaxError

​ 以上写法都是错误的。

2.yield 表达式

​ yield 表达式用来暂停和恢复一个生成器函数。关于 yield 表达式,有以下几个知识点需要注意:

(1)yield 表达式的返回值是 undefined,但是遍历器对象的 next 方法可以修改这个默认值。

  function* myGenerator() {
      let val
      val = yield 'first'
      console.log( `1:${val}` ) // 1:undefined
      val = yield 'second'
      console.log( `2:${val}` ) // 2:undefined
      val = yield 'third'
      console.log( `3:${val}` ) // 3:undefined
      return val
  }

  var g = myGenerator()

  console.log(g.next()) // {value: 'first', done: false}
  console.log(g.next()) // {value: 'second', done: false}
  console.log(g.next()) // {value: 'third', done: false}
  console.log(g.next()) // {value: undefined, done: true}

​ 上面代码执行结果如下图所示, 可以看出来yield 表达式的返回值是 undefined。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gFuy1odU-1632479443405)(Web 前端开发实战教程.assets/image-20210806091533588.png)]

(2)若Generator函数中没有yield表达式,就变成了一个单纯的暂缓执行函数。

function* fun() {
  console.log('执行Generator函数 !')
}

var g = fun()
console.log("调用Generator函数!")

g.next()

​ 上面代码中,若函数fun是普通函数,则在为变量g赋值时就会执行。但是,函数fun是一个 Generator 函数,只有调用next方法时,函数f才会执行。控制台输出如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mXKPlTKg-1632479443409)(Web 前端开发实战教程.assets/image-20210806091814159.png)]

(3)yield语句只能用在 Generator 函数里面,用在其他地方都会报错。

(function (){
  yield 1;
})()
// Uncaught SyntaxError: yield expression is only valid in generators 

(4)yield语句如果用在一个表达式之中,必须放在圆括号里面。如果没有用括号包裹起来,则会报语法错误:Uncaught SyntaxError: yield is a reserved identifier。

function* fun() {
	console.log('yield:' + (yield));
	console.log('yield:' + (yield 123)); 
}
var g = fun();
console.log(g.next());
console.log(g.next());

​ 以上代码运行,控制台输出如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d4JabU1c-1632479443412)(Web 前端开发实战教程.assets/image-20210806094255851.png)]

3.Generator 对象的方法

​ Generator对象有几个方法,nextreturnthrow

(1)next(value)

​ 前面我们讲了,Generator对象通过 next 方法来获取每一次遍历的结果,这个方法返回一个对象,这个对象包含两个属性:value和done。value是指当前程序的运行结果,done表示遍历是否结束。

​ 其实 next 是可以接受参数的,这个参数可以让你在 Generator 外部给内部传递数据。yield句本身没有返回值,next方法传递的参数就会被当作上一个yield语句的返回值。

  function* gen() {
      var val = 100
      while (true) {
          console.log( `before ${val}` )
          val = yield val
          console.log( `return ${val}` )
      }
  }

  var g = gen()
  console.log(g.next(200).value)
  console.log(g.next(300).value)
  console.log(g.next(400).value)

​ 上述代码的执行结果如下图所示:
在这里插入图片描述

​ 下面来分析这段代码的执行过程:

  • 首先g.next(200) 会执行gen函数内部的代码,遇到第一个yield暂停。所以 console.log(before ${val})执行打印了"before 100",此时变量val值为100。执行到 yield val暂停,next方法返回了 100,但此时yield val 并没有赋值给 val。
  • 接下来g.next(300) 这句代码会从 val = yield val这句继续往后执行。因为next传入了 300,300作为上一个yield表达式的返回值,所以执行 val = yield val后变量val被赋值300,故执行到 console.log( return ${val})在控制台打印了 ”return 300“。此时没有遇到yield ,继续执行 console.log(before ${val})打印出了”before 300“,再执行遇到了 ```yield val` ``程序暂停。
  • g.next(400) 重复上一步骤。

​ 这个功能有重要意义:Generator函数从暂停状态到恢复执行,它的上下文状态(context)是不变的。通过next方法的参数,就能在Generator函数开始运行之后,继续向函数体内部注入值。换句话说,在Generator函数运行的不同阶段,可以从外部向函数体内部注入不同的值,从而调整函数行为。

(2)return()

​ return方法可以终结遍历Generator函数,有点类似for循环的 break。return方法返回的也是一个对象,有value和done两个属性,value表示返回值,done表示遍历是否终止。因为return方法作用就是终止遍历Generator函数,所以return返回对象的done属性值总是为true。

function* fun() {
    yield 1
    yield 2
    yield 3
}

var g = fun()

console.log(g.next()) // {value: 1, done: false}
console.log(g.return()) // {value: undefined, done: true}
console.log(g.next()) // {value: undefined, done: true}

​ return也可以传入参数,作为返回的value值。

function* gen() {
    yield 1
    yield 2
    yield 3
}

var g = gen()

console.log(g.next()) // {value: 1, done: false}
console.log(g.return(100)) // {value: 100, done: true}
console.log(g.next()) // {value: undefined, done: true}

(3) throw()

​ throw方法可以在函数体外抛出错误,然后在Generator函数体内捕获,即Generator外部控制内部执行的“终断”。

function* gen() {
    while (true) {
        try {
            yield 'normal'
        } catch (e) {
            console.log(`内部捕获异常:${e}`)
        }
    }
}

var g = gen()
console.log(g.next()) // { value: 'normal', done: false }
console.log(g.next()) // { value: 'normal', done: false }
console.log(g.next()) // { value: 'normal', done: false }

g.throw('error') //抛出异常

console.log(g.next()) // { value: 'normal', done: false }

​ 上述代码运行结果如下图所示:
在这里插入图片描述

​ 从运行结果可以看出,只要Generator函数内部部署了try...catch代码块,那么遍历器的throw方法抛出的错误不影响下一次遍历。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值