概述

  1. ECMAScript, JavaScript, NodeJS, 它们的区别是什么?

    ECMAScript:简称ES,是一个语言标准(循环,判断,变量,数组等数据类型)

    JavaScript: 运行再浏览器端的语言,该语言使用ES标准.ES + Web api = JavaScript

    Nodejs: 运行在服务器端的语言,该语言使用ES标准. ES + node api = JavaScript

    无论JavaScript,还是Nodejs,它们都是ES的超集

  2. ECMAScript有哪些关键版本

    ES3.0: 1993

    ES5.0: 2009

    ES6.0: 2015, 从该版本之后,不再使用数字作为编号,而使用年份,每年发布一个新版本

    ES7.0: 2016

  3. 为什么ES6如此重要

    ES6解决了JS无法开发大型应用的语言层面的问题.

  4. 如何应对兼容性问题

    之后的课程会介绍如何解决

  5. 学习本课程需要哪些前置知识

    HTML, CSS, JavaScript

  6. 这套课程难不难?

块级绑定

使用var声明变量

  1. 允许重复声明导致数据被覆盖

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var a = 1;

    function print() {
    console.log(a);
    }

    // 假设这里有一千行代码

    var a = 2;
    print(); // 2
  2. 变量提升: 怪异的数据访问

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    if (Math.random() < 0.5) {
    var a = 'abc';
    console.log(a);
    } else {
    console.log(a);
    }

    console.log(a);
    // abc
    // abc
    // 或
    // undefined
    // undefined

    // 因为变量提升,所以实际如下
    var a;
    if (Math.random() < 0.5) {
    a = 'abc';
    console.log(a);
    } else {
    console.log(a);
    }
  3. 变量提升: 闭包问题

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    var container = document.querySelector('.container');
    for(var i=1; i<=10; i++) {
    var btn = document.createElement('button');
    btn.innerHTML = i;
    container.appendChild(btn);
    btn.onclick = function() {
    console.log(i);
    }
    }
    // 发现无论点击哪一个按钮,都输出11
    // 因为实际上是这样
    var i;
    var container = document.querySelector('.container');
    for(i=1; i<=10; i++) {
    var btn = document.createElement('button');
    btn.innerHTML = i;
    container.appendChild(btn);
    btn.onclick = function() {
    console.log(i);
    }
    }

    // 过去为了解决这样的问题,需要使用立即执行函数
    var container = document.querySelector('.container');
    for (var i = 1; i <= 10; i++) {
    var btn = document.createElement('button');
    btn.innerHTML = i;
    container.appendChild(btn);
    (function (i) {
    btn.onclick = function () {
    console.log(i);
    };
    })(i);
    }
  4. 全局变量挂载到全局对象: 全局对象成员污染问题

    1
    2
    var abc = "123";
    console.log(window.abc);

使用let声明变量

let声明的变量,不会挂载到全局对象

let声明的变量,不允许在当前作用域重复声明(包括块级作用域)

块级作用域: 代码执行时遇到花括号,产生块级作用域,遇到结束花括号,块级作用域销毁.

let声明的变量,不能在初始化之前使用(存在暂时性死区(TDZ)).当代码运行到该变量的初始化代码时,会从暂时性死区中移除.

在循环中,用let声明的循环变量,会特殊处理,每次进入循环体,都会开启一个新的作用域,并且将循环变量绑定到该作用域.每次循环使用的是一个全新的作用域.

在循环中,使用let声明的循环变量,在循环结束后会销毁.

1
2
3
4
5
6
7
8
9
let container = document.querySelector('.container');
for(let i=1; i<=10; i++) {
let btn = document.createElement('button');
btn.innerHTML = i;
container.appendChild(btn);
btn.onclick = function() {
console.log(i);
}
}

使用const声明常量

let基本相同,除了const必须初始化,且值不能修改.

注意如果用const声明对象类型,只要地址不变就可以.地址里面的内容可以变

在实际开发中,尽量使用const,当需要改变时,再使用let

字符串和正则表达式

更好的Unicode支持

早期,由于存储空间宝贵,Unicode使用16位二进制来存储文字.我们将一个16位的二进制编码叫做一个码元(code Unit)

后来,由于技术的发展,Unicode对文字编码进行了扩展,将某些文字扩展到了32位(占用两个码元),并且,将某个文字对应的二级制数字叫做码点(Code Point)

1
2
3
console.log('𠮷'.length)
// 2
// 因为𠮷这一个码点有两个码元

ES6为了解决这个困扰,为字符串提供了方法:codePointAt(n),可以得到序号为n的码元的码点(往后判断).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
const text = '𠮷';
console.log('得到第一个码点:', text.codePointAt(0)); // 134071
console.log('得到第二个码点:', text.codePointAt(1)); // 57271
console.log('得到第一个码元:', text.charCodeAt(0)); // 55362
console.log('得到第二个码元:', text.charCodeAt(1)); // 57271

