import{_ as s,o as n,c as a,Q as l}from"./chunks/framework.4001cd3b.js";const p="/assets/image-20221118222207332-16687813334481.64935e06.png",o="/assets/image-20221118222311200-16687813941873.8c07f79f.png",e="/assets/image-20221122103111654.29f48e34.png",c="/assets/image-20221122103256116.b7d76f81.png",r="/assets/image-20221122103715428.587185d1.png",t="/assets/image-20221125090752249.baeedc2b.png",E="/assets/image-20221125094148365.3adfbe2d.png",A=JSON.parse('{"title":"JavaScript 高级教程","description":"","frontmatter":{"editLink":false},"headers":[],"relativePath":"note/JavaScriptEnhanced.md","filePath":"note/JavaScriptEnhanced.md","lastUpdated":1697384672000}'),y={name:"note/JavaScriptEnhanced.md"},i=l(`

JavaScript 高级教程

函数中this指向

函数在调用时, Javascript会默认为this绑定一个值

js
// 定义一个函数
function foo() {
  console.log(this)
}

// 1. 直接调用
foo() // Window

// 2. 绑定对象调用
const obj = { name: 'ziu', aaa: foo }
obj.aaa() // obj

// 3. 通过call/apply调用
foo.call('Ziu') // String {'Ziu'}
// 定义一个函数
function foo() {
  console.log(this)
}

// 1. 直接调用
foo() // Window

// 2. 绑定对象调用
const obj = { name: 'ziu', aaa: foo }
obj.aaa() // obj

// 3. 通过call/apply调用
foo.call('Ziu') // String {'Ziu'}

this的绑定:

this始终指向最后调用它的对象

js
function foo() {
  console.log(this)
}
foo() // Window

const obj = {
  name: 'ziu',
  bar: function () {
    console.log(this)
  }
}
obj.bar() // obj

const baz = obj.bar
baz() // Window
function foo() {
  console.log(this)
}
foo() // Window

const obj = {
  name: 'ziu',
  bar: function () {
    console.log(this)
  }
}
obj.bar() // obj

const baz = obj.bar
baz() // Window

如何改变this的指向

new 实例化一个函数

new一个对象时发生了什么:

  1. 创建一个空对象
  2. 这个空对象会被执行prototype连接
  3. 将this指向这个空对象
  4. 执行函数体中的代码
  5. 没有显式返回这个对象时 会默认返回这个对象

函数可以作为一个构造函数, 作为一个类, 可以通过new关键字将其实例化

js
function foo() {
  console.log(this)
  this.name = 'Ziu'
}
foo() // 直接调用的话 this为Window

new foo() // 通过new关键字调用 则this指向空对象
function foo() {
  console.log(this)
  this.name = 'Ziu'
}
foo() // 直接调用的话 this为Window

new foo() // 通过new关键字调用 则this指向空对象

使用 call apply bind

在 JavaScript 中, 函数是对象。

JavaScript 函数有它的属性和方法。call() 和 apply() 是预定义的函数方法。

两个方法可用于调用函数,两个方法的第一个参数必须是对象本身


要将foo函数中的this指向obj,可以通过赋值的方式:

js
obj.foo = foo // 绑定
obj.foo() // 调用
obj.foo = foo // 绑定
obj.foo() // 调用

但是也可以通过对函数调用call / apply实现

js
var obj = {
  name: 'Ziu'
}

function foo() {
  console.log(this)
}

foo.call(obj) // 将foo执行时的this显式绑定到了obj上 并调用foo
foo.call(123) // foo的this被绑定到了 Number { 123 } 上
foo.call("ziu") // 绑定到了 String { "ziu" } 上
var obj = {
  name: 'Ziu'
}

function foo() {
  console.log(this)
}

foo.call(obj) // 将foo执行时的this显式绑定到了obj上 并调用foo
foo.call(123) // foo的this被绑定到了 Number { 123 } 上
foo.call("ziu") // 绑定到了 String { "ziu" } 上

包装类对象

当我们直接使用类似:

js
"ZiuChen".length // String.length
"ZiuChen".length // String.length

的语句时,JS会为字符串 ZiuChen 包装一个对象,随后在这个对象上调用 .length 属性

call和apply的区别

js
function foo(name, age, height) {
  console.log(this)
}

foo('Ziu', 18, 1.88)

foo.apply('targetThis', ['Ziu', 18, 1.88])

foo.call('targetThis', 'Ziu', 18, 1.88)
function foo(name, age, height) {
  console.log(this)
}

foo('Ziu', 18, 1.88)

foo.apply('targetThis', ['Ziu', 18, 1.88])

foo.call('targetThis', 'Ziu', 18, 1.88)

当我们需要给一个带参数的函数通过call/apply的方式绑定this时,就需要使用到call/apply的第二个入参了。

参数列表

当传入函数的参数有多个时,可以通过...args将参数合并到一个数组中去

js
function foo(...args) {
  console.log(args)
}

foo("Ziu", 18, 1.88) // ["Ziu", 18, 1.88]
function foo(...args) {
  console.log(args)
}

foo("Ziu", 18, 1.88) // ["Ziu", 18, 1.88]

bind绑定

如果我们希望:在每次调用foo时,都能将obj绑定到foothis上,那么就需要用到bind

js
// 将obj绑定到foo上
const fun = foo.bind(obj)
// 在后续每次调用foo时, foo内的this都将指向obj
fun() // obj
fun() // obj
// 将obj绑定到foo上
const fun = foo.bind(obj)
// 在后续每次调用foo时, foo内的this都将指向obj
fun() // obj
fun() // obj

bind()方法将创建一个新的函数,当被调用时,将其this关键字

箭头函数

箭头函数是ES6新增的编写函数的方式,更简洁。

箭头函数中的this

在箭头函数中是没有this的:

js
const foo = () => {
  console.log(this)
}
foo() // window
console.log(this) // window
const foo = () => {
  console.log(this)
}
foo() // window
console.log(this) // window

之所以找到了Window对象,是因为在调用foo()时,函数内部作用域并没有找到this,转而向上层作用域找this

因此找到了顶层的全局this,也即Window对象

箭头函数中this的查找规则

检查以下代码:

js
const obj = {
  name: "obj",
  foo: function () {
    const bar = () => {
      console.log(this)
    }
    return bar
  }
}
const fn = obj.foo()
fn() // obj
const obj = {
  name: "obj",
  foo: function () {
    const bar = () => {
      console.log(this)
    }
    return bar
  }
}
const fn = obj.foo()
fn() // obj

代码执行完毕,控制台输出this值为obj对象,这是为什么?

箭头函数中没有this,故会向上层作用域寻找thisbar的上层作用域为函数foo,而函数foothis由其调用决定

调用foo函数的为obj对象,故内部箭头函数中的this指向的是obj

检查以下代码:

js
const obj = {
  name: "obj",
  foo: () => {
    const bar = () => {
      console.log(this)
    }
    return bar
  }
}
const fn = obj.foo()
fn() // Window
const obj = {
  name: "obj",
  foo: () => {
    const bar = () => {
      console.log(this)
    }
    return bar
  }
}
const fn = obj.foo()
fn() // Window

和上面的代码不同之处在于:foo也是由箭头函数定义的,bar向上找不到foothis,故而继续向上,找到了全局this,也即Window对象

严格模式

this面试题

js
var name = 'window'

var person = {
  name: 'person',
  sayName: function () {
    console.log(this.name)
  }
}

function sayName() {
  var sss = person.sayName

  sss() // 默认绑定: window
  person.sayName();  // 隐式绑定: person
  (person.sayName)() // 隐式绑定: person, 本质与上一行代码相同
  ;(person.sayName = person.sayName)() // 间接调用: window
}

sayName()
var name = 'window'

var person = {
  name: 'person',
  sayName: function () {
    console.log(this.name)
  }
}

function sayName() {
  var sss = person.sayName

  sss() // 默认绑定: window
  person.sayName();  // 隐式绑定: person
  (person.sayName)() // 隐式绑定: person, 本质与上一行代码相同
  ;(person.sayName = person.sayName)() // 间接调用: window
}

sayName()
js
var name = 'window'

var person1 = {
  name: 'person1',
  foo1: function () {
    console.log(this.name)
  },
  foo2: () => console.log(this.name),
  foo3: function () {
    return function () {
      console.log(this.name)
    }
  },
  foo4: function () {
    return () => console.log(this.name)
  }
}

var person2 = {
  name: 'person2'
}

person1.foo1() // 隐式绑定: person1
person1.foo1.call(person2) // 显式绑定: person2

person1.foo2() // 上层作用域: window
person1.foo2.call(person2) // 上层作用域: window

person1.foo3()() // 默认绑定: window
person1.foo3.call(person2)() // 默认绑定: window
person1.foo3().call(person2) // 显式绑定: person2

person1.foo4()() // 隐式绑定: person1
person1.foo4.call(person2)() // 显式绑定: person2
person1.foo4().call(person2) // 隐式绑定: person1
var name = 'window'

var person1 = {
  name: 'person1',
  foo1: function () {
    console.log(this.name)
  },
  foo2: () => console.log(this.name),
  foo3: function () {
    return function () {
      console.log(this.name)
    }
  },
  foo4: function () {
    return () => console.log(this.name)
  }
}

var person2 = {
  name: 'person2'
}

person1.foo1() // 隐式绑定: person1
person1.foo1.call(person2) // 显式绑定: person2

person1.foo2() // 上层作用域: window
person1.foo2.call(person2) // 上层作用域: window

person1.foo3()() // 默认绑定: window
person1.foo3.call(person2)() // 默认绑定: window
person1.foo3().call(person2) // 显式绑定: person2

person1.foo4()() // 隐式绑定: person1
person1.foo4.call(person2)() // 显式绑定: person2
person1.foo4().call(person2) // 隐式绑定: person1

原型与继承

JavaScript中,任何一个对象都有一个特殊的内置属性[[prototype]],称之为原型

js
const obj = {
  name: 'Ziu',
  age: 18
}

console.log(obj) // obj
console.log(obj.__proto__) // 由浏览器添加 非标准
console.log(Object.getPrototypeOf(obj)) // 标准的获取原型的方法
const obj = {
  name: 'Ziu',
  age: 18
}

console.log(obj) // obj
console.log(obj.__proto__) // 由浏览器添加 非标准
console.log(Object.getPrototypeOf(obj)) // 标准的获取原型的方法

原型有什么作用?

当我们通过[[getter]]执行 obj.name 时:

手动为obj的原型添加message属性后,在obj上获取name属性,会沿着原型链找到原型上的message属性

js
const proto = Object.getPrototypeOf(obj)
proto.message = 'Hello, Prototype.'
console.log(obj.message) // Hello, Prototype.
const proto = Object.getPrototypeOf(obj)
proto.message = 'Hello, Prototype.'
console.log(obj.message) // Hello, Prototype.

函数的显式原型

之前我们说对象的原型都是隐式的,不能直接通过属性直接获取(.__proto__的方式是非标准)

函数是存在一个名为prototype的显式原型的,可以通过这个属性获取到函数的原型,向原型添加额外的属性

每次在通过new操作符创建对象时,将对象的隐式原型指向这个显式原型

js
function Student(name, age) {
  this.name = name
  this.age = age
}

// 向构造函数的显式原型添加公共方法
Student.prototype.running = function () {
  console.log(\`\${this.name} is running\`)
}

const s1 = new Student('ziu', 18)
const s2 = new Student('kobe', 19)
const s3 = new Student('brant', 20)

// 任何一个由该构造函数创建的对象实例的原型都指向函数的显式原型
// 可以调用原型上的公共方法
s1.running() // ziu is running
s2.running() // kobe is running

// 对象实例的原型上包含构造函数与我们手动添加上去的公共方法
console.log(Object.getPrototypeOf(s1)) // {running: ƒ, constructor: ƒ}
function Student(name, age) {
  this.name = name
  this.age = age
}

// 向构造函数的显式原型添加公共方法
Student.prototype.running = function () {
  console.log(\`\${this.name} is running\`)
}

const s1 = new Student('ziu', 18)
const s2 = new Student('kobe', 19)
const s3 = new Student('brant', 20)

// 任何一个由该构造函数创建的对象实例的原型都指向函数的显式原型
// 可以调用原型上的公共方法
s1.running() // ziu is running
s2.running() // kobe is running

// 对象实例的原型上包含构造函数与我们手动添加上去的公共方法
console.log(Object.getPrototypeOf(s1)) // {running: ƒ, constructor: ƒ}

添加在对象原型上的方法,在被多个实例调用时只会开辟一块内存空间

如果将公共方法放到构造函数中,那么每创建一个实例,都会为这个方法开辟一块新的内存空间

构造函数的显式原型中的属性constructor,这个属性即指向构造函数,因此存在关系Student --proto-> {constructor: Student}

Object的原型

当我们定义了一个对象,它的原型即为Object,而Object的原型则为Null

js
const obj = {
  name: 'Ziu'
}

const p1 = Object.getPrototypeOf(obj) // Object
const p2 = Object.getPrototypeOf(p1) // null

console.log(p1)
console.log(p2)
const obj = {
  name: 'Ziu'
}

const p1 = Object.getPrototypeOf(obj) // Object
const p2 = Object.getPrototypeOf(p1) // null

console.log(p1)
console.log(p2)

以上一节的例子举例:

js
// s1是构造函数Student的实例
const p1 = Object.getPrototypeOf(s1)
const p2 = Object.getPrototypeOf(p1)
const p3 = Object.getPrototypeOf(p2)

console.log(Student.prototype) // {running: ƒ, constructor: ƒ}
console.log(p1) // {running: ƒ, constructor: ƒ}
console.log(p2) // Object
console.log(p3) // null
// s1是构造函数Student的实例
const p1 = Object.getPrototypeOf(s1)
const p2 = Object.getPrototypeOf(p1)
const p3 = Object.getPrototypeOf(p2)

console.log(Student.prototype) // {running: ƒ, constructor: ƒ}
console.log(p1) // {running: ƒ, constructor: ƒ}
console.log(p2) // Object
console.log(p3) // null

原型链实现继承

创建两个构造函数PersonStudent,各自有自身的方法,我们希望实现Student继承Person的方法

js
function Person(name, age) {
  this.name = name
  this.age = age
}

Person.prototype.running = function () {
  console.log(\`\${this.name} is running.\`)
}

function Student(name, age, id, score) {
  this.name = name
  this.age = age
  this.id = id
  this.score = score
}

Student.prototype.studying = function () {
  console.log(\`\${this.name} is studying.\`)
}

const s1 = new Student('Ziu', 18, 2, 60)
s1.running() // ERROR: s1.running is not a function
function Person(name, age) {
  this.name = name
  this.age = age
}

Person.prototype.running = function () {
  console.log(\`\${this.name} is running.\`)
}

function Student(name, age, id, score) {
  this.name = name
  this.age = age
  this.id = id
  this.score = score
}

Student.prototype.studying = function () {
  console.log(\`\${this.name} is studying.\`)
}

const s1 = new Student('Ziu', 18, 2, 60)
s1.running() // ERROR: s1.running is not a function

显然现在StudentPerson是没有任何关联的,要让二者联系起来有以下几种方法:

方法一(错误)

js
Student.prototype = Person.prototype
Student.prototype = Person.prototype

可以让Student构造函数的显式原型指向Person的显式原型,这时可以顺利在s1实例上调用.running()方法,但是存在问题:

方法二(有效但不合理)

创建一个父类的实例对象(new Person())用这个实例对象作为子类的原型对象

js
const p = new Person()
Student.prototype = p
const p = new Person()
Student.prototype = p

创建一个p对象,其隐式原型__proto__指向Person的显式原型对象上

通过这个方法,Student添加的studying方法将被放到p对象上,此后由Student派生的类,其隐式原型也将被指定到p对象上,而不会污染原始的原型

但是当我们会发现,两个构造函数的内容是有重复的:

js
function Person(name, age) {
  this.name = name
  this.age = age
}
function Student(name, age, id, score) {
  this.name = name
  this.age = age
  this.id = id
  this.score = score
}
function Person(name, age) {
  this.name = name
  this.age = age
}
function Student(name, age, id, score) {
  this.name = name
  this.age = age
  this.id = id
  this.score = score
}

针对nameage的定义完全可以被复用

方法三(最终)

借用构造函数方法

js
function Student(name, age, id, score) {
  Person.call(this, name, age) // 通过Person.call 绑定this到当前的s实例上
  this.id = id
  this.score = score
}

Student.prototype = Person.prototype

const s = new Student('ziu', 18, 2, 60)
s.running()
function Student(name, age, id, score) {
  Person.call(this, name, age) // 通过Person.call 绑定this到当前的s实例上
  this.id = id
  this.score = score
}

Student.prototype = Person.prototype

const s = new Student('ziu', 18, 2, 60)
s.running()

创建原型对象的方法

js
function Person(name, age, height) {}
function Student() {}
function Person(name, age, height) {}
function Student() {}

要实现Student对Person的继承:

之前我们通过new关键字,为Student创建一个新的原型对象,也介绍了这种方法存在的弊端

js
const p = new Person()
Student.prototype = p.prototype
const p = new Person()
Student.prototype = p.prototype

可以使用另一种更优秀的方案:

js
var obj = {}
// obj.__proto__ = Person.prototype // 非规范用法 兼容性不保证
Object.setPrototypeOf(obj, Person.prototype) // 挂载原型对象
Student.prototype = obj
var obj = {}
// obj.__proto__ = Person.prototype // 非规范用法 兼容性不保证
Object.setPrototypeOf(obj, Person.prototype) // 挂载原型对象
Student.prototype = obj

社区中也有一种方案:

不需要再new Person(),而是用另外一个构造函数,此后需要挂载原型时都通过new F()来实现

js
function F() {}
F.prototype = Person.prototype
Student.prototype = new F()
function F() {}
F.prototype = Person.prototype
Student.prototype = new F()

最终方案:

从之前的代码可知,要达到的目的有两个:1. 创建一个新对象 让原型对象不要被污染 2. 将新对象的原型挂载到目标原型上

js
var obj = Object.create(Person.prototype)
Student.prototype = obj
var obj = Object.create(Person.prototype)
Student.prototype = obj

Object.create()传入的参数是原型对象,可以在创建新对象的同时将此对象的原型指向目标原型对象上

开发封装

根据上述最终方案的原理,在开发中可以对继承方案做如下封装:

js
function inherit(subType, superType) {
  // 创建一个新对象并将新对象的原型指向父类构造函数的原型
  subType.prototype = Object.create(superType.prototype)

  // 为子类的原型添加constructor属性
  Object.defineProperty(subType.prototype, 'constructor', {
    enumerable: false, // 不可被枚举
    configurable: true, // 值可被修改
    writable: true, // 值可被覆写
    value: subType // 初始值: 父类构造函数
  })
}

inherit(Student, Person)
function inherit(subType, superType) {
  // 创建一个新对象并将新对象的原型指向父类构造函数的原型
  subType.prototype = Object.create(superType.prototype)

  // 为子类的原型添加constructor属性
  Object.defineProperty(subType.prototype, 'constructor', {
    enumerable: false, // 不可被枚举
    configurable: true, // 值可被修改
    writable: true, // 值可被覆写
    value: subType // 初始值: 父类构造函数
  })
}

inherit(Student, Person)

另外,如果担心Object.create()的兼容性,也可以将创建对象的代码替换为:

js
function createObject(o) {
  function F() {}
  F.prototype = o
  return new F()
}
...
  subType.prototype = createObject(superType.prototype)
...
function createObject(o) {
  function F() {}
  F.prototype = o
  return new F()
}
...
  subType.prototype = createObject(superType.prototype)
...

也可以实现:创建对象并修改新对象的原型指向

对象方法补充

js
const info = {
  name: 'Ziu',
  age: 18
}
const obj = createObject(info) // 创建一个新对象并将新对象的原型指向info
console.log(obj.name) // Ziu: 对象自身上并没有该属性 会沿着原型链找到info对象
console.log(obj.hasOwnProperty('name')) // false: 因为对象身上并没有该属性
const info = {
  name: 'Ziu',
  age: 18
}
const obj = createObject(info) // 创建一个新对象并将新对象的原型指向info
console.log(obj.name) // Ziu: 对象自身上并没有该属性 会沿着原型链找到info对象
console.log(obj.hasOwnProperty('name')) // false: 因为对象身上并没有该属性

以上文中例子举例,用in操作符可以沿着原型链获取属性:

for-in遍历的不只是自己身上的属性,也包括原型上的属性,因为对象都是继承自Object,而Object中的属性其属性描述符enumerable默认为false,故自己创建的对象在遍历时不会遍历到Object对象上的属性

js
console.log('name' in obj) // true: 对象上没有 则沿着原型链找到原型上的属性
for (const key in obj) {
  console.log(key) // name age
}
console.log('name' in obj) // true: 对象上没有 则沿着原型链找到原型上的属性
for (const key in obj) {
  console.log(key) // name age
}
js
function Person() {}
function Student() {}

inherit(Student, Person) // 将Student的隐式原型指向Person的隐式原型(继承)

const s = new Student()
console.log(s instanceof Student) // true
console.log(s instanceof Person) // true
console.log(s instanceof Object) // true
console.log(s instanceof Array) // false
function Person() {}
function Student() {}

inherit(Student, Person) // 将Student的隐式原型指向Person的隐式原型(继承)

const s = new Student()
console.log(s instanceof Student) // true
console.log(s instanceof Person) // true
console.log(s instanceof Object) // true
console.log(s instanceof Array) // false

instanceof会沿着原型链查找:s -> Student -> Person -> Object

js
const s = new Student()
console.log(Student.prototype.isPrototypeOf(s)) // true
const s = new Student()
console.log(Student.prototype.isPrototypeOf(s)) // true

解读原型继承关系图

JavaScript Object Layout

在解读之前首先明确以下几点:

由上至下解读,首先解读function Foo()这一层:

通过构造函数new Foo()创建了两个实例对象f1 f2f1 f2是由function Foo创建出来的,故其隐式原型指向Foo.prototype

Foo既是一个函数,也是一个对象(由new Function()创建出来的)。

Foo.prototype作为一个原型对象,拥有他的构造函数constructor指向Foo,也拥有它的隐式原型__proto__指向Object.prototype(本质上Foo.prototype也是由new Object()创建出来的)

随后,开始解读function Object()这第二层:

通过构造函数new Object()创建出来两个实例对象o1 o2,它们是由function Object创建出来的,故其隐式原型指向Object.prototype

同样的,function Object作为构造函数对象,它既拥有作为函数的显式原型对象,也拥有作为对象的隐式原型对象

需要注意的是,原型对象Object.prototype的隐式原型指向null,它的构造函数constructor指向function Object

继续解读function Function()第三层:

JavaScript中的函数对象都是通过function Function()这个构造函数创建出来的,所以所有函数的隐式原型__proto__都指向Function.prototype

构造函数的类方法

需要区分一下类方法与实例方法:

在下面的例子中,我们向Person.prototype上添加了方法running,当我们在实例对象p上调用running时,会沿着p p.__proto__(Person.prototype)查找,在Person.prototype上找到running方法后进行调用

然而我们在Person类上直接调用running()则不行,这是因为Person只是Person.prototypeconstructor属性,是它的构造函数,与原型链没有关系,Person上并没有running()方法,Person.prototype上才有

js
function Person(name) {
  this.name = name
}
Person.prototype.running = function () {
  console.log(\`\${this.name} is running.\`)
}
const p = new Person('Ziu')
p.running() // Ziu is running
Person.running() // undefined
function Person(name) {
  this.name = name
}
Person.prototype.running = function () {
  console.log(\`\${this.name} is running.\`)
}
const p = new Person('Ziu')
p.running() // Ziu is running
Person.running() // undefined

要实现类方法,只需要直接在构造函数上添加新属性即可,因为构造函数本身也是一个对象:构造函数对象

js
const names = ['abc', 'cba', 'nba', 'mba']
Person.randomPerson = function () {
  const randName = names[Math.floor(Math.random() * names.length)]
  return new Person(randName)
}

const p2 = Person.randomPerson()
console.log(p2)
const names = ['abc', 'cba', 'nba', 'mba']
Person.randomPerson = function () {
  const randName = names[Math.floor(Math.random() * names.length)]
  return new Person(randName)
}

const p2 = Person.randomPerson()
console.log(p2)

上例中,我们手动向Person构造函数对象中添加了类方法randomPerson,可以直接在Person上进行调用

ES6继承

ES6提供了class定义类的语法糖,其本质的特性是与ES6之前实现继承的方式是一样的

js
class Person {
  constructor(name) {
    this.name = name
  }
  // 实例方法
  running() {
    console.log(\`\${this.name} is running.\`)
  }
  // 静态方法(类方法)
  static randomPerson() {
    console.log('static func: random person')
  }
}

const p = new Person('Ziu') // 根据类创建实例

console.log(Person.prototype === p.__proto__) // true
console.log(Person.prototype.constructor) // class Person ...
console.log(typeof Person) // function
class Person {
  constructor(name) {
    this.name = name
  }
  // 实例方法
  running() {
    console.log(\`\${this.name} is running.\`)
  }
  // 静态方法(类方法)
  static randomPerson() {
    console.log('static func: random person')
  }
}

const p = new Person('Ziu') // 根据类创建实例

console.log(Person.prototype === p.__proto__) // true
console.log(Person.prototype.constructor) // class Person ...
console.log(typeof Person) // function

当我们通过new关键字操作类时,会调用这个constructor函数,并执行以下操作:

  1. 在内存中创建一个新的对象(空对象)
  2. 这个对象内部的[[prototype]]属性(隐式原型__proto__)会被赋值为该类的prototype属性
  3. 构造函数内部的this,会指向创建出来的新对象
  4. 执行构造函数内的代码
  5. 如果构造函数没有返回非空对象,则返回创建出来的新对象

与function的异同

区别在于:定义构造函数与定义实例方法的部分聚合到了一起(高内聚、低耦合)相同的功能如果要用function来实现:

js
function Person(name) {
  this.name = name
}
Person.prototype.running = function () {
  console.log(\`\${this.name} is running.\`)
}
const p = new Person('Ziu')
console.log(Person.prototype === p.__proto__) // true
console.log(Person.prototype.constructor) // function Person ...
console.log(typeof Person) // function
function Person(name) {
  this.name = name
}
Person.prototype.running = function () {
  console.log(\`\${this.name} is running.\`)
}
const p = new Person('Ziu')
console.log(Person.prototype === p.__proto__) // true
console.log(Person.prototype.constructor) // function Person ...
console.log(typeof Person) // function

运行代码可知,几个console.log输出都是相同的,这证明二者的本质是相同的,Class语法只是一种语法糖

二者之间是存在区别的:

定义访问器方法

为对象属性定义访问器(Object.defineProperty()):

js
const obj = {}
let value

Object.defineProperty(obj, 'name', {
  get: function () {
    console.log('name getted')
    return value
  },
  set: function (val) {
    console.log('name setted ' + val)
    value = val
    return value
  }
})

obj.name = 'Ziu' // name setted Ziu
const tmp = obj.name // name getted
const obj = {}
let value

Object.defineProperty(obj, 'name', {
  get: function () {
    console.log('name getted')
    return value
  },
  set: function (val) {
    console.log('name setted ' + val)
    value = val
    return value
  }
})

obj.name = 'Ziu' // name setted Ziu
const tmp = obj.name // name getted

直接在对象中定义访问器:

js
const obj = {
  _name: 'Ziu',
  get name() {
    return this._name
  },
  set name(val) {
    this._name = val
    return true
  }
}
const obj = {
  _name: 'Ziu',
  get name() {
    return this._name
  },
  set name(val) {
    this._name = val
    return true
  }
}

在ES6的class关键字中定义访问器:

js
class Person {
  constructor(name) {
    this._name = name
  }
  get name() {
    return this._name
  }
  set name(val) {
    this._name = val
    return true
  }
}
class Person {
  constructor(name) {
    this._name = name
  }
  get name() {
    return this._name
  }
  set name(val) {
    this._name = val
    return true
  }
}

访问器的应用场景

当我们需要频繁的对某些属性组合进行调用时,可以将这些属性组合,在外部可以通过访问器获得计算好的值

例如下述代码中实现了一个简单的Rectangle类,可以通过访问器直接获取其positionsize属性

js
class Rectangle {
  constructor(x, y, width, height) {
    this.x = x
    this.y = y
    this.width = width
    this.height = height
  }

  get position() {
    return { x: this.x, y: this.y }
  }

  get size() {
    return this.width * this.height
  }
}

const rect = new Rectangle(10, 15, 20, 50)
console.log(rect.position, rect.size)
class Rectangle {
  constructor(x, y, width, height) {
    this.x = x
    this.y = y
    this.width = width
    this.height = height
  }

  get position() {
    return { x: this.x, y: this.y }
  }

  get size() {
    return this.width * this.height
  }
}

const rect = new Rectangle(10, 15, 20, 50)
console.log(rect.position, rect.size)

类的静态方法

在ES6之前我们将这种方法称为类方法,在其之后我们将其称为静态方法

静态方法通常用于定义直接使用类来执行的方法,不需要有类的实例,使用static关键字来定义:

js
class Person {
  constructor(name) {
    this.name = name
  }
  // 实例方法
  running() {
    console.log(\`\${this.name} is running.\`)
  }
  // 静态方法(类方法)
  static randomPerson() {
    console.log('static func: random person')
  }
}
class Person {
  constructor(name) {
    this.name = name
  }
  // 实例方法
  running() {
    console.log(\`\${this.name} is running.\`)
  }
  // 静态方法(类方法)
  static randomPerson() {
    console.log('static func: random person')
  }
}

本质上:

js
Person.randomPerson = function() {
  console.log('static func: random person')
}
Person.randomPerson = function() {
  console.log('static func: random person')
}

extends实现继承

Person类的基础上实现Student类继承自Person

在子类的构造函数内需要通过super()并传入父类(超类)构造函数需要的参数,这样就可以调用父类的构造函数

js
class Person {
  constructor(name) {
    this.name = name
  }
  running() {
    console.log(\`\${this.name} is running.\`)
  }
  sitting() {
    console.log(\`\${this.name} is sitting.\`)
  }
}

class Student extends Person {
  constructor(name, score) {
    super(name) // 在子类的constructor中通过super()调用父类的构造函数
    this.score = score
  }
  studying() {
    super.sitting() // 在子类中通过super.method()调用父类的方法
    console.log(\`\${this.name} is studying.\`)
  }
}

const s = new Student('Ziu', 60)
s.running() // 可以调用父类方法
s.studying() // 也可以调用自己的方法
class Person {
  constructor(name) {
    this.name = name
  }
  running() {
    console.log(\`\${this.name} is running.\`)
  }
  sitting() {
    console.log(\`\${this.name} is sitting.\`)
  }
}

class Student extends Person {
  constructor(name, score) {
    super(name) // 在子类的constructor中通过super()调用父类的构造函数
    this.score = score
  }
  studying() {
    super.sitting() // 在子类中通过super.method()调用父类的方法
    console.log(\`\${this.name} is studying.\`)
  }
}

const s = new Student('Ziu', 60)
s.running() // 可以调用父类方法
s.studying() // 也可以调用自己的方法

在子(派生)类的构造函数中使用this或者返回默认对象之前,必须先通过super调用父类的构造函数

js
...
constructor(name, score) {
  this.score = score
  super(name) // 这是不对的,应该在使用this之前先调用super
}
...
...
constructor(name, score) {
  this.score = score
  super(name) // 这是不对的,应该在使用this之前先调用super
}
...

继承自默认类

可以通过继承,为内置类做修改或扩展,方便我们使用:

在下例中,我们对内置类Array做了扩展,添加了访问器属性lastItem可以获取数组内最后一个元素

js
class SelfArray extends Array {
  get lastItem() {
    return this[this.length - 1]
  }
}

const arr = new SelfArray(5).fill(false)
console.log(arr)
console.log(arr.lastItem)
class SelfArray extends Array {
  get lastItem() {
    return this[this.length - 1]
  }
}

const arr = new SelfArray(5).fill(false)
console.log(arr)
console.log(arr.lastItem)

传统方式也可以实现,通过操作原型:

Array.prototype定义属性lastItem,并为其添加getter函数,可以通过lastItem获取最后一个元素

需要注意的是,这种方法会对所有Array对象产生影响,所有的数组都可以调用这个方法

js
Object.defineProperty(Array.prototype, 'lastItem', {
  get: function () {
    return this[this.length - 1]
  }
})

const arr = new Array(5).fill(false)
console.log(arr.lastItem)
Object.defineProperty(Array.prototype, 'lastItem', {
  get: function () {
    return this[this.length - 1]
  }
})

const arr = new Array(5).fill(false)
console.log(arr.lastItem)

类的混入mixin

JavaScript只支持单继承,不支持多继承。要让一个类继承自多个父类,调用来自不同父类的方法,可以使用mixin

下例中我们希望Bird能够同时继承自AnimalFlyer类,直接使用extends会报错

js
function mixinAnimal(BaseClass) {
  // 1. 创建一个新的类 继承自baseClass
  // 2. 对这个新的类进行扩展 添加running方法
  return class extends BaseClass {
    running() {
      console.log('running')
    }
  }
}
function mixinFlyer(BaseClass) {
  return class extends BaseClass {
    flying() {
      console.log('flying')
    }
  }
}

class Bird {
  eating() {
    console.log('eating')
  }
}
class newBird extends mixinAnimal(mixinFlyer(Bird)) {}

const b = new newBird()
b.eating() // eating
b.running() // running
b.flying() // flying
function mixinAnimal(BaseClass) {
  // 1. 创建一个新的类 继承自baseClass
  // 2. 对这个新的类进行扩展 添加running方法
  return class extends BaseClass {
    running() {
      console.log('running')
    }
  }
}
function mixinFlyer(BaseClass) {
  return class extends BaseClass {
    flying() {
      console.log('flying')
    }
  }
}

class Bird {
  eating() {
    console.log('eating')
  }
}
class newBird extends mixinAnimal(mixinFlyer(Bird)) {}

const b = new newBird()
b.eating() // eating
b.running() // running
b.flying() // flying

通过调用mixin方法,创建一个新的类 继承自baseClass,对这个新的类进行扩展 添加我们希望新类能够继承的方法

除了这种方式,也可以通过复制对象的方法实现继承Object.assign(Bird.prototype, {})

React中的高阶组件

在React的高阶组件实现中有connect方法,即是使用到了类似mixin的方法实现多继承

Babel是如何转化ES6的

Babel可以对更高级的代码进行转义,转义后的代码支持在更低级的浏览器上运行。开发者可以使用高级语法进行开发而不需要担心在低级浏览器上的兼容问题。

例如Babel会将ES6的类(Class)写法转义为ES5的prototype继承式写法

class是如何转化的

假设有以下ES6代码,我们解读通过Babel转义后的ES5代码:

js
class Person {
  constructor(name) {
    this.name = name
  }
  running() {
    console.log(\`\${this.name} is running.\`)
  }
  static randomPerson() {
    return new this('xxx')
  }
}

const p = new Person('Ziu')
class Person {
  constructor(name) {
    this.name = name
  }
  running() {
    console.log(\`\${this.name} is running.\`)
  }
  static randomPerson() {
    return new this('xxx')
  }
}

const p = new Person('Ziu')
js
'use strict'

function _typeof(obj) {
  '@babel/helpers - typeof'
  return (
    (_typeof =
      'function' == typeof Symbol && 'symbol' == typeof Symbol.iterator
        ? function (obj) {
            return typeof obj
          }
        : function (obj) {
            return obj &&
              'function' == typeof Symbol &&
              obj.constructor === Symbol &&
              obj !== Symbol.prototype
              ? 'symbol'
              : typeof obj
          }),
    _typeof(obj)
  )
}
function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError('Cannot call a class as a function')
  }
}
function _defineProperties(target, props) {
  for (var i = 0; i < props.length; i++) {
    var descriptor = props[i]
    descriptor.enumerable = descriptor.enumerable || false
    descriptor.configurable = true
    if ('value' in descriptor) descriptor.writable = true
    Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor)
  }
}
function _createClass(Constructor, protoProps, staticProps) {
  if (protoProps) _defineProperties(Constructor.prototype, protoProps)
  if (staticProps) _defineProperties(Constructor, staticProps)
  Object.defineProperty(Constructor, 'prototype', { writable: false })
  return Constructor
}
function _toPropertyKey(arg) {
  var key = _toPrimitive(arg, 'string')
  return _typeof(key) === 'symbol' ? key : String(key)
}
function _toPrimitive(input, hint) {
  if (_typeof(input) !== 'object' || input === null) return input
  var prim = input[Symbol.toPrimitive]
  if (prim !== undefined) {
    var res = prim.call(input, hint || 'default')
    if (_typeof(res) !== 'object') return res
    throw new TypeError('@@toPrimitive must return a primitive value.')
  }
  return (hint === 'string' ? String : Number)(input)
}
var Person = /*#__PURE__*/ (function () {
  function Person(name) {
    _classCallCheck(this, Person)
    this.name = name
  }
  _createClass(
    Person,
    [
      {
        key: 'running',
        value: function running() {
          console.log(''.concat(this.name, ' is running.'))
        }
      }
    ],
    [
      {
        key: 'randomPerson',
        value: function randomPerson() {
          return new this('xxx')
        }
      }
    ]
  )
  return Person
})()
var p = new Person('Ziu')
'use strict'

function _typeof(obj) {
  '@babel/helpers - typeof'
  return (
    (_typeof =
      'function' == typeof Symbol && 'symbol' == typeof Symbol.iterator
        ? function (obj) {
            return typeof obj
          }
        : function (obj) {
            return obj &&
              'function' == typeof Symbol &&
              obj.constructor === Symbol &&
              obj !== Symbol.prototype
              ? 'symbol'
              : typeof obj
          }),
    _typeof(obj)
  )
}
function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError('Cannot call a class as a function')
  }
}
function _defineProperties(target, props) {
  for (var i = 0; i < props.length; i++) {
    var descriptor = props[i]
    descriptor.enumerable = descriptor.enumerable || false
    descriptor.configurable = true
    if ('value' in descriptor) descriptor.writable = true
    Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor)
  }
}
function _createClass(Constructor, protoProps, staticProps) {
  if (protoProps) _defineProperties(Constructor.prototype, protoProps)
  if (staticProps) _defineProperties(Constructor, staticProps)
  Object.defineProperty(Constructor, 'prototype', { writable: false })
  return Constructor
}
function _toPropertyKey(arg) {
  var key = _toPrimitive(arg, 'string')
  return _typeof(key) === 'symbol' ? key : String(key)
}
function _toPrimitive(input, hint) {
  if (_typeof(input) !== 'object' || input === null) return input
  var prim = input[Symbol.toPrimitive]
  if (prim !== undefined) {
    var res = prim.call(input, hint || 'default')
    if (_typeof(res) !== 'object') return res
    throw new TypeError('@@toPrimitive must return a primitive value.')
  }
  return (hint === 'string' ? String : Number)(input)
}
var Person = /*#__PURE__*/ (function () {
  function Person(name) {
    _classCallCheck(this, Person)
    this.name = name
  }
  _createClass(
    Person,
    [
      {
        key: 'running',
        value: function running() {
          console.log(''.concat(this.name, ' is running.'))
        }
      }
    ],
    [
      {
        key: 'randomPerson',
        value: function randomPerson() {
          return new this('xxx')
        }
      }
    ]
  )
  return Person
})()
var p = new Person('Ziu')

_createClass传入一个构造函数,在_createClass内部:

extends是如何转化的

js
class Person {
  constructor(name) {
    this.name = name
  }
  running() {
    console.log(\`\${this.name} is running.\`)
  }
  static randomPerson() {
    return new this('xxx')
  }
}

class Student extends Person {
  constructor(name, score) {
    super(name)
    this.score = score
  }
  studying() {
    console.log(\`\${this.name} is studying.\`)
  }
  static randomStudent() {
    return new this('xxx', 66)
  }
}

const s = new Student('Kobe', 66)
class Person {
  constructor(name) {
    this.name = name
  }
  running() {
    console.log(\`\${this.name} is running.\`)
  }
  static randomPerson() {
    return new this('xxx')
  }
}

class Student extends Person {
  constructor(name, score) {
    super(name)
    this.score = score
  }
  studying() {
    console.log(\`\${this.name} is studying.\`)
  }
  static randomStudent() {
    return new this('xxx', 66)
  }
}

const s = new Student('Kobe', 66)
js
'use strict'

function _inherits(subClass, superClass) {
  if (typeof superClass !== 'function' && superClass !== null) {
    throw new TypeError('Super expression must either be null or a function')
  }
  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: { value: subClass, writable: true, configurable: true }
  })
  Object.defineProperty(subClass, 'prototype', { writable: false })
  if (superClass) _setPrototypeOf(subClass, superClass)
}
function _setPrototypeOf(o, p) {
  _setPrototypeOf = Object.setPrototypeOf
    ? Object.setPrototypeOf.bind()
    : function _setPrototypeOf(o, p) {
        o.__proto__ = p
        return o
      }
  return _setPrototypeOf(o, p)
}
function _createSuper(Derived) {
  var hasNativeReflectConstruct = _isNativeReflectConstruct()
  return function _createSuperInternal() {
    var Super = _getPrototypeOf(Derived),
      result
    if (hasNativeReflectConstruct) {
      var NewTarget = _getPrototypeOf(this).constructor
      result = Reflect.construct(Super, arguments, NewTarget)
    } else {
      result = Super.apply(this, arguments)
    }
    return _possibleConstructorReturn(this, result)
  }
}
function _possibleConstructorReturn(self, call) {
  if (call && (_typeof(call) === 'object' || typeof call === 'function')) {
    return call
  } else if (call !== void 0) {
    throw new TypeError('Derived constructors may only return object or undefined')
  }
  return _assertThisInitialized(self)
}
function _assertThisInitialized(self) {
  if (self === void 0) {
    throw new ReferenceError("this hasn't been initialised - super() hasn't been called")
  }
  return self
}
function _isNativeReflectConstruct() {
  if (typeof Reflect === 'undefined' || !Reflect.construct) return false
  if (Reflect.construct.sham) return false
  if (typeof Proxy === 'function') return true
  try {
    Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {}))
    return true
  } catch (e) {
    return false
  }
}
function _getPrototypeOf(o) {
  _getPrototypeOf = Object.setPrototypeOf
    ? Object.getPrototypeOf.bind()
    : function _getPrototypeOf(o) {
        return o.__proto__ || Object.getPrototypeOf(o)
      }
  return _getPrototypeOf(o)
}
function _typeof(obj) {
  '@babel/helpers - typeof'
  return (
    (_typeof =
      'function' == typeof Symbol && 'symbol' == typeof Symbol.iterator
        ? function (obj) {
            return typeof obj
          }
        : function (obj) {
            return obj &&
              'function' == typeof Symbol &&
              obj.constructor === Symbol &&
              obj !== Symbol.prototype
              ? 'symbol'
              : typeof obj
          }),
    _typeof(obj)
  )
}
function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError('Cannot call a class as a function')
  }
}
function _defineProperties(target, props) {
  for (var i = 0; i < props.length; i++) {
    var descriptor = props[i]
    descriptor.enumerable = descriptor.enumerable || false
    descriptor.configurable = true
    if ('value' in descriptor) descriptor.writable = true
    Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor)
  }
}
function _createClass(Constructor, protoProps, staticProps) {
  if (protoProps) _defineProperties(Constructor.prototype, protoProps)
  if (staticProps) _defineProperties(Constructor, staticProps)
  Object.defineProperty(Constructor, 'prototype', { writable: false })
  return Constructor
}
function _toPropertyKey(arg) {
  var key = _toPrimitive(arg, 'string')
  return _typeof(key) === 'symbol' ? key : String(key)
}
function _toPrimitive(input, hint) {
  if (_typeof(input) !== 'object' || input === null) return input
  var prim = input[Symbol.toPrimitive]
  if (prim !== undefined) {
    var res = prim.call(input, hint || 'default')
    if (_typeof(res) !== 'object') return res
    throw new TypeError('@@toPrimitive must return a primitive value.')
  }
  return (hint === 'string' ? String : Number)(input)
}
var Person = /*#__PURE__*/ (function () {
  function Person(name) {
    _classCallCheck(this, Person)
    this.name = name
  }
  _createClass(
    Person,
    [
      {
        key: 'running',
        value: function running() {
          console.log(''.concat(this.name, ' is running.'))
        }
      }
    ],
    [
      {
        key: 'randomPerson',
        value: function randomPerson() {
          return new this('xxx')
        }
      }
    ]
  )
  return Person
})()
var Student = /*#__PURE__*/ (function (_Person) {
  _inherits(Student, _Person)
  var _super = _createSuper(Student)
  function Student(name, score) {
    var _this
    _classCallCheck(this, Student)
    _this = _super.call(this, name)
    _this.score = score
    return _this
  }
  _createClass(
    Student,
    [
      {
        key: 'studying',
        value: function studying() {
          console.log(''.concat(this.name, ' is studying.'))
        }
      }
    ],
    [
      {
        key: 'randomStudent',
        value: function randomStudent() {
          return new this('xxx', 66)
        }
      }
    ]
  )
  return Student
})(Person)
var s = new Student('Kobe', 66)
'use strict'

function _inherits(subClass, superClass) {
  if (typeof superClass !== 'function' && superClass !== null) {
    throw new TypeError('Super expression must either be null or a function')
  }
  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: { value: subClass, writable: true, configurable: true }
  })
  Object.defineProperty(subClass, 'prototype', { writable: false })
  if (superClass) _setPrototypeOf(subClass, superClass)
}
function _setPrototypeOf(o, p) {
  _setPrototypeOf = Object.setPrototypeOf
    ? Object.setPrototypeOf.bind()
    : function _setPrototypeOf(o, p) {
        o.__proto__ = p
        return o
      }
  return _setPrototypeOf(o, p)
}
function _createSuper(Derived) {
  var hasNativeReflectConstruct = _isNativeReflectConstruct()
  return function _createSuperInternal() {
    var Super = _getPrototypeOf(Derived),
      result
    if (hasNativeReflectConstruct) {
      var NewTarget = _getPrototypeOf(this).constructor
      result = Reflect.construct(Super, arguments, NewTarget)
    } else {
      result = Super.apply(this, arguments)
    }
    return _possibleConstructorReturn(this, result)
  }
}
function _possibleConstructorReturn(self, call) {
  if (call && (_typeof(call) === 'object' || typeof call === 'function')) {
    return call
  } else if (call !== void 0) {
    throw new TypeError('Derived constructors may only return object or undefined')
  }
  return _assertThisInitialized(self)
}
function _assertThisInitialized(self) {
  if (self === void 0) {
    throw new ReferenceError("this hasn't been initialised - super() hasn't been called")
  }
  return self
}
function _isNativeReflectConstruct() {
  if (typeof Reflect === 'undefined' || !Reflect.construct) return false
  if (Reflect.construct.sham) return false
  if (typeof Proxy === 'function') return true
  try {
    Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {}))
    return true
  } catch (e) {
    return false
  }
}
function _getPrototypeOf(o) {
  _getPrototypeOf = Object.setPrototypeOf
    ? Object.getPrototypeOf.bind()
    : function _getPrototypeOf(o) {
        return o.__proto__ || Object.getPrototypeOf(o)
      }
  return _getPrototypeOf(o)
}
function _typeof(obj) {
  '@babel/helpers - typeof'
  return (
    (_typeof =
      'function' == typeof Symbol && 'symbol' == typeof Symbol.iterator
        ? function (obj) {
            return typeof obj
          }
        : function (obj) {
            return obj &&
              'function' == typeof Symbol &&
              obj.constructor === Symbol &&
              obj !== Symbol.prototype
              ? 'symbol'
              : typeof obj
          }),
    _typeof(obj)
  )
}
function _classCallCheck(instance, Constructor) {
  if (!(instance instanceof Constructor)) {
    throw new TypeError('Cannot call a class as a function')
  }
}
function _defineProperties(target, props) {
  for (var i = 0; i < props.length; i++) {
    var descriptor = props[i]
    descriptor.enumerable = descriptor.enumerable || false
    descriptor.configurable = true
    if ('value' in descriptor) descriptor.writable = true
    Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor)
  }
}
function _createClass(Constructor, protoProps, staticProps) {
  if (protoProps) _defineProperties(Constructor.prototype, protoProps)
  if (staticProps) _defineProperties(Constructor, staticProps)
  Object.defineProperty(Constructor, 'prototype', { writable: false })
  return Constructor
}
function _toPropertyKey(arg) {
  var key = _toPrimitive(arg, 'string')
  return _typeof(key) === 'symbol' ? key : String(key)
}
function _toPrimitive(input, hint) {
  if (_typeof(input) !== 'object' || input === null) return input
  var prim = input[Symbol.toPrimitive]
  if (prim !== undefined) {
    var res = prim.call(input, hint || 'default')
    if (_typeof(res) !== 'object') return res
    throw new TypeError('@@toPrimitive must return a primitive value.')
  }
  return (hint === 'string' ? String : Number)(input)
}
var Person = /*#__PURE__*/ (function () {
  function Person(name) {
    _classCallCheck(this, Person)
    this.name = name
  }
  _createClass(
    Person,
    [
      {
        key: 'running',
        value: function running() {
          console.log(''.concat(this.name, ' is running.'))
        }
      }
    ],
    [
      {
        key: 'randomPerson',
        value: function randomPerson() {
          return new this('xxx')
        }
      }
    ]
  )
  return Person
})()
var Student = /*#__PURE__*/ (function (_Person) {
  _inherits(Student, _Person)
  var _super = _createSuper(Student)
  function Student(name, score) {
    var _this
    _classCallCheck(this, Student)
    _this = _super.call(this, name)
    _this.score = score
    return _this
  }
  _createClass(
    Student,
    [
      {
        key: 'studying',
        value: function studying() {
          console.log(''.concat(this.name, ' is studying.'))
        }
      }
    ],
    [
      {
        key: 'randomStudent',
        value: function randomStudent() {
          return new this('xxx', 66)
        }
      }
    ]
  )
  return Student
})(Person)
var s = new Student('Kobe', 66)

创建Student类的代码仍然是一个纯函数:

Student内部通过调用_inherits,传入StudentPerson,将二者之间做了关联:

实现继承后,通过_createClass创建类,相关内容不再重复叙述(挂载实例方法、类方法、prototype属性)

至此完成了Student类的创建,随后我们通过var s = new Student(...)创建一个Student实例

自己实现的ES5对比

在之前的内容中我们自己实现了相关的类定义/继承功能

js
function Person(name) {
  this.name = name // 定义内部属性
}
// 定义实例方法
Person.prototype.running = function() {
  console.log(\`\${this.name} is running.\`)
}
// 定义类方法(静态方法)
Person.randomPerson = function() {
  return new this('xxx')
}
function Person(name) {
  this.name = name // 定义内部属性
}
// 定义实例方法
Person.prototype.running = function() {
  console.log(\`\${this.name} is running.\`)
}
// 定义类方法(静态方法)
Person.randomPerson = function() {
  return new this('xxx')
}

自己实现的继承:

js
function inherit(SubClass, SuperClass) {
  // 创建一个新对象并将新对象的原型指向父类构造函数的原型
  subType.prototype = Object.create(superType.prototype)

  // 为子类的原型添加constructor属性
  Object.defineProperty(subType.prototype, 'constructor', {
    enumerable: false, // 不可被枚举
    configurable: true, // 值可被修改
    writable: true, // 值可被覆写
    value: subType // 初始值: 父类构造函数
  })
}

inherit(Student, Person)
function inherit(SubClass, SuperClass) {
  // 创建一个新对象并将新对象的原型指向父类构造函数的原型
  subType.prototype = Object.create(superType.prototype)

  // 为子类的原型添加constructor属性
  Object.defineProperty(subType.prototype, 'constructor', {
    enumerable: false, // 不可被枚举
    configurable: true, // 值可被修改
    writable: true, // 值可被覆写
    value: subType // 初始值: 父类构造函数
  })
}

inherit(Student, Person)

相比之下不难发现,Babel转义用ES5实现相关功能的核心思想是一致的,只不过Babel转义后的代码添加了更多的边界条件的判定。

浏览器运行原理

网页解析过程

输入域名 => DNS解析为IP => 目标服务器返回index.html

DNS:Domain Name System

HTML解析过程

浏览器解析HTML过程浏览器是和如何工作的

How browsers work

生成CSS规则

在解析的过程中,如果遇到<link>元素,那么会由浏览器负责下载对应的CSS文件

浏览器下载完CSS文件后,就会对CSS文件进行解析,解析出对应的规则树:

构建Render Tree

有了DOM Tree和CSSOM Tree之后,就可以将二者结合,构建Render Tree了

此时,如果有某些元素的CSS属性display: none;那么这个元素就不会出现在Render Tree中

布局和绘制(Layout & Paint)

第四步是在渲染树(Render Tree)上运行布局(Layout),以计算每个节点的几何体

第五步是将每个节点绘制(Paint)到屏幕上

回流和重绘(Reflow & )

回流也可称为重排

理解回流(Reflow):

什么情况下会引起回流?

理解重绘(Repaint):

什么情况下会引起重绘?

回流一定会引起重绘,所以回流是一件很消耗性能的事情

特殊解析: composite合成

在绘制的过程中,可以将布局后的元素绘制到多个合成图层中

标准流 => LayouTree => RenderLayer
\`position:fixed;\` => RenderLayer
标准流 => LayouTree => RenderLayer
\`position:fixed;\` => RenderLayer

默认情况,标准流中的内容都是被绘制在同一个图层(Layer)中的

而一些特殊的属性,浏览器会创建一个新的合成层(CompositingLayer),并且新的图层可以利用GPU来加速绘制

当元素具有哪些属性时,浏览器会为其创建新的合成层呢?

案例1:同一层渲染
css
.box1 {
  width: 100px;
  height: 100px;
  background-color: red;
}
.box2 {
  width: 100px;
  height: 100px;
  background-color: blue;
}
.box1 {
  width: 100px;
  height: 100px;
  background-color: red;
}
.box2 {
  width: 100px;
  height: 100px;
  background-color: blue;
}
html
<body>
  <div class="box1"></div>
  <div class="box2"></div>
</body>
<body>
  <div class="box1"></div>
  <div class="box2"></div>
</body>

在开发者工具的图层工具中可以看到,两个元素.box1.box2都是在一个层(Document)下渲染的:

image-20221122103111654

案例2:分层渲染

当我们为.box2添加上position: fixed;属性,这时.box2将在由浏览器创建出来的合成层,分层单独渲染

css
.box2 {
  width: 100px;
  height: 100px;
  background-color: blue;
  position: fixed;
}
.box2 {
  width: 100px;
  height: 100px;
  background-color: blue;
  position: fixed;
}

image-20221122103256116

案例3:transform 3D

为元素添加上transform属性时,浏览器也会为对应元素创建一个合成层,需要注意的是:只有3D的变化浏览器才会创建

如果是translateXtranslateY则不会

css
.box2 {
  width: 100px;
  height: 100px;
  background-color: blue;
  /* position: fixed; */
  transform: translateZ(10px);
}
.box2 {
  width: 100px;
  height: 100px;
  background-color: blue;
  /* position: fixed; */
  transform: translateZ(10px);
}

image-20221122103715428

案例4:transition+transform

当我们为元素添加上动画时,动画的中间执行过程的渲染会在新的图层上进行,但是中间动画渲染完成后,结果会回到原始图层上

css
.box2 {
  width: 100px;
  height: 100px;
  background-color: blue;
  transition: transform 0.5s ease;
}
.box2:hover {
  transform: translateY(10px);
}
.box2 {
  width: 100px;
  height: 100px;
  background-color: blue;
  transition: transform 0.5s ease;
}
.box2:hover {
  transform: translateY(10px);
}
案例5:transition+opacity

transform类似,使用transition过渡的opacity动画,浏览器也会为其创建一个合成层

css
.box2 {
  width: 100px;
  height: 100px;
  background-color: blue;
  opacity: 1;
  transition: opacity 0.5s ease;
}
.box2:hover {
  opacity: 0.2;
}
.box2 {
  width: 100px;
  height: 100px;
  background-color: blue;
  opacity: 1;
  transition: opacity 0.5s ease;
}
.box2:hover {
  opacity: 0.2;
}
总结

分层确实可以提高性能,但是它是以内存管理为代价的,因此不应当作为Web性能优化策略的一部分过度使用

浏览器对script元素的处理

之前我们说到,在解析到link标签时,浏览器会异步下载其中的css文件,并在DOM树构建完成后,将其与CSS Tree合成为RenderTree

但是当浏览器解析到script标签时,整个解析过程将被阻塞,当前script标签后面的DOM树将停止解析,直到当前script代码被下载、解析、执行完毕,才会继续解析HTML,构建DOM树

为什么要这样做呢?

这也会带来新的问题,比如在现代的页面开发中:

为了解决这个问题,浏览器的script标签为我们提供了两个属性(attribute):deferasync

defer属性

defer 即推迟,为script标签添加这个属性,相当于告诉浏览器:不要等待此脚本下载,而是继续解析HTML,构建DOM Tree

html
<script>
  console.log('script enter')
  window.addEventListener('DOMContentLoaded', () => {
    console.log('DOMContentLoaded enter')
  })
</script>
<script src="./defer.js" defer></script>
<script>
  console.log('script enter')
  window.addEventListener('DOMContentLoaded', () => {
    console.log('DOMContentLoaded enter')
  })
</script>
<script src="./defer.js" defer></script>
js
// defer.js
console.log('defer script enter')
// defer.js
console.log('defer script enter')

上述代码在控制台的输出为:

script enter
defer script enter
DOMContentLoaded enter
script enter
defer script enter
DOMContentLoaded enter

async属性

async属性也可以做到:让脚本异步加载而不阻塞DOM树的构建,它与defer的区别:

要使用async属性标记的script操作DOM,必须在其中使用DOMContentLoaded监听器的回调函数,在该事件触发(DOM树构建完毕)后,执行相应的回调函数

JavaScript 运行原理

JS代码的执行

JavaScript代码下载好后,是如何一步步被执行的?

浏览器内核是由两部分组成的,以Webkit为例:

JavaScript V8引擎

image-20221125090752249

JS源代码经过解析,生成抽象语法树(词法分析器、语法分析器),经过ignition转为字节码(二进制、跨平台),即可由CPU执行

在ignition的过程中,会由TurboFan收集类型信息(检查哪些函数是被重复执行的),对优化生成的机器码,得到性能更高的机器指令

如果生成的优化的机器码不符合函数实际执行,则会进行Deoptimization(反优化),降级回普通的字节码

image-20221125094148365

Blink 获取到源码 => 转为Stream => Scanner扫描器

JavaScript代码执行过程

js
console.log('script start')

function fun1() {
  console.log('fun1')
}

function fun2() {
  console.log('fun2 start')
  fun1()
  console.log('fun2 end')
}

fun2()

console.log('script end')
console.log('script start')

function fun1() {
  console.log('fun1')
}

function fun2() {
  console.log('fun2 start')
  fun1()
  console.log('fun2 end')
}

fun2()

console.log('script end')

创建全局执行上下文 => 函数执行到fun2() => 创建fun2执行上下文 => 函数执行到fun1() => 创建fun1执行上下文 => fun1执行完毕出栈 => 继续执行fun2后续代码 => fun2执行完毕出栈 => 继续执行全局上下文后续代码 => 结束

sh
'script start'
'fun2 start'
'fun1'
'fun2 end'
'script end'
'script start'
'fun2 start'
'fun1'
'fun2 end'
'script end'

Proxy与Reflect

监听对象方法

Proxy监听对象

可以使用Proxy对象将原对象包裹,此后的操作都对proxy进行,每次getset被触发时都会自动执行相应代码

js
const obj = {
  name: 'ziu',
  age: 18,
}
const proxy = new Proxy(obj, {
  // 需要注意的是 get 与 set 都需要有返回值
  get(target, key) {
    console.log('get', key)
    return target[key] // 默认行为是返回属性值
  },
  set(target, key, value) {
    console.log('set', key, value)
    target[key] = value
    return true // 代表执行成功
  }
})
const obj = {
  name: 'ziu',
  age: 18,
}
const proxy = new Proxy(obj, {
  // 需要注意的是 get 与 set 都需要有返回值
  get(target, key) {
    console.log('get', key)
    return target[key] // 默认行为是返回属性值
  },
  set(target, key, value) {
    console.log('set', key, value)
    target[key] = value
    return true // 代表执行成功
  }
})
js
const tmp = proxy.age // getter被触发
proxy.name = 'Ziu' // setter被触发
const tmp = proxy.age // getter被触发
proxy.name = 'Ziu' // setter被触发

defineProperty监听对象

除此之外,可以通过Object.defineProperty为对象中某个属性设置gettersetter函数,可以达到类似的效果

需要注意的是,此处要用for-in遍历对象的所有属性,并逐个为其设置gettersetter

js
for (const key of Object.keys(obj)) {
  let value = obj[key]
  Object.defineProperty(obj, key, {
    get() {
      console.log('get', value)
      return value
    },
    set(newVal) {
      console.log('set', key, newVal)
      value = newVal
      return true
    }
  })
}
for (const key of Object.keys(obj)) {
  let value = obj[key]
  Object.defineProperty(obj, key, {
    get() {
      console.log('get', value)
      return value
    },
    set(newVal) {
      console.log('set', key, newVal)
      value = newVal
      return true
    }
  })
}

Proxy与defineProperty的区别

defineProperty 和 Proxy区别

  1. 监听数据的角度

    1. defineproperty只能监听某个属性而不能监听整个对象。
    2. proxy不用设置具体属性,直接监听整个对象。
    3. defineproperty监听需要知道是哪个对象的哪个属性,而proxy只需要知道哪个对象就可以了。也就是会省去for in循环提高了效率。
  2. 监听对原对象的影响

    1. 因为defineproperty是通过在原对象身上新增或修改属性增加描述符的方式实现的监听效果,一定会修改原数据。
    2. proxy只是原对象的代理,proxy会返回一个代理对象不会在原对象上进行改动,对原数据无污染。
  3. 实现对数组的监听

    1. 因为数组 length 的特殊性 (length 的描述符configurable 和 enumerable 为 false,并且妄图修改 configurable 为 True 的话 js 会直接报错:VM305:1 Uncaught TypeError: Cannot redefine property: length)
    2. defineproperty无法监听数组长度变化, Vue只能通过重写数组方法的方式变现达成监听的效果,光重写数组方法还是不能解决修改数组下标时监听的问题,只能再使用自定义的$set的方式
    3. proxy因为自身特性,是创建新的代理对象而不是在原数据身上监听属性,对代理对象进行操作时,所有的操作都会被捕捉,包括数组的方法和length操作,再不需要重写数组方法和自定义set函数了。(代码示例在下方)

    4. 监听的范围

    1. defineproperty只能监听到valueget set 变化。
    2. proxy可以监听除 [[getOwnPropertyNames]] 以外所有JS的对象操作。监听的范围更大更全面。

Proxy

JS
const proxy = new Proxy(target, handler)
const proxy = new Proxy(target, handler)

即使不传入handler,默认也会进行基本的代理操作,将对代理对象的操作透传给原始对象

js
const obj = {
  name: 'ziu',
  age: 18
}
const proxy = new Proxy(obj, {})
proxy.height = 1.88 // 添加新属性
proxy.name = 'Ziu' // 修改原属性

console.log(obj) // { name: 'Ziu', age: 18, height: 1.88 }
const obj = {
  name: 'ziu',
  age: 18
}
const proxy = new Proxy(obj, {})
proxy.height = 1.88 // 添加新属性
proxy.name = 'Ziu' // 修改原属性

console.log(obj) // { name: 'Ziu', age: 18, height: 1.88 }

捕获器 (trap)

handler 对象的方法

常用的捕获器有setget函数

js
const proxy = new Proxy(obj, {
  set: function (target, key, newVal) {
    console.log(\`监听: \${key} 设置 \${newVal}\`)
    target[key] = newVal
  },
  get: function (target, key) {
    console.log(\`监听: \${key} 获取\`)
    return target[key]
  }
})
const proxy = new Proxy(obj, {
  set: function (target, key, newVal) {
    console.log(\`监听: \${key} 设置 \${newVal}\`)
    target[key] = newVal
  },
  get: function (target, key) {
    console.log(\`监听: \${key} 获取\`)
    return target[key]
  }
})

另外介绍两个捕获器:hasdeleteProperty

js
const proxy = new Proxy(obj, {
  ...
  has: function (target, key) {
    console.log(\`监听: \${key} 判断\`)
    return key in target
  },
  deleteProperty: function (target, key) {
    console.log(\`监听: \${key} 删除 \`)
    return true
  }
})

delete proxy.name // 监听: name 删除
console.log('age' in proxy) // 监听: age 判断
const proxy = new Proxy(obj, {
  ...
  has: function (target, key) {
    console.log(\`监听: \${key} 判断\`)
    return key in target
  },
  deleteProperty: function (target, key) {
    console.log(\`监听: \${key} 删除 \`)
    return true
  }
})

delete proxy.name // 监听: name 删除
console.log('age' in proxy) // 监听: age 判断

this指向问题

Proxy对象可以对我们的目标对象进行访问,但没有做任何拦截时,也不能保证与目标对象的行为一致,因为目标对象内部的this会自动改变为Proxy代理对象。

js
const obj = {
  name: 'ziu',
  foo() {
    return this === proxy
  }
}

const proxy = new Proxy(obj, {})

console.log(obj.foo()) // false
console.log(proxy.foo()) // true
const obj = {
  name: 'ziu',
  foo() {
    return this === proxy
  }
}

const proxy = new Proxy(obj, {})

console.log(obj.foo()) // false
console.log(proxy.foo()) // true

使用Proxy监听嵌套对象

TODO

Reflect

Reflect是ES6新增的一个API,它本身是一个对象

如果我们又Object对象可以完成这些操作,为什么还需要Reflect呢?

与Object操作的区别

删除对象上的某个属性

js
const obj = {
  name: 'ziu',
  age: 18
}
// 当且仅当该属性的 configurable 键值为 true 时,该属性的描述符才能够被改变
// 同时该属性也能从对应的对象上被删除。 默认为 false。
Object.defineProperty(obj, 'name', {
  configurable: false
})

// 1. 旧方法 检查\`delete obj.name\`是否执行成功
// 结果: 需要额外编写检查代码且存在问题(严格模式下删除configurable为false的属性将报错)
delete obj.name
if (obj.name) {
  console.log(false)
} else {
  console.log(true)
}

// 2. Reflect
// 结果: 根据是否删除成功返回结果
if (Reflect.deleteProperty(obj, 'name')) {
  console.log(true)
} else {
  console.log(false)
}
const obj = {
  name: 'ziu',
  age: 18
}
// 当且仅当该属性的 configurable 键值为 true 时,该属性的描述符才能够被改变
// 同时该属性也能从对应的对象上被删除。 默认为 false。
Object.defineProperty(obj, 'name', {
  configurable: false
})

// 1. 旧方法 检查\`delete obj.name\`是否执行成功
// 结果: 需要额外编写检查代码且存在问题(严格模式下删除configurable为false的属性将报错)
delete obj.name
if (obj.name) {
  console.log(false)
} else {
  console.log(true)
}

// 2. Reflect
// 结果: 根据是否删除成功返回结果
if (Reflect.deleteProperty(obj, 'name')) {
  console.log(true)
} else {
  console.log(false)
}

Reflect常见方法

其中的方法与Proxy的方法是一一对应的,一共13个。其中的一些方法是Object对象中没有的:

代理对象的目的:不再直接操作原始对象,一切读写操作由代理完成。我们先前在编写Proxy的代理代码时,仍然有操作原对象的行为:

js
const proxy = new Proxy(obj, {
  set: function (target, key, newVal) {
    console.log(\`监听: \${key} 设置 \${newVal}\`)
    target[key] = newVal // 直接操作原对象
  },
})
const proxy = new Proxy(obj, {
  set: function (target, key, newVal) {
    console.log(\`监听: \${key} 设置 \${newVal}\`)
    target[key] = newVal // 直接操作原对象
  },
})

这时我们可以让Reflect登场,代替我们对原对象进行操作,之前的代码可以修改:

js
const proxy = new Proxy(obj, {
  set: function (target, key, newVal) {
    console.log(\`监听: \${key} 设置 \${newVal}\`)
    Reflect.set(target, key, newVal)
  },
  get: function (target, key) {
    console.log(\`监听: \${key} 获取\`)
    return Reflect.get(target, key)
  },
  has: function (target, key) {
    console.log(\`监听: \${key} 判断\`)
    return Reflect.has(target, key)
  }
})
const proxy = new Proxy(obj, {
  set: function (target, key, newVal) {
    console.log(\`监听: \${key} 设置 \${newVal}\`)
    Reflect.set(target, key, newVal)
  },
  get: function (target, key) {
    console.log(\`监听: \${key} 获取\`)
    return Reflect.get(target, key)
  },
  has: function (target, key) {
    console.log(\`监听: \${key} 判断\`)
    return Reflect.has(target, key)
  }
})

使用Reflect替代之前的对象操作有以下好处:

针对好处三,做出如下解释。以下述代码为例,set name(){}函数中的this指向的是obj

js
const obj = {
  _name: 'ziu',
  set name(newVal) {
    console.log(\`set name \${newVal}\`)
    console.log(this)
    this._name = newVal
  },
  get name() {
    console.log(\`get name\`)
    console.log(this)
    return this._name
  }
}

console.log(obj.name)
obj.name = 'Ziu'
const obj = {
  _name: 'ziu',
  set name(newVal) {
    console.log(\`set name \${newVal}\`)
    console.log(this)
    this._name = newVal
  },
  get name() {
    console.log(\`get name\`)
    console.log(this)
    return this._name
  }
}

console.log(obj.name)
obj.name = 'Ziu'
js
const proxy = new Proxy(obj, {
  set: function (target, key, newVal, receiver) {
    console.log(\`监听: \${key} 设置 \${newVal}\`)
    Reflect.set(target, key, newVal, receiver)
  },
  get: function (target, key, receiver) {
    console.log(\`监听: \${key} 获取\`)
    return Reflect.get(target, key, receiver)
  }
})
const proxy = new Proxy(obj, {
  set: function (target, key, newVal, receiver) {
    console.log(\`监听: \${key} 设置 \${newVal}\`)
    Reflect.set(target, key, newVal, receiver)
  },
  get: function (target, key, receiver) {
    console.log(\`监听: \${key} 获取\`)
    return Reflect.get(target, key, receiver)
  }
})

我们使用Proxy代理,并且使用Reflect操作对象时,输出的this仍然为obj,需要注意的是,此处的this指向是默认指向原始对象obj,而如果业务需要改变this指向,此时可以为Reflect.set()的最后一个参数传入receiver

Reflect.construct方法

以下两段代码的实现结果是一样的:

js
function Person(name, age) {
  this.name = name
  this.age = age
}

function Student(name, age) {
  Person.call(this, name, age) // 借用
}

const stu = new Student('ziu', 18)
console.log(stu)
function Person(name, age) {
  this.name = name
  this.age = age
}

function Student(name, age) {
  Person.call(this, name, age) // 借用
}

const stu = new Student('ziu', 18)
console.log(stu)
js
function Person(name, age) {
  this.name = name
  this.age = age
}

function Student(name, age) {
  // Person.call(this, name, age) // 借用
}

const stu = new Reflect.construct(Person, ['ziu', 18], Student)
console.log(stu)
function Person(name, age) {
  this.name = name
  this.age = age
}

function Student(name, age) {
  // Person.call(this, name, age) // 借用
}

const stu = new Reflect.construct(Person, ['ziu', 18], Student)
console.log(stu)

理解Proxy与Reflect中的receiver参数

Proxy和Reflect中的receiver到底是个什么东西

ES6的Proxy中,为什么推荐使用Reflect.get而不是target[key]?

Promise详解

异步代码

ES5之前,都是通过回调函数完成异步逻辑代码的,如果存在多次异步操作,会导致回调函数不断嵌套,回调地狱

js
function fetchData(callback) {
  setTimeout(() => {
    const res = ['data1', 'data2', 'data3']
    callback(res)
  }, 2000)
}
fetchData((res) => {
  const div = document.createElement('div')
  div.innerHTML = res
  document.body.appendChild(div)
})
function fetchData(callback) {
  setTimeout(() => {
    const res = ['data1', 'data2', 'data3']
    callback(res)
  }, 2000)
}
fetchData((res) => {
  const div = document.createElement('div')
  div.innerHTML = res
  document.body.appendChild(div)
})

认识Promise

用Promise改造上述代码:

js
function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const res = ['data1', 'data2', 'data3']
      resolve(res)
    }, 2000)
  })
}
const data = fetchData()
data.then((res) => {
  const div = document.createElement('div')
  div.innerHTML = res
  document.body.appendChild(div)
})
function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const res = ['data1', 'data2', 'data3']
      resolve(res)
    }, 2000)
  })
}
const data = fetchData()
data.then((res) => {
  const div = document.createElement('div')
  div.innerHTML = res
  document.body.appendChild(div)
})

.catch() 其实只是没有给处理已兑现状态的回调函数预留参数位置的 .then() 而已。

Promise状态变化

resolve值

resolve函数传入普通值,Promise的状态会被设置为兑现,而为resolve传入一个Promise对象时,当前Promise对象的状态将由传入的Promise对象决定。如下述代码中,'Outer Promise Result'会被作为最终结果被兑现

js
const p = new Promise((res, rej) => {
  res('Outer Promise Result')
})
function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const res = ['data1', 'data2', 'data3']
      resolve(p)
    }, 2000)
  })
}
const p = new Promise((res, rej) => {
  res('Outer Promise Result')
})
function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const res = ['data1', 'data2', 'data3']
      resolve(p)
    }, 2000)
  })
}

resolve还可以传入thenable对象:该对象中的then方法会被回调

js
function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const res = ['data1', 'data2', 'data3']
      resolve({
        name: 'Ziu',
        then: (resolve, reject) => {
          resolve('thenable Object resolve')
        }
      })
    }, 2000)
  })
}
function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const res = ['data1', 'data2', 'data3']
      resolve({
        name: 'Ziu',
        then: (resolve, reject) => {
          resolve('thenable Object resolve')
        }
      })
    }, 2000)
  })
}

综上:

.then方法

实际上,then方法可以传入两个回调函数,可以同时传入成功处理的回调函数和失败处理的回调函数。

js
const data = fetchData()
data
  .then(
  (res) => {
    const div = document.createElement('div')
    div.innerHTML = res
    document.body.appendChild(div)
  },
  (err) => {
    console.log(err)
  }
)
const data = fetchData()
data
  .then(
  (res) => {
    const div = document.createElement('div')
    div.innerHTML = res
    document.body.appendChild(div)
  },
  (err) => {
    console.log(err)
  }
)

在前文中我们描述.catch为:

.catch() 其实只是没有给处理已兑现状态的回调函数预留参数位置的 .then() 而已。

那么.catch的实现本质上就是为成功回调传入null的.then

js
const data = fetchData()
data
  .catch((err) => {
  console.log(err)
})
  .then(null, (err) => {
  console.log(err)
})
const data = fetchData()
data
  .catch((err) => {
  console.log(err)
})
  .then(null, (err) => {
  console.log(err)
})

Promise的.then方法可以被多次调用,下例中,控制台只会输出一次'execute'而会输出三次'success',证明三次.then都被调用了。同理,.catch方法也可以被多次调用

js
const p = new Promise((res, rej) => {
  console.log('execute')
  res(['data1', 'data2', 'data3'])
})

p.then(() => {
  console.log('success')
})
p.then(() => {
  console.log('success')
})
p.then(() => {
  console.log('success')
})
const p = new Promise((res, rej) => {
  console.log('execute')
  res(['data1', 'data2', 'data3'])
})

p.then(() => {
  console.log('success')
})
p.then(() => {
  console.log('success')
})
p.then(() => {
  console.log('success')
})

.then的返回值

Promise本身支持链式调用,.then方法实际上是返回了一个新的Promise,链式调用中的.then是在等待这个新的Promise兑现之后执行

js
const p = new Promise((res, rej) => {
  res('aaa')
})

p.then((res) => {
  console.log(res)
  return 'bbb' // return new Promise((res) => res('bbb'))
}).then((res) => {
  console.log(res)
})
const p = new Promise((res, rej) => {
  res('aaa')
})

p.then((res) => {
  console.log(res)
  return 'bbb' // return new Promise((res) => res('bbb'))
}).then((res) => {
  console.log(res)
})

.catch的返回值

可以通过throw抛出一个错误实现在链式调用中抛出异常,让.catch对异常进行处理

js
const p = new Promise((res, rej) => {
  res('aaa')
})

p.then((res) => {
  console.log(res)
  return 'bbb' // return new Promise((res) => res('bbb'))
})
  .then((res) => {
  console.log(res)
  throw new Error('Error Info') // return new Promise((_, rej) => rej('Error Info'))
})
  .catch((err) => {
  console.log(err)
})
const p = new Promise((res, rej) => {
  res('aaa')
})

p.then((res) => {
  console.log(res)
  return 'bbb' // return new Promise((res) => res('bbb'))
})
  .then((res) => {
  console.log(res)
  throw new Error('Error Info') // return new Promise((_, rej) => rej('Error Info'))
})
  .catch((err) => {
  console.log(err)
})

Promise类方法

Promise.resolve()

类方法与实例方法,下例中两个方式是等效的,其中Promise.resolve()即为类方法。如果你现在已经有了一个数据内容,希望通过Promise包装来使用,这时就可以通过Promise.resolve()方法

js
Promise.resolve(['data1', 'data2', 'data3'])
// 等价于
new Promise((res) => res(['data1', 'data2', 'data3']))
Promise.resolve(['data1', 'data2', 'data3'])
// 等价于
new Promise((res) => res(['data1', 'data2', 'data3']))

Promise.reject()

resolve方法类似,下述两种写法的效果是相同的

js
Promise.reject(['data1', 'data2', 'data3'])
// 等价于
new Promise((_, rej) => rej(['data1', 'data2', 'data3']))
Promise.reject(['data1', 'data2', 'data3'])
// 等价于
new Promise((_, rej) => rej(['data1', 'data2', 'data3']))

Promise.all()

**Promise.all(iterable)** 方法返回一个 Promise 实例,此实例在 iterable 参数内所有的 promise 都“完成(resolved)”或参数中不包含 promise 时回调完成(resolve);如果参数中 promise 有一个失败(rejected),此实例回调失败(reject),失败原因的是第一个失败 promise 的结果。

js
const p1 = new Promise((res) => res(['data1', 'data2', 'data3']))
const p2 = new Promise((res) => res('Hello, Promise.'))
const p3 = new Promise((_, rej) => rej('Failed'))

const promises = [p1, p2, p3]

Promise.all(promises)
  .then((res) => {
  console.log(res)
})
  .catch((err) => {
  console.log(err)
})
const p1 = new Promise((res) => res(['data1', 'data2', 'data3']))
const p2 = new Promise((res) => res('Hello, Promise.'))
const p3 = new Promise((_, rej) => rej('Failed'))

const promises = [p1, p2, p3]

Promise.all(promises)
  .then((res) => {
  console.log(res)
})
  .catch((err) => {
  console.log(err)
})

Promise.allSettled() (ES11)

.all()方法有一个缺陷:当某一个Promise被reject时,新的Promise会立刻变成reject状态,此时对于其他处于resolved和pending状态的Promise就获取不到结果了

js
Promise.allSettled(promises)
  .then((res) => {
  console.log(res)
})
  .catch((err) => {
  console.log(err)
})
Promise.allSettled(promises)
  .then((res) => {
  console.log(res)
})
  .catch((err) => {
  console.log(err)
})

Promise.race()

Promise.race(iterable) 方法返回一个 promise,一旦迭代器中的某个promise解决或拒绝,返回的 promise就会解决或拒绝。

js
Promise.race(promises)
  .then((res) => {
  console.log(res)
})
  .catch((err) => {
  console.log(err)
})
Promise.race(promises)
  .then((res) => {
  console.log(res)
})
  .catch((err) => {
  console.log(err)
})

Promise.any() (ES12)

迭代器与生成器

什么是迭代器

迭代器是帮助我们对某个数据结构进行遍历的对象,不同语言对迭代器都有不同的实现

在JavaScript中,迭代器是一个具体的对象,这个对象需要符合迭代器协议(literator protocol)

这个next()方法有如下要求:

案例:

js

异步处理

假设有如下场景:请求3的数据依赖请求2,请求2的数据依赖请求1,这样会存在嵌套的问题

js
// 第一次请求
reqData('ziu').then((res) => {
  // 第二次请求
  reqData('Ziu').then((res) => {
    // 第三次请求
    reqData('Ziuc').then((res) => {
      console.log(res)
    })
  })
})
// 第一次请求
reqData('ziu').then((res) => {
  // 第二次请求
  reqData('Ziu').then((res) => {
    // 第三次请求
    reqData('Ziuc').then((res) => {
      console.log(res)
    })
  })
})

可以通过Promise链式调用,避免深层嵌套回调,但是代码并不足够优秀

js
reqData('ziu')
  .then((res) => {
  return reqData(res + 'Ziu')
})
  .then((res) => {
  return reqData(res + 'Ziuc')
})
  .then((res) => {
  console.log(res)
})
reqData('ziu')
  .then((res) => {
  return reqData(res + 'Ziu')
})
  .then((res) => {
  return reqData(res + 'Ziuc')
})
  .then((res) => {
  console.log(res)
})

生成器写法

js
function* getData() {
  const res1 = yield reqData('ziu')
  const res2 = yield reqData(res1 + 'Ziu')
  const res3 = yield reqData(res2 + 'Ziuc')
  console.log(res3)
}

const generator = getData()
generator.next().value.then((res) => {
  generator.next(res).value.then((res) => {
    generator.next(res).value.then((res) => {
      generator.next(res)
    })
  })
})
function* getData() {
  const res1 = yield reqData('ziu')
  const res2 = yield reqData(res1 + 'Ziu')
  const res3 = yield reqData(res2 + 'Ziuc')
  console.log(res3)
}

const generator = getData()
generator.next().value.then((res) => {
  generator.next(res).value.then((res) => {
    generator.next(res).value.then((res) => {
      generator.next(res)
    })
  })
})

通过递归优化生成器代码

js
function execGenFn(genFn) {
  const generator = genFn()
  function exec(res) {
    const result = generator.next(res)
    if (result.done) return
    else result.value.then((res) => exec(res))
  }
  exec()
}

execGenFn(getData)
function execGenFn(genFn) {
  const generator = genFn()
  function exec(res) {
    const result = generator.next(res)
    if (result.done) return
    else result.value.then((res) => exec(res))
  }
  exec()
}

execGenFn(getData)

此时引入asyncawait可以提高代码的阅读性,本质上就是生成器函数和yeild的语法糖

js
async function getData() {
  const res1 = await reqData('ziu')
  const res2 = await reqData(res1 + 'Ziu')
  const res3 = await reqData(res2 + 'Ziuc')
  console.log(res3)
}
getData()
async function getData() {
  const res1 = await reqData('ziu')
  const res2 = await reqData(res1 + 'Ziu')
  const res3 = await reqData(res2 + 'Ziuc')
  console.log(res3)
}
getData()

let与const

作用域提升

var定义的变量会自动进行变量提升

js
console.log(typeof name) // undefined
var name = 'Ziu'
console.log(typeof name) // undefined
var name = 'Ziu'

在声明name前就可以访问到这个变量,且其值为undefined

但是用let const定义的变量在初始化前访问会抛出错误

js
console.log(address) // 报错
let address = 'China'
console.log(address) // 报错
let address = 'China'

暂时性死区

在作用域内,从作用域开始到变量被定义之前的区域被称为暂时性死区,这部分里是不能访问变量的。

js
function foo() {
  console.log(name, age) // 报错
  console.log('Hello, World!')
  let name = 'Ziu'
  let age = 18
}

foo()
function foo() {
  console.log(name, age) // 报错
  console.log('Hello, World!')
  let name = 'Ziu'
  let age = 18
}

foo()
js
// 代码报错:
console.log(name)
let name = 'Hello, World!'
// 代码报错:
console.log(name)
let name = 'Hello, World!'
js
// 代码正确执行
function foo() {
  console.log(name)
}
let name = 'Hello, World!'
foo() // Hello, World!
// 代码正确执行
function foo() {
  console.log(name)
}
let name = 'Hello, World!'
foo() // Hello, World!

在上例中,先执行let name = 'Hello, World!',随后再执行foo()输出name变量,这样就不会报错。

所以得出结论:暂时性死区与代码的执行顺序有关


下述代码能正常执行:调用foo()时,现在内部作用域查找message,没找到则到外部作用域找到message并输出

js
let message = 'Hello, World!'
function foo() {
  console.log(message)
}
foo()
let message = 'Hello, World!'
function foo() {
  console.log(message)
}
foo()

但是稍作修改,下面的代码执行将报错:这是因为函数foo()内部形成了暂时性死区,函数内定义了message,所以优先访问内部的message变量,在输出语句访问它时正处于暂时性死区中,所以会抛出错误

js
let message = 'Hello, World!'
function foo() {
  console.log(message)
  let message = 'Ziu'
}
foo()
let message = 'Hello, World!'
function foo() {
  console.log(message)
  let message = 'Ziu'
}
foo()

变量保存位置

既然通过let const声明的变量不会被保存在window上,那他们保存在哪里呢?我们从ECMA文档入手

A Global Environment Record is logically a single record but it is specified as a composite encapsulating an Object Environment Record and a Declarative Environment Record.

全局环境记录包括:1 对象环境记录 2 声明环境记录

Table 20: Additional Fields of Global Environment Records

Field NameValueMeaning
[[ObjectRecord]]an Object Environment RecordBinding object is the global object. It contains global built-in bindings as well as FunctionDeclaration, GeneratorDeclaration, AsyncFunctionDeclaration, AsyncGeneratorDeclaration, and VariableDeclaration bindings in global code for the associated realm.
[[GlobalThisValue]]an ObjectThe value returned by this in global scope. Hosts may provide any ECMAScript Object value.
[[DeclarativeRecord]]a Declarative Environment RecordContains bindings for all declarations in global code for the associated realm code except for FunctionDeclaration, GeneratorDeclaration, AsyncFunctionDeclaration, AsyncGeneratorDeclaration, and VariableDeclaration bindings.
[[VarNames]]a List of StringsThe string names bound by FunctionDeclaration, GeneratorDeclaration, AsyncFunctionDeclaration, AsyncGeneratorDeclaration, and VariableDeclaration declarations in global code for the associated realm.

由上表可知,对象环境记录也就是global object,在浏览器中也就是window对象,它包含了全局内置绑定:函数声明、生成器声明、异步函数声明、异步生成器声明、变量声明(特指通过var声明的变量)

而在声明环境记录中,它包含了除了上述内容的声明,比如const let声明的变量

块级作用域

用花括号括起来的代码块是一个块级作用域,在ES5之前,只有全局作用域与函数作用域

js
{
  var bar = 'Ziu'
}
console.log(bar) // Ziu
{
  var bar = 'Ziu'
}
console.log(bar) // Ziu

在代码块内声明的变量,在外部仍然可以访问到。

但是通过let const function class声明的变量具有块级作用域限制:

js
{
  let foo = 'foo'
  function bar() {
    console.log('bar')
  }
  class Person {}
}

console.log(foo) // 报错
bar() // bar 函数能够被调用
var p = new Person() // 报错
{
  let foo = 'foo'
  function bar() {
    console.log('bar')
  }
  class Person {}
}

console.log(foo) // 报错
bar() // bar 函数能够被调用
var p = new Person() // 报错

但是,函数虽然有块级作用域限制,但是仍然是可以在外界被访问的

js
bar() // 报错
{
  function bar() {
    console.log('bar')
  }
}
bar() // 报错
{
  function bar() {
    console.log('bar')
  }
}

开发中的应用

使用var定义的变量会被自动提升,在某些情况下会导致非预期行为:

使用var的非预期行为

我们希望为每个按钮添加监听事件,点击后在控制台输出按钮编号,然而通过var定义循环变量i时,其所在作用域是全局的。代码执行完毕后,当多个回调函数访问位于全局的i时,它的值已经变成了3,所以每次点击控制台都会输出btn 4 is clicked

html
<button>按钮1</button>
<button>按钮2</button>
<button>按钮3</button>
<button>按钮4</button>
<script>
  const btns = document.querySelectorAll('button')
  for (var i = 1; i < btns.length; i++) {
    const btn = btns[i]
    btn.onclick = function () {
      console.log(\`btn \${i + 1} is clicked\`) // btn 4 is clicked
    }
  }
</script>
<button>按钮1</button>
<button>按钮2</button>
<button>按钮3</button>
<button>按钮4</button>
<script>
  const btns = document.querySelectorAll('button')
  for (var i = 1; i < btns.length; i++) {
    const btn = btns[i]
    btn.onclick = function () {
      console.log(\`btn \${i + 1} is clicked\`) // btn 4 is clicked
    }
  }
</script>

早期解决方案

当然,在没有let const的早期也有解决方法:为每个DOM引用添加额外属性index,保存当前次遍历时的i

js
const btns = document.querySelectorAll('button')
for (var i = 0; i < btns.length; i++) {
  const btn = btns[i]
  btn.index = i
  btn.onclick = function () {
    console.log(\`btn \${this.index + 1} is clicked\`) // btn 0,1,2,3 is clicked
  }
}
const btns = document.querySelectorAll('button')
for (var i = 0; i < btns.length; i++) {
  const btn = btns[i]
  btn.index = i
  btn.onclick = function () {
    console.log(\`btn \${this.index + 1} is clicked\`) // btn 0,1,2,3 is clicked
  }
}

也可以使用立即执行函数,每次添加监听回调函数时都创建一个新的作用域,并且将当前的i值作为参数传入,这样每次回调函数在被调用时访问到的都是各自的词法环境(作用域),传入函数的m值是固定的

这里用到了闭包,立即执行函数会拥有自己的AO,而每次遍历时立即执行函数的AO都通过闭包保存了下来,这样挂载到AO上的变量就是各自独立的了

js
const btns = document.querySelectorAll('button')
for (var i = 0; i < btns.length; i++) {
  const btn = btns[i]
  ;(function (m) {
    btn.onclick = function () {
      console.log(\`btn \${m + 1} is clicked\`) // btn 0,1,2,3 is clicked
    }
  })(i)
}
const btns = document.querySelectorAll('button')
for (var i = 0; i < btns.length; i++) {
  const btn = btns[i]
  ;(function (m) {
    btn.onclick = function () {
      console.log(\`btn \${m + 1} is clicked\`) // btn 0,1,2,3 is clicked
    }
  })(i)
}

使用let解决

如果使用let来定义循环用的临时变量i,那么每次遍历时都是一个新的作用域,变量i不会泄露到全局,这样每个回调函数在被调用时访问到的i都来自各自的词法环境,都来自作用域

js
const btns = document.querySelectorAll('button')
for (let i = 0; i < btns.length; i++) {
  const btn = btns[i]
  btn.onclick = function () {
    console.log(\`btn \${i + 1} is clicked\`) // btn 0,1,2,3 is clicked
  }
}
const btns = document.querySelectorAll('button')
for (let i = 0; i < btns.length; i++) {
  const btn = btns[i]
  btn.onclick = function () {
    console.log(\`btn \${i + 1} is clicked\`) // btn 0,1,2,3 is clicked
  }
}

await async 事件循环

异步函数 async

js
// 普通函数
function fun() {}
const fun = function () {}
// 箭头函数
const fun = () => {}
// 生成器函数
function* fun() {}
// 普通函数
function fun() {}
const fun = function () {}
// 箭头函数
const fun = () => {}
// 生成器函数
function* fun() {}
js
// 异步函数写法
async function fun() {}
const fun = async function () {}
const fun = async () => {}

// 默认包裹Promise
async function getData() {
  return '123'
}
const res = getData()
console.log(res) // Promise {<fulfilled>: '123'}

res.then((r) => {
  console.log(r) // '123'
})
// 异步函数写法
async function fun() {}
const fun = async function () {}
const fun = async () => {}

// 默认包裹Promise
async function getData() {
  return '123'
}
const res = getData()
console.log(res) // Promise {<fulfilled>: '123'}

res.then((r) => {
  console.log(r) // '123'
})

异步函数的返回值默认会被包裹一层Promise。

如果代码中抛出了错误,不会像普通函数一样报错,而是会在内部调用reject(err)将错误抛给.catch方法(如果有的话),否则会直接抛出

js
async function getData() {
  throw new Error('Error Info')
  return '123'
}

const res = getData()
.then((r) => {
  console.log(r)
})
.catch((err) => {
  console.log(err)
})
async function getData() {
  throw new Error('Error Info')
  return '123'
}

const res = getData()
.then((r) => {
  console.log(r)
})
.catch((err) => {
  console.log(err)
})

await关键字

会阻塞代码执行,与yeild类似

js
async function reqData(param) {
  return param
}

async function getData() {
  const data1 = await reqData('ziu')
  const data2 = await reqData('Ziu')
  const data3 = await reqData('Ziuc')

  console.log(data1, data2, data3)
}

getData()
async function reqData(param) {
  return param
}

async function getData() {
  const data1 = await reqData('ziu')
  const data2 = await reqData('Ziu')
  const data3 = await reqData('Ziuc')

  console.log(data1, data2, data3)
}

getData()

如果代码在await过程中出现异常,也可以通过.catch捕获到异常

js
async function reqData(param) {
  throw new Error('Error Info')
  return param
}

async function getData() {
  const data = await reqData('ziu')
  console.log(data)
}

getData().catch((err) => {
  console.log(err) // 捕获到异步函数内抛出的异常
})
async function reqData(param) {
  throw new Error('Error Info')
  return param
}

async function getData() {
  const data = await reqData('ziu')
  console.log(data)
}

getData().catch((err) => {
  console.log(err) // 捕获到异步函数内抛出的异常
})

进程与线程

操作系统可以同时让多个进程同时工作

JavaScript线程

JavaScript是单线程(可以开启Worker)的,但是JavaScript线程应该有自己的容器进程:浏览器或Node

setTimeout

当函数执行到setTimeout时,会由浏览器单独的另一个线程负责计时,将这个定时器任务放到Event Table,计时完成后Event Table会将定时器的回调函数放入Event Queue,当主线程的代码执行完毕后,会去Event Queue中取出队头的任务放入主线程中执行

这样的过程会不断重复,这也就是常说的Event Loop 事件循环

js
setTimeout(() => {
  console.log('timeout')
}, 500)
setTimeout(() => {
  console.log('timeout')
}, 500)

setInterval

setIntervalsetTimeout类似,只不过setInterval会每隔将注册的回调函数放入Event Queue,如果前面的事件处理得太久也会出现延迟执行的问题,如果队列前面的事件阻塞的时间太长,那么后续队列中就连续被放入了多个setInterval的回调函数任务,这样就失去了定期执行的初衷。

js
setInterval(() => {
  console.log('interval')
}, 500)
setInterval(() => {
  console.log('interval')
}, 500)

onClick

DOM监听时注册的回调函数也会作为异步任务进入事件循环中,每次点击都会将回调函数任务加入到Event Queue中。

js
const btn = document.querySelector('button')
btn.addEventListener('click', () => {
  console.log('btn Clicked')
})
const btn = document.querySelector('button')
btn.addEventListener('click', () => {
  console.log('btn Clicked')
})

微任务与宏任务

js
console.log('script start')

setTimeout(() => {
  console.log('timeout1')
})

new Promise((res) => {
  console.log('promise')
  res()
}).then((res) => {
  console.log('then')
})

setTimeout(() => {
  console.log('timeout2')
})

console.log('script end')
console.log('script start')

setTimeout(() => {
  console.log('timeout1')
})

new Promise((res) => {
  console.log('promise')
  res()
}).then((res) => {
  console.log('then')
})

setTimeout(() => {
  console.log('timeout2')
})

console.log('script end')
sh
'script start'
'promise'
'script end'
'then'
'timeout1'
'timeout2'
'script start'
'promise'
'script end'
'then'
'timeout1'
'timeout2'

Promise内的代码会直接在主线程中执行,而.then内的回调则会作为微任务进入微任务队列

事件循环 面试题

面试题1

js
console.log('script start')

setTimeout(() => {
  console.log('setTimeout1')
  new Promise((res) => {
    res()
  }).then(() => {
    new Promise((res) => {
      res()
    }).then(() => {
      console.log('then4')
    })
    console.log('then2')
  })
})

new Promise((res) => {
  console.log('promise1')
  res()
}).then(() => {
  console.log('then1')
})

setTimeout(() => {
  console.log('setTimeout2')
})

console.log('2')

queueMicrotask(() => {
  console.log('queueMicrotask1')
})

new Promise((res) => {
  res()
}).then(() => {
  console.log('then3')
})

console.log('script end')
console.log('script start')

setTimeout(() => {
  console.log('setTimeout1')
  new Promise((res) => {
    res()
  }).then(() => {
    new Promise((res) => {
      res()
    }).then(() => {
      console.log('then4')
    })
    console.log('then2')
  })
})

new Promise((res) => {
  console.log('promise1')
  res()
}).then(() => {
  console.log('then1')
})

setTimeout(() => {
  console.log('setTimeout2')
})

console.log('2')

queueMicrotask(() => {
  console.log('queueMicrotask1')
})

new Promise((res) => {
  res()
}).then(() => {
  console.log('then3')
})

console.log('script end')
sh
'script start'
'promise1'
'2'
'script end'
'then1'
'queueMicrotask1'
'then3'
'setTimeout1'
'then2'
'then4'
'setTimeout2'
'script start'
'promise1'
'2'
'script end'
'then1'
'queueMicrotask1'
'then3'
'setTimeout1'
'then2'
'then4'
'setTimeout2'

先开始执行微任务队列,微任务队列执行完毕清空后,开始setTimeout1宏任务

await的使用

js
console.log('script start')

function reqData(param) {
  return new Promise((res) => {
    setTimeout(() => {
      console.log('setTimeout')
      res(param)
    }, 2000)
  })
}

function getData() {
  console.log('getData start')

  reqData('Ziu').then((res) => {
    console.log('res', res)
  })

  console.log('getData end')
}

getData()

console.log('script end')
console.log('script start')

function reqData(param) {
  return new Promise((res) => {
    setTimeout(() => {
      console.log('setTimeout')
      res(param)
    }, 2000)
  })
}

function getData() {
  console.log('getData start')

  reqData('Ziu').then((res) => {
    console.log('res', res)
  })

  console.log('getData end')
}

getData()

console.log('script end')
sh
'script start'
'getData start'
'getData end'
'script end'
'setTimeout'
'res Ziu'
'script start'
'getData start'
'getData end'
'script end'
'setTimeout'
'res Ziu'

主线程运行过程中,执行到getData start后的Promise时,其中代码在主线程中进行

遇到setTimeout,放入Event Table由浏览器开始计时,计时2s结束后,将回调函数放入宏任务队列中执行setTimeout

setTimeout执行完毕,res(param)执行后,返回的Promise成功resolved,将.then回调函数放入微任务队列并开始执行

微任务.then执行完毕输出res 代码执行结束

改用await实现微任务

js
console.log('script start')

function reqData(param) {
  return new Promise((res) => {
    setTimeout(() => {
      console.log('setTimeout')
      res(param)
    }, 2000)
  })
}

async function getData() {
  console.log('getData start')

  const res = await reqData('Ziu')

  console.log('res', res)
  console.log('getData end')
}

getData()

console.log('script end')
console.log('script start')

function reqData(param) {
  return new Promise((res) => {
    setTimeout(() => {
      console.log('setTimeout')
      res(param)
    }, 2000)
  })
}

async function getData() {
  console.log('getData start')

  const res = await reqData('Ziu')

  console.log('res', res)
  console.log('getData end')
}

getData()

console.log('script end')

改写上述面试题2,将res部分代码改为使用await,这里的代码输出为:

sh
'script start'
'getData start'
'script end'
'setTimeout'
'res Ziu'
'getData end'
'script start'
'getData start'
'script end'
'setTimeout'
'res Ziu'
'getData end'

await后续的代码,实质上就是一个微任务,这些代码不在主线程中被执行,而是被放入微任务队列:

js
/* 使用await */
const res = await reqData('Ziu')
console.log('res', res)
console.log('getData end')
/* 不使用await */
reqData('Ziu').then((res) => {
  console.log('res', res)
})
console.log('getData end')
/* 使用await */
const res = await reqData('Ziu')
console.log('res', res)
console.log('getData end')
/* 不使用await */
reqData('Ziu').then((res) => {
  console.log('res', res)
})
console.log('getData end')

这样就会出现:先script end,后setTimeout, res Ziu, getData end的情况

面试题2

js
console.log('script start')

async function async1() {
  console.log('async1 start')
  await async2()
  console.log('async1 end')
}

async function async2() {
  console.log('async2')
}

setTimeout(() => {
  console.log('setTimeout')
})

async1()

new Promise((res) => {
  console.log('promise1')
  res()
}).then(() => {
  console.log('promise2')
})

console.log('script end')
console.log('script start')

async function async1() {
  console.log('async1 start')
  await async2()
  console.log('async1 end')
}

async function async2() {
  console.log('async2')
}

setTimeout(() => {
  console.log('setTimeout')
})

async1()

new Promise((res) => {
  console.log('promise1')
  res()
}).then(() => {
  console.log('promise2')
})

console.log('script end')
sh
'script start'
'async1 start'
'async2'
'promise1'
'script end'
'async1 end'
'promise2'
'setTimeout'
'script start'
'async1 start'
'async2'
'promise1'
'script end'
'async1 end'
'promise2'
'setTimeout'

await前的代码是跟随之前代码的执行顺序执行的,而其后的代码会被放入微任务队列中延迟执行

Node事件循环

TODO

防抖与节流

防抖函数

降低高频操作的发生频次,如输入框防抖

防抖的应用场景

基本实现

js
function debounce(callback, timeout) {
  let timer = null
  const _debounce = function () {
    if (timer) clearTimeout(timer) // 高频调用 清理上次调用的timer
    // 创建本次调用的timer
    timer = setTimeout(() => {
      callback()
      timer = null // 执行回调 清理此次调用的timer
    }, timeout)
  }
  return _debounce
}

const input = document.querySelector('input')
input.oninput = debounce(function() {
  console.log(this.value)
}, 500)
function debounce(callback, timeout) {
  let timer = null
  const _debounce = function () {
    if (timer) clearTimeout(timer) // 高频调用 清理上次调用的timer
    // 创建本次调用的timer
    timer = setTimeout(() => {
      callback()
      timer = null // 执行回调 清理此次调用的timer
    }, timeout)
  }
  return _debounce
}

const input = document.querySelector('input')
input.oninput = debounce(function() {
  console.log(this.value)
}, 500)

优化 绑定this 传入参数

此时并没有绑定this,无法通过this.value获取到当前输入框内的值,现在要将返回的函数_debounce绑定给input

以DOM事件绑定的函数为例,事件触发回调的入参会有参数event可供使用

js
function debounce(callback, timeout) {
  let timer = null
  const _debounce = function (...args) {
    if (timer) clearTimeout(timer) // 高频调用 清理上次调用的timer
    // 创建本次调用的timer
    timer = setTimeout(() => {
      callback.apply(this, args)
      timer = null // 执行回调 清理此次调用的timer
    }, timeout)
  }
  return _debounce
}

const input = document.querySelector('input')
input.oninput = debounce(function (event) {
  console.log(this.value, event)
}, 500)
function debounce(callback, timeout) {
  let timer = null
  const _debounce = function (...args) {
    if (timer) clearTimeout(timer) // 高频调用 清理上次调用的timer
    // 创建本次调用的timer
    timer = setTimeout(() => {
      callback.apply(this, args)
      timer = null // 执行回调 清理此次调用的timer
    }, timeout)
  }
  return _debounce
}

const input = document.querySelector('input')
input.oninput = debounce(function (event) {
  console.log(this.value, event)
}, 500)

需要注意的是:在箭头函数中没有this,会直接去上层作用域找this,直到找到function定义的函数。

优化 取消功能

如果用户输入过程中执行了页面跳转,旧的防抖函数需要被取消

_debounce绑定一个取消的函数cancel(),执行后如果有timer,则清理此timer

js
function debounce(callback, timeout) {
  let timer = null
  const _debounce = function (...args) {
    if (timer) clearTimeout(timer) // 高频调用 清理上次调用的timer
    // 创建本次调用的timer
    timer = setTimeout(() => {
      callback.apply(this, args)
      timer = null // 执行回调 清理此次调用的timer
    }, timeout)
  }
  _debounce.cancel = function () {
    if (timer) clearTimeout(timer)
    timer = null
  }
  return _debounce
}

const input = document.querySelector('input')
const callback = debounce(function (event) {
  console.log(this.value, event)
}, 2000, true)
input.oninput = callback

const cancel = () => {
  console.log('debounce canceled')
  callback.cancel()
}
function debounce(callback, timeout) {
  let timer = null
  const _debounce = function (...args) {
    if (timer) clearTimeout(timer) // 高频调用 清理上次调用的timer
    // 创建本次调用的timer
    timer = setTimeout(() => {
      callback.apply(this, args)
      timer = null // 执行回调 清理此次调用的timer
    }, timeout)
  }
  _debounce.cancel = function () {
    if (timer) clearTimeout(timer)
    timer = null
  }
  return _debounce
}

const input = document.querySelector('input')
const callback = debounce(function (event) {
  console.log(this.value, event)
}, 2000, true)
input.oninput = callback

const cancel = () => {
  console.log('debounce canceled')
  callback.cancel()
}

优化 立即执行功能

第一次触发时立即执行,后续时才防抖(默认为false

js
function debounce(callback, timeout, immediate = false) {
  let timer = null
  let isInvoke = false
  const _debounce = function (...args) {
    if (timer) clearTimeout(timer) // 高频调用 清理上次调用的timer

    // 需要立即执行 并且是第一次执行 立即执行回调
    if (immediate && !isInvoke) {
      callback.apply(this, args)
      isInvoke = true // 标记已经执行过
      return
    }
    // 创建本次调用的timer
    timer = setTimeout(() => {
      callback.apply(this, args)
      timer = null // 执行回调 清理此次调用的timer
      isInvoke = false // 恢复初始状态
    }, timeout)
  }
  _debounce.cancel = function () {
    if (timer) clearTimeout(timer)
    timer = null
    isInvoke = false
  }
  return _debounce
}
function debounce(callback, timeout, immediate = false) {
  let timer = null
  let isInvoke = false
  const _debounce = function (...args) {
    if (timer) clearTimeout(timer) // 高频调用 清理上次调用的timer

    // 需要立即执行 并且是第一次执行 立即执行回调
    if (immediate && !isInvoke) {
      callback.apply(this, args)
      isInvoke = true // 标记已经执行过
      return
    }
    // 创建本次调用的timer
    timer = setTimeout(() => {
      callback.apply(this, args)
      timer = null // 执行回调 清理此次调用的timer
      isInvoke = false // 恢复初始状态
    }, timeout)
  }
  _debounce.cancel = function () {
    if (timer) clearTimeout(timer)
    timer = null
    isInvoke = false
  }
  return _debounce
}

优化 获取返回值

在某些场景下,我们希望获取防抖函数中回调的返回值

js
const myDebounce = debounce(function (name, age, height) {
  const res = \`\${name} \${age} \${height}\`
  console.log(res)
  return res
}, 1000)

// 三次调用只会执行最后一次
const res1 = myDebounce('Ziu', 18, 1.88)
const res2 = myDebounce('Ziu', 18, 1.88)
const res3 = myDebounce('Ziu', 18, 1.88)

console.log(res1, res2, res3) // undefined undefined undefined
const myDebounce = debounce(function (name, age, height) {
  const res = \`\${name} \${age} \${height}\`
  console.log(res)
  return res
}, 1000)

// 三次调用只会执行最后一次
const res1 = myDebounce('Ziu', 18, 1.88)
const res2 = myDebounce('Ziu', 18, 1.88)
const res3 = myDebounce('Ziu', 18, 1.88)

console.log(res1, res2, res3) // undefined undefined undefined

可以将callback的返回值用Promise返回,后续有结果直接通过.then即可获取到返回值,同时通过try-catch包裹,抛出代码执行错误

js
function debounce(callback, timeout, immediate = false) {
  let timer = null
  let isInvoke = false
  const _debounce = function (...args) {
    return new Promise((resolve, reject) => {
      try {
        if (timer) clearTimeout(timer) // 高频调用 清理上次调用的timer
        // 需要立即执行 并且是第一次执行 立即执行回调
        if (immediate && !isInvoke) {
          const res = callback.apply(this, args)
          resolve(res)
          isInvoke = true // 标记已经执行过
          return
        }
        // 创建本次调用的timer
        timer = setTimeout(() => {
          const res = callback.apply(this, args)
          resolve(res)
          timer = null // 执行回调 清理此次调用的timer
          isInvoke = false // 恢复初始状态
        }, timeout)
      } catch (error) {
        reject(error)
      }
    })
  }
  return _debounce
}

const myDebounce = debounce(function (name, age, height) {
  const res = \`\${name} \${age} \${height}\`
  return res
}, 1000)

myDebounce('Ziu', 18, 1.88).then((res) => {
  console.log(res) // Ziu 18 1.88
})
function debounce(callback, timeout, immediate = false) {
  let timer = null
  let isInvoke = false
  const _debounce = function (...args) {
    return new Promise((resolve, reject) => {
      try {
        if (timer) clearTimeout(timer) // 高频调用 清理上次调用的timer
        // 需要立即执行 并且是第一次执行 立即执行回调
        if (immediate && !isInvoke) {
          const res = callback.apply(this, args)
          resolve(res)
          isInvoke = true // 标记已经执行过
          return
        }
        // 创建本次调用的timer
        timer = setTimeout(() => {
          const res = callback.apply(this, args)
          resolve(res)
          timer = null // 执行回调 清理此次调用的timer
          isInvoke = false // 恢复初始状态
        }, timeout)
      } catch (error) {
        reject(error)
      }
    })
  }
  return _debounce
}

const myDebounce = debounce(function (name, age, height) {
  const res = \`\${name} \${age} \${height}\`
  return res
}, 1000)

myDebounce('Ziu', 18, 1.88).then((res) => {
  console.log(res) // Ziu 18 1.88
})

节流函数

基本实现

js
function throttle(callback, interval) {
  let startTime = 0 // 开始时间
  const _throttle = function (...args) {
    const nowTime = new Date().getTime() // 当前时间
    const waitTime = interval - (nowTime - startTime) // 需要等待的时间
    // 结束等待 调用函数
    if (waitTime <= 0) {
      callback.apply(this, args)
      startTime = nowTime // 重置开始时间 准备下一次调用
    }
  }
  return _throttle
}

const input = document.querySelector('input')
const callback = throttle(function (event) {
  console.log(this.value, event)
}, 2000)
input.oninput = callback
function throttle(callback, interval) {
  let startTime = 0 // 开始时间
  const _throttle = function (...args) {
    const nowTime = new Date().getTime() // 当前时间
    const waitTime = interval - (nowTime - startTime) // 需要等待的时间
    // 结束等待 调用函数
    if (waitTime <= 0) {
      callback.apply(this, args)
      startTime = nowTime // 重置开始时间 准备下一次调用
    }
  }
  return _throttle
}

const input = document.querySelector('input')
const callback = throttle(function (event) {
  console.log(this.value, event)
}, 2000)
input.oninput = callback

立即执行

一般情况下需要立即执行,也是默认状态。当需要取消第一次输入时的立即执行时,可以令startTimenowTime相等,这样_throttle第一次触发时,计算出来的waitTime值就为interval的值,也就是等待一个间隔的时间再执行。

需要注意的是触发条件为!immediate && startTime ===0同时满足,默认立即执行,只有同时满足startTime === 0即第一次触发函数时才会设置startTime = nowTime

js
function throttle(callback, interval, immediate = true) {
  let startTime = 0 // 开始时间
  const _throttle = function (...args) {
    const nowTime = new Date().getTime() // 当前时间

    if (!immediate && startTime === 0) {
      startTime = nowTime
    }

    const waitTime = interval - (nowTime - startTime) // 需要等待的时间
    // 结束等待 调用函数
    if (waitTime <= 0) {
      callback.apply(this, args)
      startTime = nowTime // 重置开始时间 准备下一次调用
    }
  }
  return _throttle
}

const input = document.querySelector('input')
const callback = throttle(
  function (event) {
    console.log(this.value, event)
  },
  2000,
  false
)
input.oninput = callback
function throttle(callback, interval, immediate = true) {
  let startTime = 0 // 开始时间
  const _throttle = function (...args) {
    const nowTime = new Date().getTime() // 当前时间

    if (!immediate && startTime === 0) {
      startTime = nowTime
    }

    const waitTime = interval - (nowTime - startTime) // 需要等待的时间
    // 结束等待 调用函数
    if (waitTime <= 0) {
      callback.apply(this, args)
      startTime = nowTime // 重置开始时间 准备下一次调用
    }
  }
  return _throttle
}

const input = document.querySelector('input')
const callback = throttle(
  function (event) {
    console.log(this.value, event)
  },
  2000,
  false
)
input.oninput = callback

深拷贝与浅拷贝

对象属于引用类型,如果单纯复制

浅拷贝

js
const obj1 = {
  name: 'ziu',
  age: 18,
  friend: {
    name: 'kobe'
  }
}

// 1. 引用赋值
// 修改obj2会影响原数据
const obj2 = obj1
obj2.name = 'Ziu'
console.log(obj1, obj2)

// 2. 浅拷贝
// 修改基本数据类型不会影响原对象
// 但是引用类型数据仍然是原对象的引用
const obj3 = { ...obj1 } // Object.assign({}, obj1)
obj3.name = 'Ziuc'
obj3.friend.name = 'james'
console.log(obj1, obj3)
const obj1 = {
  name: 'ziu',
  age: 18,
  friend: {
    name: 'kobe'
  }
}

// 1. 引用赋值
// 修改obj2会影响原数据
const obj2 = obj1
obj2.name = 'Ziu'
console.log(obj1, obj2)

// 2. 浅拷贝
// 修改基本数据类型不会影响原对象
// 但是引用类型数据仍然是原对象的引用
const obj3 = { ...obj1 } // Object.assign({}, obj1)
obj3.name = 'Ziuc'
obj3.friend.name = 'james'
console.log(obj1, obj3)

深拷贝

基本实现

封装函数isObject判断传入变量是否为对象类型

js
function isObject(variable) {
  // null | object | array -> object
  // function -> function
  const type = typeof variable
  return variable !== null && (type === 'object' || type === 'function')
}

console.log(isObject(() => {})) // true
console.log(isObject(null)) // false
console.log(isObject({})) // true
console.log(isObject(undefined)) // false
function isObject(variable) {
  // null | object | array -> object
  // function -> function
  const type = typeof variable
  return variable !== null && (type === 'object' || type === 'function')
}

console.log(isObject(() => {})) // true
console.log(isObject(null)) // false
console.log(isObject({})) // true
console.log(isObject(undefined)) // false

通过递归实现基本的deepCopy函数,可以深拷贝嵌套对象

传入的对象如果包含数组,通过 Array.isArray() .toString() 等方法对数据类型进行具体的判断

js
function deepCopy(originValue) {
  // 递归的值非对象 直接返回其本身
  if (!isObject(originValue)) {
    return originValue
  }
  const target = Array.isArray(originValue) ? [] : {} // 判断原始值是数组还是对象
  for (const key in originValue) {
    // 递归调用deepCopy 实时创建新对象
    target[key] = deepCopy(originValue[key])
  }
  return target
}
function deepCopy(originValue) {
  // 递归的值非对象 直接返回其本身
  if (!isObject(originValue)) {
    return originValue
  }
  const target = Array.isArray(originValue) ? [] : {} // 判断原始值是数组还是对象
  for (const key in originValue) {
    // 递归调用deepCopy 实时创建新对象
    target[key] = deepCopy(originValue[key])
  }
  return target
}

支持Set

此时的代码,如果原始对象包含Set(),他也会被当做Object处理,但是需要注意的是:

js
// 处理Set
if (originValue instanceof Set) {
  const set = new Set()
  for (const item of originValue) {
    console.log(item)
    set.add(deepCopy(item))
  }
  return set
}
// 处理Set
if (originValue instanceof Set) {
  const set = new Set()
  for (const item of originValue) {
    console.log(item)
    set.add(deepCopy(item))
  }
  return set
}

支持函数

一般情况下是不需要深拷贝函数的,对性能有较大的影响,当然,如果一定要支持深拷贝函数,只需要对函数做一次判断即可

直接返回其本身,这样两个函数都是同一片内存空间的引用,不必重新开辟新的内存空间

js
// 处理Function
if (typeof originValue === 'function') {
  return originValue
}
// 处理Function
if (typeof originValue === 'function') {
  return originValue
}

支持Symbol作为值

typeof Symbol() 返回的是 'symbol',同时需要考虑到如果Symbol()传入了description的情况

js
// 处理Symbol
if (typeof originValue === 'symbol') {
  return Symbol(originValue.description)
}
// 处理Symbol
if (typeof originValue === 'symbol') {
  return Symbol(originValue.description)
}

支持Symbol作为键

Symbol作为键在执行for-in时不会被遍历得到,以下是MDN对for-in的说明:

for...in 语句以任意顺序迭代一个对象的除Symbol以外的可枚举属性,包括继承的可枚举属性。

js
// 遍历对象中的Symbol键
const symbolKeys = Object.getOwnPropertySymbols(originValue)
for (const symbol of symbolKeys) {
  target[Symbol(symbol.description)] = deepCopy(originValue[symbol])
}
// 遍历对象中的Symbol键
const symbolKeys = Object.getOwnPropertySymbols(originValue)
for (const symbol of symbolKeys) {
  target[Symbol(symbol.description)] = deepCopy(originValue[symbol])
}

使用Object.getOwnPropertySymbols()方法获取所有键名类型为SymbolSymbo对象并遍历,逐个向新的深拷贝对象中添加对应description的新的Symbol键值对

支持循环引用

循环引用:在使用JSON.stringify对包含循环引用的对象进行转化时会报错

js
const obj = {
  name: 'Object'
}
obj.self = obj
console.log(obj === obj.self) // true
console.log(obj.self.self.self.self.self.self) // obj
const obj = {
  name: 'Object'
}
obj.self = obj
console.log(obj === obj.self) // true
console.log(obj.self.self.self.self.self.self) // obj

如果使用我们之前编写的deepCopy拷贝一个包含循环引用的对象,会出现函数执行栈溢出的问题,这是因为target[key] = deepCopy(originValue[key])不会跳出而是会一直递归,最终导致栈溢出

可以为增加一个默认入参,每次调用deepCopy时为其传入一个Map,其中保存着拷贝出来的新对象的引用,在调用deepCopy时对此Map进行判断,如果其中已经包含了拷贝出来的新对象则直接取值并赋给当前的key,不再重新拷贝

每次执行完深拷贝需要对这个Map中保存的对象进行删除操作,如果通过map = null销毁此map,由于Map是强引用,里面保存的对象在内存中并不会被真正销毁。可以使用WeakMap替换,WeakMap中的引用是弱引用,如果引用的原始对象被销毁,能保证其引用的对象也被销毁

完整代码

js
function isObject(variable) {
  // null | object | array -> object
  // function -> function
  const type = typeof variable
  return variable !== null && (type === 'object' || type === 'function')
}

function deepCopy(originValue, map = new WeakMap()) {
  // 处理Symbol
  if (typeof originValue === 'symbol') {
    return Symbol(originValue.description)
  }

  // 递归的值非对象 直接返回其本身
  if (!isObject(originValue)) {
    return originValue
  }

  // 处理Set
  if (originValue instanceof Set) {
    const set = new Set()
    for (const item of originValue) {
      console.log(item)
      set.add(deepCopy(item))
    }
    return set
  }

  // 处理Function
  if (typeof originValue === 'function') {
    return originValue
  }

  // 如果存在引用循环 且上次已经设置过引用 则跳出递归
  if (map.get(originValue)) {
    return map.get(originValue)
  }

  // 如果是对象/数组类型 需要创建新的对象/数组
  const target = Array.isArray(originValue) ? [] : {} // 判断原始值是数组还是对象
  map.set(originValue, target) // 将新对象自身保存到WeakMap中

  // 遍历当前对象中所有普通的key
  for (const key in originValue) {
    // 递归调用deepCopy 实时创建新对象
    target[key] = deepCopy(originValue[key], map)
  }

  // 遍历对象中的Symbol键
  const symbolKeys = Object.getOwnPropertySymbols(originValue)
  for (const symbol of symbolKeys) {
    target[Symbol(symbol.description)] = deepCopy(originValue[symbol], map)
  }

  return target
}

const obj = {
  name: 'Ziu',
  age: 18,
  friend: {
    name: 'kobe',
    address: {
      name: '洛杉矶',
      detail: '斯坦普斯中心'
    }
  },
  array: [
    { name: 'JavaScript权威指南', price: 99 },
    { name: 'JavaScript高级程序设计', price: 66 }
  ],
  set: new Set(['abc', 'cba', 'nba']),
  func: function () {
    console.log('hello, deepCopy')
  },
  [Symbol('abc')]: 'Hello, Symbol Key.',
  [Symbol('cba')]: 'Hello, Symbol Key.'
}
obj.self = obj

const newObj = deepCopy(obj)
console.log(newObj)
function isObject(variable) {
  // null | object | array -> object
  // function -> function
  const type = typeof variable
  return variable !== null && (type === 'object' || type === 'function')
}

function deepCopy(originValue, map = new WeakMap()) {
  // 处理Symbol
  if (typeof originValue === 'symbol') {
    return Symbol(originValue.description)
  }

  // 递归的值非对象 直接返回其本身
  if (!isObject(originValue)) {
    return originValue
  }

  // 处理Set
  if (originValue instanceof Set) {
    const set = new Set()
    for (const item of originValue) {
      console.log(item)
      set.add(deepCopy(item))
    }
    return set
  }

  // 处理Function
  if (typeof originValue === 'function') {
    return originValue
  }

  // 如果存在引用循环 且上次已经设置过引用 则跳出递归
  if (map.get(originValue)) {
    return map.get(originValue)
  }

  // 如果是对象/数组类型 需要创建新的对象/数组
  const target = Array.isArray(originValue) ? [] : {} // 判断原始值是数组还是对象
  map.set(originValue, target) // 将新对象自身保存到WeakMap中

  // 遍历当前对象中所有普通的key
  for (const key in originValue) {
    // 递归调用deepCopy 实时创建新对象
    target[key] = deepCopy(originValue[key], map)
  }

  // 遍历对象中的Symbol键
  const symbolKeys = Object.getOwnPropertySymbols(originValue)
  for (const symbol of symbolKeys) {
    target[Symbol(symbol.description)] = deepCopy(originValue[symbol], map)
  }

  return target
}

const obj = {
  name: 'Ziu',
  age: 18,
  friend: {
    name: 'kobe',
    address: {
      name: '洛杉矶',
      detail: '斯坦普斯中心'
    }
  },
  array: [
    { name: 'JavaScript权威指南', price: 99 },
    { name: 'JavaScript高级程序设计', price: 66 }
  ],
  set: new Set(['abc', 'cba', 'nba']),
  func: function () {
    console.log('hello, deepCopy')
  },
  [Symbol('abc')]: 'Hello, Symbol Key.',
  [Symbol('cba')]: 'Hello, Symbol Key.'
}
obj.self = obj

const newObj = deepCopy(obj)
console.log(newObj)

事件总线

js
class ZiuEventBus {
  constructor() {
    this.eventMap = {}
  }

  on(eventName, eventFn) {
    // 获取保存事件回调的数组
    let eventFns = this.eventMap[eventName]
    if (!eventFns) {
      eventFns = []
      this.eventMap[eventName] = eventFns // 一个事件名可以同时注册多个回调函数
    }
    // 将当前回调存入数组
    eventFns.push(eventFn)
  }

  off(eventName, eventFn) {
    let eventFns = this.eventMap[eventName]
    if (!eventFns) return // 如果没有注册过事件回调 直接返回

    for (let i = 0; i < eventFns.length; i++) {
      const fn = eventFns[i]

      // 是同一个函数引用
      if (fn === eventFn) {
        eventFns.splice(i, 1)
        break
      }
    }
  }

  emit(eventName, ...args) {
    let eventFns = this.eventMap[eventName]
    if (!eventFns) return
    eventFns.forEach((fn) => {
      fn(...args)
    })
  }
}

const eventBus = new ZiuEventBus()

const callBack = (payload) => {
  console.log('navClick', payload)
}

// 支持为同一事件注册多个回调
eventBus.on('navClick', callBack)
eventBus.on('navClick', callBack)

const btn = document.querySelector('button')
btn.onclick = () => {
  // 手动触发事件回调
  eventBus.emit('navClick', {
    name: 'Ziu',
    age: 18
  })
}

// 支持移除回调
setTimeout(() => {
  eventBus.off('navClick', callBack) // 移除某个事件的某次回调
}, 5000)
class ZiuEventBus {
  constructor() {
    this.eventMap = {}
  }

  on(eventName, eventFn) {
    // 获取保存事件回调的数组
    let eventFns = this.eventMap[eventName]
    if (!eventFns) {
      eventFns = []
      this.eventMap[eventName] = eventFns // 一个事件名可以同时注册多个回调函数
    }
    // 将当前回调存入数组
    eventFns.push(eventFn)
  }

  off(eventName, eventFn) {
    let eventFns = this.eventMap[eventName]
    if (!eventFns) return // 如果没有注册过事件回调 直接返回

    for (let i = 0; i < eventFns.length; i++) {
      const fn = eventFns[i]

      // 是同一个函数引用
      if (fn === eventFn) {
        eventFns.splice(i, 1)
        break
      }
    }
  }

  emit(eventName, ...args) {
    let eventFns = this.eventMap[eventName]
    if (!eventFns) return
    eventFns.forEach((fn) => {
      fn(...args)
    })
  }
}

const eventBus = new ZiuEventBus()

const callBack = (payload) => {
  console.log('navClick', payload)
}

// 支持为同一事件注册多个回调
eventBus.on('navClick', callBack)
eventBus.on('navClick', callBack)

const btn = document.querySelector('button')
btn.onclick = () => {
  // 手动触发事件回调
  eventBus.emit('navClick', {
    name: 'Ziu',
    age: 18
  })
}

// 支持移除回调
setTimeout(() => {
  eventBus.off('navClick', callBack) // 移除某个事件的某次回调
}, 5000)
`,668),b=[i];function F(u,d,m,C,h,g){return n(),a("div",null,b)}const D=s(y,[["render",F]]);export{A as __pageData,D as default};