/**
* 判断字符串char,是32位还是16位
* @param {String} char
*/
function is32bit(char, i) {
char.codePointAt(i) > 0xFFFF
}

/**
* 得到一个字符串真实的码点数量
* @param {String} str 要判断的字符串
* @returns 字符串真实的码点数量
*/
function getLengthOfCodePoint(str) {
let len = 0;
for(let i=0; i<str.length; i++) {
// i在索引码元
if(is32bit(str,i)) {
// 当前字符串,在i的位置,占用了两个码元,跳过下一个码元
i++;
}
len++;
}
return len;
};

同时,ES6为正则表达式添加了一个flag:u,如果添加了该配置,匹配的时候使用码点匹配

更多的字符串API

一下均为字符串的实例方法

includes

1
str.includes('substr', startIndex)

判断字符串中是否包含指定的子字符串

startWith

判断是否以substr开始

1
str.startWith('substr')

endWith

判断是否以substr结束

1
str.endWith('substr')

repeat

返回一个字符串重复n次的结果

1
str.repeat(n)

*正则中的粘连标记

标记名: y

含义: 匹配时,完全按照正则对象中的lastIndex位置开始匹配,并且匹配的位置必须在lastIndex位置

1
2
3
4
5
const text = "Hello World!!!"
const reg1 = /W\w+/
const reg2 = /W\w+/
console.log(reg1.test(text)) // true
console.log(reg2.test(text)) // false

模板字符串

ES6之前处理字符串繁琐的两个方面:

  1. 多行字符串
  2. 字符串拼接

ES6提供了模板字符串的写法用``来包裹字符串

如果要在字符串中使用表达式,则在``中用${}包裹即可

*模板字符串标记

标记是一个函数,参数如下

  1. 参数1: 被插值分割的字符串的数组
  2. 剩余参数: 插值的值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const love1 = '苹果';
const love2 = '香蕉';
const myTag = ([...args],...variables) => {
for(const item of args) {
console.log(item);
}
for(const item of variables) {
console.log(item);
}
};
let text = myTag`我喜欢了${love1}${love2}`;


// 相当于
// text = myTag(['我喜欢了', '和'], love1, love2);

console.log(text);

// 我喜欢了
// 和

// 苹果
// 香蕉
// undefined

用处,转义字符当普通字符输出,可以使用String.raw模板字符串标记

1
2
3
4
5
6
7
console.log('abc\nbcd');

// abc
// bcd

console.log(String.raw`abc\nbcd`);
// abc\nbcd

下面这种情况,当用户在text中写入标签时,如<h1>jdsfj</h1>,就会导致输出为标题.我们本意想让他输出纯文本.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<textarea name="" id="container"></textarea>
<p id="text"></p>
<button id="btn">按钮</button>
<script>
const container = document.getElementById('container')
const btn = document.getElementById('btn')
const text = document.getElementById('text')
btn.onclick = () => {
text.innerHTML = container.value
}
</script>
</body>
</html>

则进行如下改造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<textarea name="" id="container"></textarea>
<div id="text"></div>
<button id="btn">按钮</button>
<script>
const container = document.getElementById('container')
const btn = document.getElementById('btn')
const text = document.getElementById('text')
btn.onclick = () => {
text.innerHTML = safe`<p>${container.value}</p>`
}
function safe(strings, ...values) {
let str = ""
for(let i=0; i<values.length; i++) {
values[i] = values[i].replace(/</g, '&lt;').replace(/>/g, '&gt;')
str += strings[i] + values[i]
if(i === values.length - 1) {
str += strings[i+1]
}
}
return str
}
</script>
</body>
</html>

函数

参数默认值

在书写时,直接给形参赋的值就是默认值

对arguments的影响

非严格模式下,在函数内部修改形参,会导致arguments的值也发生改变.

1
2
3
4
5
6
7
8
9
10
11
12
13
function sum(a, b) {
console.log(arguments[0], arguments[1]);
console.log(a, b);
a = 3;
console.log(arguments[0]);
console.log(a);
}

sum(1, 2);
// 1 2
// 1 2
// 3
// 3

但是在严格模式下,在函数内部修改形参,arguments的值不会发生改变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"use strict"
function sum(a, b) {
console.log(arguments[0], arguments[1]);
console.log(a, b);
a = 3;
console.log(arguments[0]);
console.log(a);
}

sum(1, 2);
// 1 2
// 1 2
// 1
// 3

只要给函数加上参数默认值,该函数会自动变成严格模式下的规则:arguments和形参脱离

留意暂时性死区

形参和ES6中的letconst一样,具有作用域,并且根据参数的声明顺序,存在暂时性死区(TDZ)

1
2
3
4
5
6
7
function test(a, b = a) {
console.log(a, b);
}

test(1);

// 1 1
1
2
3
4
5
6
7
function test(a = b, b) {
console.log(a, b);
}

test(undefined, 1);

// ReferenceError: Cannot access 'b' before initialization

剩余参数

使用arguments的缺陷

  1. 如果和形参配合使用,容易导致混乱
  2. 从语义上,使用arguments获取参数,由于形参缺失,无法从函数定义上理解函数的真实意图

ES6的剩余参数,专门用于收集末尾的所有参数到一个形参数组中.

一个函数最多只能有一个剩余参数,而且必须是最后一个形参

展开运算符

使用...要展开的东西

函数柯里化

用户固定某个函数前面的参数,得到一个新的函数,新的函数调用时,接收剩余参数

1
2
3
4
5
6
7
8
9
function curry(func, ...args) {
return function(...subArgs) {
const allArgs = [...args,...subArgs];
if(allArgs.length >= func.length) {
return func(...allArgs);
}
return curry(func, ...allArgs);
}
}

明确函数的双重用途

ES6提供了一个特殊的API,可以使用该API在函数内部,判断该函数是否使用了new来调用

1
new.target

如果没有使用new来调用函数,则返回undefined

如果使用new调用函数,则得到的是new关键字后面的函数本身

箭头函数

回顾: this指向

  1. 通过对象调用函数,this指向调用对象
  2. 直接调用函数,this指向全局对象
  3. 通过new调用函数,this指向新创建的对象
  4. 通过apply,call,bind调用函数,this指向指定的数据
  5. DOM事件函数,this指向事件源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const obj = {
count: 0,
start: function () {
// this -> obj
setInterval(function () {
// this -> windox/global
this.count++;
console.log(this.count);
}, 1000);
},
};

obj.start();

/**
* NaN
* NaN
* ...
*/

在过去需要通过使用闭包来解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const obj = {
count: 0,
start: function () {
// this -> obj
let _this = this;
setInterval(function () {
// this -> windox/global
_this.count++;
console.log(this_.count);
}, 1000);
},
};

obj.start();

/**
* NaN
* NaN
* ...
*/

现在可以直接使用箭头函数.

使用语法

1
2
3
const func = (arg) => {

}

注意细节

箭头函数没有this,arguments,new.target,它只能继承定义位置的执行上下文的对应变量,与如何调用无关

应用场景

  1. 事件处理函数
  2. 异步处理函数
  3. 临时函数
  4. 为了绑定外层this

对象

新增的对象字面量语法

成员速写

如果对象字面量初始化时,如果一个成员的值来源于同名变量的值,则可以直接简写

1
2
3
4
5
6
const name = "zhangsan"
const obj = {
name
}
console.log(obj);
// { name: 'sdfj' }

方法速写

对象字面量初始化时,方法可以省略冒号function关键字.

1
2
3
4
5
6
7
8
9
10
const ojb = {
sayHello: function(){
console.log('hello')
}
}
const obj = {
sayHello(){
console.log('hello')
}
}

计算属性名

有的时候,初始化对象时,某些属性名可能来源于某个表达式的值,在ES6,可以使用中括号来表示该属性名是通过计算得到的.

1
2
3
4
5
6
const pro1 = 'name';
const obj = {
[pro1]: '张三',
};
console.log(obj)
//{ name: '张三' }

Object的新增API

以下都是静态方法

Object.is

用于判断两个对象是否相等,基本上和严格相等一致

1
2
3
4
5
console.log(NaN===NaN) 	// false
console.log(+0===-0) // true
// 上述是历史遗留问题
console.log(Object.is(NaN, NaN)) // true
cosnole.log(Object.is(+0,-0)) // false

Object.assign

1
2
3
4
5
6
const obj = Object.assign(obj1,obj2);
// 将obj2的数据覆盖到obj1,并改变obj1,返回obj1
// 要想不改动obj1,可以用如下方法
const obj = Object.assign({},obj1,obj2)
// ES7之后可以使用展开运算符
const obj = {...obj1, ...obj2}

Object.getOwnProperNames的枚举顺序

Object.getOwnPropertyNames方法之前就存在,只不过,官方没有明确要求,对属性的顺序如何排列,并没有明确要求.

ES6规定了该方法返回的数组排序方式与for...in循环或Object.keys()方法获取的顺序一致.

根据现代 ECMAScript 规范,遍历顺序是明确定义的,并且在实现之间是一致的。在原型链的每个组件中,所有非负整数键(可以是数组索引的键)将首先按值升序遍历,然后按属性创建的时间升序遍历其他字符串键。

Object.setPrototypeOf

用于设置某个对象的隐式原型

1
2
3
Object.setPrototypeOf(obj1, obj2)
// 相当于
obj1.__proto__ = obj2

面向对象简介

面向对象: 一种编程思想,和具体语言无关.

对比面向过程:

  • 面向过程:思考的切入点是功能的步骤
  • 面向对象:思考的切入点是对象的划分

类: 构造函数的语法糖

传统构造函数的问题

  1. 属性和原型方法定义分离,降低了可读性
  2. 原型成员可以被枚举
  3. 默认情况下,构造函数仍然可以被当作普通函数使用

类的特点

  1. 类声明不会被提升,与letconst一样,存在暂时性死区
  2. 类中所有代码均在严格模式下执行
  3. 类的所有方法都是不可枚举的
  4. 类的所有方法内部都无法被当作构造函数使用
  5. 类的构造器必须使用new来调用

类的其他书写方式

可计算的成员名

1
2
3
4
const pro1 = 'name'
class Aniaml {
[pro1]: 'zhangsan'
}

getter和setter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Animal {
#name;
constructor(name) {
this.#name = name;
}
get name() {
return this.#name;
}
set name(name) {
this.#name = name;
}
}
const dog = new Animal('旺财');
console.log(dog.name);
dog.name = '小黑';
console.log(dog.name);
// 旺财
// 小黑

静态成员

使用static关键字定义的成员即静态成员

1
2
3
class Animal {
static name = 'zhangsan'
}

字段初始化器(ES7)

字段初始化器相当于在constructor中加上这些内容

1
2
3
4
5
6
7
8
9
10
11
12
class Animal {
constructor() {
this.name = 'zhangsan';
this.age = 15
}
}
// 可以直接写为如下形式
class Animal {
name = 'zhangsan',
age = 15
}
// 直接书写,是定义在实例上的,而不是静态属性,这与方法不同,方法是定义在原型上的
  1. 使用static的字段初始化器,添加的是静态成员.
  2. 没有使用static的字段初始化器,添加的成员位于对象上,注意,不是位于原型上
  3. 箭头函数在字段初始化器位置上,this指向当前对象
1
2
3
4
5
6
7
8
9
class Animal {
constructor(name) {
this.name = name;
}
print = () => {
console.log(this.name)
}
}
// 这里的print没有定义在原型上,而是每一个实例都会有一个自己的print

类表达式

1
2
3
4
5
6
7
const A = class {
// 匿名类,类表达式
a = 1;
b = 2;
}
const a = new A();
console.log(a);

装饰器(ES7)

类的继承

如果两个类A和B,如果可以描述为B是A,则A和B形成继承关系

如果B是A,则:

  1. B继承自A
  2. A派生B
  3. B是A的子类
  4. A是B的父类

如果A是B的父类,则B会自动拥有A中所有实例成员

新的关键字

  • extends:继承,用于类的定义
  • super:
    • 直接当作函数调用,当作父类的构造函数
      • 如果定义了construcor,且该类是子类,必须在访问子类的constructor的this之前,调用super
      • 如果子类不写constructor,则会自动
    • 当作对象调用
      • 在实例方法中,指向父类原型
      • 在静态方法中,指向父类本身

抽象类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Animal {
constructor(name) {
if (new.target === Animal) {
throw new Error('不能实例化Animal类');
}
this.name = name;
}
eat() {
console.log(`${this.name}吃东西`);
}
}

class Dog extends Animal {
constructor(name, age) {
super(name);
this.age = age;
}
run() {
console.log(`${this.name}在跑`);
}
}

解构

符号

普通符号

符号是ES6新增的一个数据类型,它通过使用函数Symbol(符号描述)来创建

符号设计的初衷,是为了给对象设置私有属性

私有属性:只能在对象内部使用,外面无法使用.

符号具有以下特点:

  • 没有字面量
  • 使用typeof得到的类型是symbol
  • 每次调用Symbol得到的符号永远不相等,无论符号名是否相同
  • 符号可以作为对象的属性名存在,这种属性称之为符号属性
    • 开发者可以通过精心的设计,让这些属性无法通过常规方式被外界访问.
    • 符号属性是不能被枚举的,因此,在for-in循环中无法读取到符号属性,Object.keys方法也无法读取到符号属性
    • Object.getOwnPropertyNames尽管可以得到所有无法枚举的属性,但是仍然无法读取到符号属性
    • ES6新增了Object.getOwnPropertySymbols方法,可以读取符号
  • 符号无法被隐式转换,因此不能被用于数学运算,字符串拼接或其他隐式转换的场景,但符号可显示地转换为字符串,通过String构造函数进行转换即可.console.log之所以可以输出符号,是因为它在内部进行了显示转换.

共享符号

根据某个符号描述能够得到同一个符号

1
Symbol.for("符号描述")   // 获取共享符号

知名(公共,具名)符号

知名符号是一些具有特殊含义的共享符号,通过Symbol的静态属性得到

ES6延续了ES5的思想:减少魔法,暴露内部实现!

因此,ES6用知名符号暴露了某些场景的内部实现

  1. Symbol.hasInstance

    该符号用于定义构造函数的静态成员,它将影响instanceof的判定

    1
    2
    3
    obj instanceof A
    // 等效于
    A[Symbol.hasInstance](obj)
  2. Symbol.isConcatSpreadable

    该符号会影响到数组的cancat方法

    1
    2
    3
    4
    const arr = [3];
    const result = arr.concat(56, [5, 6, 7, 8]);
    console.log(result)
    // [3, 56, 5, 6, 7, 8]
  3. Symbol.toPrimitive

    该知名符号会影响类型转换的结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const obj = {
    a: 1,
    b: 2,
    };
    // 会依次尝试以下方法,直达obj转化为基本类型
    console.log(obj[Symbol.toPrimitive]) // undefined
    console.log(obj.valueOf()) // {a: 1, b: 2}
    console.log(obj.toString()) // [object Object]



    console.log(obj + 123); // [object Object]123
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    const obj = {
    [Symbol.toPrimitive](hint) {
    if (hint === "number") {
    return 42; // 数值上下文
    }
    if (hint === "string") {
    return "Hello"; // 字符串上下文
    }
    return "default"; // 默认上下文
    }
    };

    console.log(+obj); // 42
    console.log(`${obj}`); // "Hello"
    console.log(obj + ""); // "default"

  4. Symbol.toStringTag

    该知名符号会影响Object.prototype.toString的返回值

  1. 其他知名符号

迭代器和生成器

迭代器

背景知识

  1. 什么是迭代

    从一个数据集合中按照一定的顺序,不断取出数据的过程

  2. 迭代和遍历的区别?

    迭代强调的是依次取数据,并不保证取多少,也不保证把所有的数据取完

    遍历强调的是完整性,要把整个数据依次全部取出

  3. 迭代器

    对迭代过程的封装,在不同的语言中有不同的表现形式,通常为对象

  4. 迭代模式

    一种设计模式,用于统一迭代的过程,并规范了迭代器的规格:

    • 迭代器应该具有得到下一个数据的能力
    • 迭代器应该具有判断是否还有后续数据的能力

JS中的迭代器

JS规定,如果一个对象具有next()方法,并且该方法返回一个对象,该对象的格式如下

1
{value: 值, done:是否迭代完成}

则认为该对象是一个迭代器

含义:

  • next方法:用于得到下一个数据
  • 返回的对象
    • value下一个对象的数据
    • done是否结束
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const arr = [1, 2, 3, 4, 5];
const iterator = {
i: 0,
next() {
//当前的数组下标
return {
value: arr[this.i++],
done: this.i >= arr.length,
};
},
};

console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());

// { value: 1, done: false }
// { value: 2, done: false }
// { value: 3, done: false }
// { value: 4, done: false }
// { value: 5, done: true }
// { value: undefined, done: true }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function createFeivoIterator() {
let prev1 = 1,
prev2 = 1;
return {
next() {
const result = {
value: prev1 + prev2,
done: false,
};
prev2 = prev1;
prev1 = result.value;
return result;
},
};
}

const iter = createFeivoIterator();
console.log(iter.next().value);
console.log(iter.next().value);

可迭代协议与for-of循环

概念回顾

  • 迭代器(iterator): 一个具有next方法的对象, next方法返回下一个数据并且能指示是否迭代完成.
  • 迭代器创建函数(iterator creator): 一个返回迭代器的函数

可迭代协议

ES6规定,如果一个对象具有知名符号属性Symbol.iterator,并且属性值是一个迭代器创建函数,则该对象是可迭代的(iterable)

思考:如何知晓一个对象是否是可迭代的?

思考:如何遍历一个可迭代对象?

for-of循环

for-of循环用于遍历可迭代对象,格式如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//迭代完成后循环
for(const item of arr) {
console.log(item)
}

// 相当于
const iterator = arr[Symbol.iterator]();
let result = iterator.next();
while(!result.done) {
const item = result.value; // 取出数据
console.log(item);
// 下一次迭代
result = iterator.next();
}

展开运算符与可迭代对象

展开运算符可以将可迭代对象展开,这样可以轻松将其转换为数组

生成器

  1. 什么是生成器?

生成器是通过构造函数Generator创建的对象,生成器既是一个迭代器,同时又是一个可迭代对象.

  1. 如何创建生成器?

生成器的创建,必须使用生成器函数(Generator Function)

  1. 如何书写一个生成器函数呢?
1
2
3
function* mthod() {

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function* test() {
yield 1;
yield 2;
yield 3;
}

const generator = test();
console.log(generator.next().value);
console.log(generator.next().value);
console.log(generator.next().value);

// 1
// 2
// 3
  1. 生成器函数内部是如何执行的?

生成器函数内部是为了给生成器提供迭代数据

每次调用生成器的next方法,将导致生成器函数运行到下一个yield关键字位置

yield是一个关键字,该关键字只能在生成器函数内部使用,表达产生一个迭代数据.

  1. 有哪些需要注意的细节

    1. 生成器函数可以有返回值,返回值表示第一次done: true时对应的value
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    function* test() {
    yield 1;
    yield 2;
    return 4;
    yield 3;
    }

    const generator = test();
    console.log(generator.next());
    console.log(generator.next());
    console.log(generator.next());
    console.log(generator.next());

    // { value: 1, done: false }
    // { value: 2, done: false }
    // { value: 4, done: true }
    // { value: undefined, done: true }
    1. 调用生成器的next方法时,可以传递参数,传递的参数会交给yield表达式的返回值
    2. 第一次调用next方法时,传参没有任何意义
    1. 在生成器函数内部还可以调用其他生成器函数,但是要注意加上*

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      function* t1() {
      yield "a"
      yield "b"
      }
      function* test() {
      yield* t1();
      yield 1;
      yield 2;
      yield 3;
      }
  2. 生成器的其他API

    • return方法: 调用该方法,可以提前结束生成器函数,从而让整个迭代过程结束
    • throw方法: 调用该方法,在生成器中产生一个错误

生成器的应用-异步任务控制

ES6之后有了Promise,但是asyncawait要ES7才有.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
function* task() {
const d = yield 1;
// d: 1
const resp = yield fetch('http://101.132.72.36:5100/api/local');
const result = yield resp.json();
console.log(result);
}

run(task);

function run(generatorFunc) {
const generator = generatorFunc();
let result = generator.next(); // 启动任务,开始迭代
handleResult();
function handleResult() {
if (result.done) {
return; // 迭代完成
}
// 1. 迭代的数据是一个Promise
if (result.value.then === 'function') {
result.value.then(
function (data) {
result = generator.next(data);
handleResult();
},
function (error) {
result = generator.throw(error);
handleResult();
}
);
} else {
result = generator.next(result.value);
handleResult();
}
}
}

代理与反射

属性描述符

Property Descriptor属性描述符,用于描述一个属性的相关信息.

通过Object.getOnwPropertyDescriptor(对象,"属性名")来得到某对象的某属性的属性描述符

1
2
3
4
5
6
7
const obj = {
a: 1,
b: 2
}

console.log(Object.getOwnPropertyDescriptor(obj, 'a'));
// {value: 1, writable: true, enumerable: true, configurable: true}

通过Object.getOnwPropertyDescriptors(对象)来得到该对象的所有属性的属性描述符

1
2
3
4
5
6
7
8
9
10
11
12
13
const obj = {
a: 1,
b: 2
}

console.log(Object.getOwnPropertyDescriptor(obj, 'a'));
// {value: 1, writable: true, enumerable: true, configurable: true}
console.log(Object.getOwnPropertyDescriptors(obj));

// {
// a: { value: 1, writable: true, enumerable: true, configurable: true },
// b: { value: 2, writable: true, enumerable: true, configurable: true }
// }
  • value: 属性值
  • configurable: 该属性描述符是否可以被修改
  • enumerable: 该属性是否可以被枚举,for-in,Object.keys()``Object.values()等方法
  • writable: 该属性的值是否可以被修改

如果需要为某个对象添加属性修改属性时,配置其属性描述符,可以使用下面的代码

1
Object.defineProperty(对象,"属性名",属性描述符)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const obj = {
a: 1,
b: 2,
};

Object.defineProperty(obj, 'c', {
value: 3,
writable: false,
enumerable: false,
configurable: false,
});
console.log(obj);
// node: { a: 1, b: 2 }
// 浏览器: { a: 1, b: 2, c: 3 }
console.log(obj.c);
// 3
obj.c = 4;
console.log(obj);
// node: { a: 1, b: 2 }
// 浏览器: { a: 1, b: 2, c: 3 }

也可以使用下面的代码为多个属性配置属性描述符

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const obj = {
a: 1,
b: 2
}

Object.defineProperties(ojb,{
a: {
value: 3,
configurable: false,
enumrable: false,
writable: false
},
b: {
///....
}
})

存取器属性

注意(get,set)是不能与(value,writable)属性共存的,只能配置两者之一

属性描述符中,如果配置了getset中的任何一个,则该属性,不再是一个普通属性,而是变成了存取器属性.

getset配置均为函数,如果一个属性是存取器属性,则读取该属性时,会运行get方法,将get方法的返回值作为属性值;如果给该属性赋值,则会运行set方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const obj = {
b: 2,
};

Object.defineProperty(obj, 'a', {
get() {
return 1;
},
set(val) {
console.log('set', val);
},
});

console.log(obj.a);
obj.a = 3;

// 1
// set 3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const obj = {
b: 2,
};

Object.defineProperty(obj, 'a', {
get() {
console.log('get')
},
set(val) {
console.log('set', val);
},
});

obj.a = obj.a + 1;

// get
// set NaN

存取器属性存在的最大意义,在于可以控制属性的读取和赋值

Reflect

  1. Reflect是什么?

    Reflect是一个内置的JS对象,它提供了一系列方法,可以让开发者通过调用这些方法,访问一些JS底层功能

    由于它类似于其他语言的反射,因此取名为Reflect

  2. 它可以做什么?

    使用Reflect可以实现诸如属性的赋值与取值,调用普通函数,调用构造函数,判断属性是否在对象中等等功能

  3. 这些功能不是已经存在了吗?为什么还需要Reflect实现一次?

    有一个重要的理念,在ES5就被提出:较少魔法,让代码更加纯粹

    有这种里面很大程度上是受到函数式编程的影响

    ES6进一步贯彻了这种理念,它认为,对属性内存的控制,原型链的修改,函数的调用等等,这些都属于底层实现,属于一种魔法,因此,需要将它们提取处理啊,形成一个正常的API,并高度聚合到某个对象中,于是,就造就了Reflect对象

    因此,你可以看到Reflect对象中的很多API都可以使用过去的某种语法或其他API实现

  4. 它里面到底提供了哪些API呢?

  • Reflect.set(target,propertyKey, value):设置对象target的属性propertyKey的值为value,等同于给对象的属性赋值
  • Reflect.get(target, propertyKey):读取对象target的属性propertyKey的值为,等同于读取对象的属性值
  • Reflect.apply(target, thisArgument, argumentsList):调用一个指定的函数,并绑定this和参数列表.等同于函数调用
  • Reflect.deleteProperty(target, propertyKey): 删除一个对象的属性
  • Reflect.defineProperty(target, propertyKey, attribute):类似于Object.defineProperty,不同的是如果配置出现问题,返回false而不是报错
  • Reflect.construct(target, argumentsList):用构造函数的方式创建一个对象
  • Reflect.has(target, propertyKey):判断一个对象是否拥有一个属性
  • 其他API: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect

Proxy

代理:提供了修改底层实现的方式

1
2
3
new Proxy(target, handler)
//target: 目标对象
// handler: 是一个普通对象,可以重写底层实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const obj = {
a: 1,
b: 2,
};

const proxy = new Proxy(obj, {
set(target, key, value) {
target[key] = value + 1;
},
});

proxy.a = 3;
console.log(proxy.a);
// 4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const obj = {
a: 1,
b: 2,
};

const proxy = new Proxy(obj, {
set(target, key, value) {
console.log(target, key, value);
Reflect.set(target, key, value);
},
});

proxy.a = 3;
console.log(proxy.a);
// 3

应用-观察者模式

有一个对象,是观察者,它用于观察另外一个对象的属性值变化,当属性值变化后会收到一个通知,可能会做一些事情.

在过去,我们可以使用如下模式,但是可能导致空间浪费和数据不一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="container"></div>
<script>
function observer(target) {
const div = document.querySelector('#container');
const ob = {};
const props = Object.keys(target);
for (const prop of props) {
Object.defineProperty(ob, prop, {
get() {
return target[prop];
},
set(val) {
target[prop] = val;
render();
},
enumerable: true,
configurable: true,
});
}
function render() {
let html = '';
for (const prop of Object.keys(ob)) {
html += `<p><span>${prop}</span><span>${ob[prop]}</span></p>`;
}
div.innerHTML = html;
}
return ob;
}
const obj = observer({
a: 1,
b: 2,
});
</script>
</body>
</html>

使用代理可以如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="container"></div>
<script>
function observer(target) {
const div = document.querySelector('#container');
const proxy = new Proxy(target, {
set(target, prop, value) {
Reflect.set(target, prop, value);
render();
},
get(target, prop) {
return Reflect.get(target, prop);
},
});
function render() {
let html = '';
for (const prop of Object.keys(proxy)) {
html += `<p><span>${prop}</span><span>${proxy[prop]}</span></p>`;
}
div.innerHTML = html;
}
return proxy;
}
const obj = observer({
a: 1,
b: 2,
});
</script>
</body>
</html>

应用-偷懒的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class User {
constructor() {}
}

function ConstructorProxy(Class, ...propNames) {
return new Proxy(Class, {
construct(target, argumentsList) {
const obj = Reflect.construct(target, argumentsList);
propNames.forEach((name, i) => {
obj[name] = argumentsList[i];
});
return obj;
},
});
}

const UserProxy = ConstructorProxy(User, 'firstName', 'lastName', 'age');

const user = new UserProxy('张', '三', 18);

console.log(user);

应用-可验证的函数参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function sum(a, b) {
return a + b;
}

function valiadatorFunction(func, ...types) {
const proxy = new Proxy(func, {
apply(target, thisArg, argumentsList) {
types.forEach((type, i) => {
const arg = argumentsList[i];
if (typeof arg !== type) {
throw new TypeError(`Argument ${i} is not ${type}`);
}
});
return Reflect.apply(target, thisArg, argumentsList);
},
});
return proxy;
}

const sumProxy = valiadatorFunction(sum, 'number', 'number');
console.log(sumProxy(1, 2)); // 3
console.log(sumProxy('1', 2)); // TypeError: Argument 0 is not number

增强的数组功能

新增的数组API

静态方法

  • Array.of(...args):使用指定的数组项创建一个新数组
  • Array.from(arg):通过给定的类数组或可迭代对象,创建一个新的数组

实例方法

  • find(callback):用于查找满足条件的第一个元素
  • findIndex(callback):用于查找满足条件的第一个元素的下标
  • fill(data):用指定的数据填充满数组所有的内容
  • copyWithin(target, start?, end?):在数组内部完成复制
  • includes(data)判断数组是否包含某个值,使用sameValueZero比较算法.与Object.is的区别是,sameValueZero认为+0-0相等.

类型化数组

JS中所有的数字,均使用双精度浮点数保存(64bit, 1bit符号位, 11bit阶码, 52位尾数)

类型化数组:用于优化多个数字的存储

具体分为

  • Int8Array: 8位有符号整数
  • UInt8Array: 8位无符号整数
  • Int16Array
  • UInt16Array
  • Int32Array
  • UInt32Array
  1. 如何创建类型化数组
1
2
3
4
5
6
7
const arr = new Int8Array(10);
console.log(arr);

// Int8Array(10) [
// 0, 0, 0, 0, 0,
// 0, 0, 0, 0, 0
// ]
  1. 得到长度
1
2
3
4
5
6
7
8
9
10
11
const arr = new Int8Array(10);
console.log(arr);
console.log(arr.length);
console.log(arr.byteLength);

// Int8Array(10) [
// 0, 0, 0, 0, 0,
// 0, 0, 0, 0, 0
// ]
// 10
// 10
  1. 其他的用法跟普通数组一致,但是:
    • 不能增加和删除数据,类型化数组的长度固定
    • 一些返回数据的方法,返回的数组是同类型化的数组

ArrayBuffer

ArrayBuffer:一个对象,用于存储一块固定内存大小的数据.

1
new ArrayBuffer(字节数)
1
2
3
4
5
6
const obj = new ArrayBuffer(10);
console.log(obj);
// ArrayBuffer {
// [Uint8Contents]: <00 00 00 00 00 00 00 00 00 00>,
// byteLength: 10
// }

可通过属性byteLength得到字节数,可以通过方法slice得到新的ArrayBuffer,

读写ArrayBuffer

  1. 使用DataView
1
2
3
4
5
const obj = new ArrayBuffer(10);
const view = new DataView(obj);

console.log(view.buffer.byteLength);
// 10
  1. 使用类型化数组

实际上,每一个类型化数组都对应一个ArrayBuffer,如果没有手动指定ArrayBuffer,类型化数组创建时,会新建一个ArrayBuffer

制作黑白图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div style="display: flex">
<img src="./image.png" alt="" /><button onclick="change()">转换</button>
<canvas width="366" height="223"></canvas>
</div>
<script>
function change() {
// 将img转换为黑白图像,显示在canvas中
const img = document.querySelector("img");
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");

ctx.drawImage(img, 0, 0);
const imgData = ctx.getImageData(0, 0, img.width, img.height);
for(let i = 0; i < imgData.data.length; i += 4) {
const red = imgData.data[i];
const green = imgData.data[i + 1];
const blue = imgData.data[i + 2];
const alpha = imgData.data[i + 3];

const gray = (red + green + blue) / 3;

imgData.data[i] = gray;
imgData.data[i + 1] = gray;
imgData.data[i+ 2] = gray;
}
ctx.putImageData(imgData, 0, 0);
console.log(imgData);
}
</script>
</body>
</html>