# Javascript 基础

本篇深入探索JavaScript基础知识,掌握语言的核心概念和基本原理,加深对JavaScript的理解

# 作用域

注意

Javascript 语言采用的是词法作用域(静态作用域), 而对应的作用域类型还有动态作用域

# 词法作用域

词法作用域意味着作用域是由书写代码时函数声明的位置来决定的。编译的词法分析阶段 基本能够知道全部标识符在哪里以及是如何声明的,从而能够预测在执行过程中如何对它 们进行查找。

# 作用域查找

JS 引擎在作用域中查找变量的过程中, 有两种查找方式:

LHS(Left Hand Side)左侧查找: 变量出现在赋值操作的左侧时进行 LHS 查询 RHS(Right Hand Side)右侧查找: 变量出现在赋值操作的右侧时进行 RHS 查询

在 ES5 中实现块级作用域可以使用try{undefined()}catch(){}, catch 后面的作用域是一个独立的作用域

with中通过var关键词声明的变量会被添加到 with 所处的函数作 用域中(不在 with 内)。

# 欺骗词法

词法作用域完全由写代码期间函数所声明的位置来定义,如果需要运行时来“修 改”(也可以说欺骗)词法作用域使用下面几种方式:

  • eval

eval 的代码是以动态形式插入进来,并对词法作用域的环境进行修改的。引擎只会如往常地进行词法作用域查找。

  • with

with 声明实际上是根据你传递给它的对象凭空创建了一个全新的词法作用域

# 闭包

闭包概念

闭包是一个特殊的对象, 它由两部分组成, 分别是执行上下文 A 和在 A 中创建的函数 B

当 B 执行时, 如果访问了 A 中的变量对象, 那么闭包就会产生

在大多数理解中, 包括著名书籍、文章中都是以函数 B 的名称来指代这里生成的闭包

而在 Chrome 中调用栈中, 则以执行上下文 A 的函数名指代这里的闭包

无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用 域的引用,无论在何处执行这个函数都会使用闭包。

在定时器、事件监听器、 Ajax 请求、跨窗口通信、Web Workers 或者任何其他的异步(或者同步)任务中,只要使 用了回调函数,实际上就是在使用闭包!

严格来讲, 闭包必须在原始词法作用域以外被调用

// 不是闭包
var a = 2
;(function IIFE() {
  console.log(a)
})()

# this 绑定规则

在理解 this 的绑定过程之前,首先要理解调用位置, 调用位置就是函数在代码中被调用的 位置(而不是声明的位置)。只有仔细分析调用位置才能回答这个问题:这个 this 到底引 用的是什么?

在调用位置看函数如何被调用的, 即采用的下面哪种绑定规则, 才能得出 this 引用的是什么;

# 默认绑定

独立函数调用。可以把这条规则看作是无法应用其他规则时的默认规则。如果函数体处于非严格模式下默认规则的 this 指向全局对象, 否则默认规则的 this 指向undefined

例如: 在下面的代码中,foo() 是直接使用不带任何修饰的函数引用进行调用的,因此只能使用 默认绑定,无法应用其他规则。

function foo() {
  console.log(this.a)
}
var a = 2
foo() // 2
function foo() {
  'use strict'
  console.log(this.a)
}
var a = 2
foo() // TypeError: this is undefined

# 隐式绑定

当函 数引用有上下文对象时,隐式绑定规则会把函数调用中的 this 绑定到这个上下文对象。

注意

对象属性引用链中只有上一层或者说最后一层在调用位置中起作用

  • 隐式绑定

下面的例子中, 调用位置会使用 obj 上下文来引用函数, 调用 foo() 时 this 被绑定到 obj

function foo() {
  console.log(this.a)
}
var obj = { a: 2, foo: foo }
obj.foo() // 2
  • 隐式丢失

注意

回调函数丢失 this 绑定是非常常见的。除此之外,还有一种情 况 this 的行为会出乎我们意料:调用回调函数的函数可能会修改 this

下面的例子, 虽然 bar 是 obj.foo 的一个引用,但是实际上,它引用的是 foo 函数本身,因此此时的 bar() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定

function foo() {
  console.log(this.a)
}
var obj = { a: 2, foo: foo }
var bar = obj.foo // 函数别名!
var a = 'oops, global' // a 是全局对象的属性 bar(); // "oops, global"

# 显式绑定

显式绑定即使用call或者apply方法强行将 this 绑定到传入的对象上, 显式绑定还包括硬绑定API调用的上下文

function foo() {
  console.log(this.a)
}
var obj = { a: 2 }
foo.call(obj) // 2
  • 硬绑定

提示

ES 中已经内置了硬绑定函数bind(), 会返回一个硬编码的新函数,它会把你指定的参数设置为 this 的上下文并调用 原始函数。

创建一个可以重复使用的辅助函数:

function foo(something) {
  console.log(this.a, something)
  return this.a + something
}
// 简单的辅助绑定函数
function bind(fn, obj) {
  return function () {
    return fn.apply(obj, arguments)
  }
}

var obj = { a: 2 }
var bar = bind(foo, obj)
var b = bar(3) // 2 3
console.log(b) // 5
  • API 执行上下文

第三方库的许多函数,以及 JavaScript 语言和宿主环境中许多新的内置函数,都提供了一 个可选的参数,通常被称为“上下文”(context),其作用和 bind(..) 一样,确保你的回调 函数使用指定的 this。

例如: forEach(fn,[thisArg]) 第二个参数就是 this 的绑定值;

# New 绑定

在 JavaScript 中,构造函数只是一些 使用 new 操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上, 它们甚至都不能说是一种特殊的函数类型,它们只是被 new 操作符调用的普通函数而已。JS 中实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。

  1. 创建一个全新的对象。
  2. 这个新对象会被执行[[Prototype]]连接。
  3. 这个新对象会绑定到函数调用的 this。
  4. 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象。
function foo(a) {
  this.a = a
}
var bar = new foo(2)
console.log(bar.a) // 2

# 绑定优先级

优先级

绑定的优先级: new 绑定 > 显式绑定 > 隐式绑定 > 默认绑定

function foo(v) {
  this.a = v
}
var obj1 = { foo: foo }
var obj2 = {}
obj1.foo(2)
console.log(obj1.a) // 2

obj1.foo.call(obj2, 3)
console.log(obj2.a) // 3
// 此处已经得出 显式绑定 > 隐式绑定

var bar = new obj1.foo(4)
console.log(obj1.a) // 2
console.log(bar.a) // 4
// 此处已经得出 new绑定 > 隐式绑定
function foo(v) {
  this.a = v
}
var obj1 = { foo: foo }
var obj2 = { a: 222 }

const baz = new (obj1.foo.bind(obj2))(333)
console.log(baz.a) //333
// 此处可以得出 new绑定 > 显式绑定

# 判断规则

判断 this 现在我们可以根据优先级来判断函数在某个调用位置应用的是哪条规则。可以按照下面的 顺序来进行判断:

  1. 函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象。 var bar = new foo()
  2. 函数是否通过 call、apply(显式绑定)或者硬绑定调用?如果是的话,this 绑定的是 指定的对象。 var bar = foo.call(obj2)
  3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上 下文对象。 var bar = obj1.foo()
  4. 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到 undefined,否则绑定 到全局对象。 var bar = foo() 就是这样。对于正常的函数调用来说,理解了这些知识你就可以明白 this 的绑定原理了。

# 例外情况

  • this 被忽略

当把 null 或者 undefined 作为 this 的绑定对象传入 call、apply 或者 bind, 值被忽略, 变成默认绑定; 其实更安全的做法是通过Object.create(null)作为忽略时候的参数

function foo(a, b) {
  console.log('a:' + a + ', b:' + b)
}
// 我们的 DMZ 空对象
var ø = Object.create(null) // 把数组展开成参数
foo.apply(ø, [2, 3]) // a:2, b:3
// 使用 bind(..) 进行柯里化
var bar = foo.bind(ø, 2)
bar(3) // a:2, b:3
  • 间接引用

创建一个函数的“间接引用”,在这 种情况下,调用这个函数会应用默认绑定规则。

function foo() {
  console.log(this.a)
}
var a = 2
var o = { a: 3, foo: foo }
var p = { a: 4 }
o.foo() // 3
;(p.foo = o.foo)() // 2
  • 箭头函数

箭头函数不使用 this 的四种标准规则, 而是根据外层(函数或者全局)作用域来决定 this; 具体来说, 箭头函数会继承外层函数调用的 this 绑定(无论 this 绑定到什么)。这 其实和 ES6 之前代码中的 self = this 机制一样。

function foo() {
  // 返回一个箭头函数
  return (a) => {
    // 箭头函数中的this 继承自 foo()
    console.log(this.a)
  }
}
var obj1 = {
  a: 2
}
var obj2 = {
  a: 3
}
var bar = foo.call(obj1)
bar.call(obj2) // 2, 不是 3 !

# 原型继承关系

在 JS 的继承中, 可以用一言蔽之, 都符合通用的继承关系, 即:

截屏2022-06-13 17.39.44.png

# ES5 继承实现

主要分为四类继承

  • 原型继承
  • 构造继承
  • 组合继承
  • 寄生组合继承

# 原型继承

缺点: 无法传递参数

function People() {
  this.type = 'prople'
}
People.prototype.eat = function () { console.log(' ');

function Man(name) {
  this.name = name;
  this.color = 'black';
}

// 继承实现
Man.prototype = new People();
Man.prototype.construct = Man

# 构造继承

缺点: 未继承父类原型的属性和方法, 无法访问类原型的属性和方法

// 父类
function People(type) {
  this.type = type
}
People.prototype.eat = function () { console.log(' ');

// 子类
function Man(type, name) {
  // 借用构造函数继承
  People.call(this, type)
  this.name = name;
  this.color = 'black';
}

# 组合继承

缺点: 调用了多次父类构造函数, 浪费内存

// 父类
function People(type) {
  this.type = type
}
People.prototype.eat = function () { console.log(' ');

// 子类
function Man(type, name) {
  // 借用构造函数继承
  People.call(this, type)
  this.name = name;
  this.color = 'black';
}
Man.prototype = new People();
Man.prototype.construct = Man

# 寄生组合继承

这是一种最佳的继承方式

// 父类
function People(type) {
  this.type = type
}
People.prototype.eat = function () { console.log(' ');

// 子类
function Man(type, name) {
  // 借用构造函数继承
  People.call(this, type)
  this.name = name;
  this.color = 'black';
}
Man.prototype = Object.create(People.prototype, {
  constructor: {
  value: Man
  }
})

ES6 提供了新的数据结构,平时用得少, 总是搞忘, 所以算是一个记录

  • Set: 它类似于数组,但是成员的值都是唯一的,没有重复的值
  • WeakSet: 弱化版的 Set, 对象都是弱引用, 不影响 GC 的回收
  • Map: Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现
  • WeakMap: 弱化版的 Map, 对象都是弱引用, 不影响 GC 的回收

# 深入async转换

ES6中, async / await极大简写了异步代码, 它其实只是一个Generator的语法糖。如何理解async / await, 应该根据async返回值await的右侧值来区分

# 根据 async 返回值区分

可以把async的返回值分为三类, 即thenable对象、promise对象、其他值(非thenable、非promise)三类。 根据不同的返回值, 微任务队列会等待不同的then时间

# thenable

如果async函数体返回值为一个类thenable对象, 在标准实现中, 它的执行过程会等待一个then(一个微任务)的时间

async function testB() {
  return {
    then(cb) {
      cb();
    },
  };
}

// 等价于
function testB1() {
  return Promise.resolve((v) => v).then((cb) => {
    cb();
  });
}

# promise

如果async函数体返回值为一个promise对象, 在标准实现中, 它的执行过程会等待两个then(2个微任务)的时间

async function testC() {
  return new Promise((resolve, reject) => {
    resolve();
  });
}

//等价于
function testC1() {
  return new Promise((resolve, reject) => {
    resolve();
  })
    .then((v) => v)
    .then((v) => v);
}

# 其他值

如果async函数体返回值是一个普通的值, 既不是thenable也不是promise, 那么会按照普通程序处理, 不等待

async function testA() {
  return 1;
}
// 等价于
function testA1() {
  return Promise.resolve(1);
}

# 根据 await 右侧值区分

await右侧值也分为上述三种情况, 我们来一一分析

# thenable

如果await右值是一个thenable值, 需要等待一个 then 的时间之后执行

async function test() {
  console.log(1);
  await {
    then(cb) {
      cb();
    },
  };
  console.log(2);
}

//等价于
function test() {
  console.log(1);
  Promise.resolve((v) => v)
    // 等待的这个then
    .then((cb) => {
      cb();
    })
    .then(() => {
      console.log(2);
    });
}

# promise

如果await右值是一个promise值, 要求这个promise有确定的返回值。

这种情况, 按照理论应该等价于2then 的新 promise , 早期版本也确实如此, 但是经过 TC 39(ECMAScript标准制定者) 对 await 后面是 promise 的情况如何处理进行了一次修改,移除了额外的两个微任务,在早期版本,依然会等待两个 then 的时间

但是最新的标准中, 移除了额外的两个微任务,现在 await 后面接 promise 类型会立即向微任务队列添加一个微任务then,且不需等待

究其原因, 有大佬翻译了官方解释:更快的 async 函数和 promises[1] (opens new window),但在这次更新中并没有修改 thenable 的情况

async function test() {
  console.log(1);
  await new Promise((resolve, reject) => {
    resolve();
  });
  console.log(2);
}

// 现在等价于第一种方式
function test() {
  console.log(1);
  new Promise((resolve, reject) => {
    resolve();
  }).then(() => {
    console.log(2);
  });
}

# 其他值

如果await右值是一个普通的值, 既不是thenable也不是promise, 会立即向微任务队列添加一个微任务then,但不需等待

async function test() {
  console.log(1);
  await 123;
  console.log(2);
}

// 等价于
function test() {
  console.log(1);
  Promise.resolve(123).then(() => {
    console.log(2);
  });
}

# 一些特殊情况

如果Promisethen回调中, 返回一个Promise实例, 那执行过程是怎么样的?

Promise.resolve()
  .then(() => {
    console.log('0')
    // Promise A+ 标准未明确规定返回值, 交由浏览器去实现, 因此V8源码中明确规定了返回值
    // 根据V8源码分析得知:
    // 等同于 return Promise.resolve(4).then(()=>noop).then(()=>noop)
    return Promise.resolve(4)
  })
  .then((res) => {
    console.log(res)
  })

Promise.resolve()
  .then(() => {
    console.log('1')
  })
  .then(() => {
    console.log('2')
  })
  .then(() => {
    console.log('3')
  })
  .then(() => {
    console.log('5')
  })
  .then(() => {
    console.log('6')
  })

查阅ECMA标准之后, 发现并没有标准化, 而是交由浏览器厂商实现。 目前的Chrome v116版本的源码中, 明确了需要等待两个空then的周期, 因此

上述的输出结果为 0 1 noop 2 noop 3 4 5 6 (里面的noop只是更形象的展示顺序, 并不会真的打印)

# 新数据结构

# Set

Set 是构造函数, 参数接受一个数组, 数组成员可以是原始类型也可以是引用类型

const items = new Set([1, 2, 3, 4, 5, 5, 5, 5])

Set 包含的实例方法及属性:

  • Set.prototype.constructor:构造函数,默认就是 Set 函数
  • Set.prototype.size:返回 Set 实例的成员总数
  • Set.prototype.add(value):添加某个值,返回 Set 结构本身
  • Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功
  • Set.prototype.has(value):返回一个布尔值,表示该值是否为 Set 的成员
  • Set.prototype.clear():清除所有成员,没有返回值
  • Set.prototype.keys():返回键名的遍历器
  • Set.prototype.values():返回键值的遍历器
  • Set.prototype.entries():返回键值对的遍历器
  • Set.prototype.forEach():使用回调函数遍历每个成员,

无法通过遍历来直接修改原来的 set 结构,只能映射一个新的重新赋值回去

// 方法一
let set = new Set([1, 2, 3])
set = new Set([...set].map((val) => val * 2))
// set的值是2, 4, 6

// 方法二
let set = new Set([1, 2, 3])
set = new Set(Array.from(set, (val) => val * 2))
// set的值是2, 4, 6

# WeakSet

WeakSet 的成员只能是对象,而不能是其他类型的值, 且每个元素的引用都是弱引用, 会被 GC 回收

const a = [
  [1, 2],
  [3, 4]
]
const ws = new WeakSet(a)

WeakSet 结构有以下三个方法。

  • WeakSet.prototype.add(value):向 WeakSet 实例添加一个新成员。
  • WeakSet.prototype.delete(value):清除 WeakSet 实例的指定成员。
  • WeakSet.prototype.has(value):返回一个布尔值,表示某个值是否在 - WeakSet 实例之中。

WeakSet 没有遍历方法以及 size 属性,因为是弱引用,随时引用都可是失效, 主要的作用是保存临时的引用, 例如储存 DOM 节点,而不用担心这些节点从文档移除时,会引发内存泄漏

# Map

ES6 提供了 Map 数据结构, 它是一种值-值的对应集合, 也可以认为是增强型的 Object, 可以通过两种方式添加元素

  • 通过 set 方法添加
onst m = new Map();
const o = {p: 'Hello World'};

m.set(o, 'content')
m.get(o) // "content"

m.has(o) // true
m.delete(o) // true
m.has(o) // false
  • 通过构造函数一次性添加

任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构(详见《Iterator》一章)都可以当作 Map 构造函数的参数

// 数组构造
const map = new Map([
  ['name', '张三'],
  ['title', 'Author']
])

map.size // 2
map.has('name') // true
map.get('name') // "张三"
map.has('title') // true
map.get('title') // "Author"

// set构造
const set = new Set([
  ['foo', 1],
  ['bar', 2]
])
const m1 = new Map(set)
m1.get('foo') // 1

// map构造
const m2 = new Map([['baz', 3]])
const m3 = new Map(m2)
m3.get('baz') // 3

Map 包含的实例方法及属性为:

  • Map.prototype.size:属性返回 Map 结构的成员总数
  • Map.prototype.set(key, value):set 方法设置键名 key 对应的键值为 value,然后返回整个 Map 结构。如果 key 已经有值,则键值会被更新,否则就新生成该键
  • Map.prototype.get(key): get 方法读取 key 对应的键值,如果找不到 key,返回 undefined
  • Map.prototype.has(key): has 方法返回一个布尔值,表示某个键是否在当前 Map 对象之中
  • Map.prototype.delete(key):delete 方法删除某个键,返回 true。如果删除失败,返回 false
  • Map.prototype.clear(): clear 方法清除所有成员,没有返回值
  • Map.prototype.keys():返回键名的遍历器
  • Map.prototype.values():返回键值的遍历器
  • Map.prototype.entries():返回所有成员的遍历器
  • Map.prototype.forEach():遍历 Map 的所有成员
//由于map[Symbol.iterator] === map.entries,以下两者等效
for (let [key, value] of map.entries()) {
  console.log(key, value)
}
// "F" "no"
// "T" "yes"

// 等同于使用map.entries()
for (let [key, value] of map) {
  console.log(key, value)
}
// "F" "no"
// "T" "yes"

注意 Map 的键名有非字符串,可以选择转为数组 JSON

function mapToArrayJson(map) {
  return JSON.stringify([...map])
}

let myMap = new Map().set(true, 7).set({ foo: 3 }, ['abc'])
mapToArrayJson(myMap)
// '[[true,7],[{"foo":3},["abc"]]]'

# WeakMap

WeakMap 只接受对象作为键名(null 除外),不接受其他类型的值作为键名,WeakMap 的键名所指向的对象,不计入垃圾回收机制(键值不是弱引用)

const map = new WeakMap()
map.set(1, 2)
// TypeError: 1 is not an object!
map.set(Symbol(), 2)
// TypeError: Invalid value used as weak map key
map.set(null, 2)
// TypeError: Invalid value used as weak map key

WeakMap 就是为了解决 Map 对象必须手动删除引用的问题而诞生的,它的键名所引用的对象都是弱引用, 主要用于 DOM 节点存储这种类似引用可能消失的内存回收模型,防止内存泄露

WeakMap 只有四个方法可用:get()set()has()delete(),且没有遍历方法和size属性

# 作用域分类

JS 总共有 9 种作用域,我们通过调试的方式来分析了下:

  • Global 作用域: 全局作用域,在浏览器环境下就是 window,在 node 环境下是 global
  • Local 作用域:本地作用域,或者叫函数作用域
  • Block 作用域:块级作用域
  • Script 作用域:let、const 声明的全局变量会保存在 Script 作用域,这些变量可以直接访问,但却不能通过 window.xx 访问
  • Module 作用域:es module 模块运行的时候会生成 Module 作用域,而 commonjs 模块运行时严格来说也是函数作用域,因为 node 执行它的时候会包一层函数,算是比较特殊的函数作用域,有 module、exports、require 等变量
  • Catch Block 作用域: catch 语句的作用域可以访问错误对象
  • With Block 作用域:with 语句会把传入的对象的值放到单独的作用域里,这样 with 语句里就可以直接访问了
  • Closure 作用域:函数返回函数的时候,会把用到的外部变量保存在 Closure 作用域里,这样再执行的时候该有的变量都有,这就是闭包。eval 的闭包比较特殊,会把所有变量都保存到 Closure 作用域
  • Eval 作用域:eval 代码声明的变量会保存在 Eval 作用域

# 函数分类

函数分类众多,高阶函数(HOF)的应用广泛, 本小节记录主要是以下几种高阶函数

  • 偏函数
  • 惰性函数
  • 柯里化函数
  • Thunk 函数

# 偏函数

定义

偏函数(partial)是固定一个函数的一个或者多个参数,也就是将一个 n 元函数转换成一个 n - x 元函数

function add(a, b) {
  return a + b
}

// 执行 add 函数,一次传入两个参数即可
add(1, 2) // 3

// 假设有一个 partial 函数可以做到局部应用
var addOne = partial(add, 1)

addOne(2) // 3

# 惰性函数

定义

当我们每次都需要进行条件判断,其实只需要判断一次,接下来的使用方式都不会发生改变的时候,将不变的地方固定化(局部化)

var foo = (function () {
  var t
  return function () {
    if (t) return t
    t = new Date()
    return t
  }
})()

# 柯里化函数

定义

将一个多参数函数转换成多个单参数函数,也就是将一个 n 元函数转换成 n 个一元函数

function add(a, b) {
  return a + b
}

// 执行 add 函数,一次传入两个参数即可
add(1, 2) // 3

// 假设有一个 curry 函数可以做到柯里化
var addCurry = curry(add)
addCurry(1)(2) // 3

# Thunk 函数

上个世纪 60 年代引出了一个编程

函数的参数到底应该何时求值, 即求值策略

最后形成了两种方案传值调用(C 语言、JS)和传名调用(Haskell 语言)

传名调用的实现

编译器的“传名调用”实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数

Javascript 的实现

Thunk 函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数, 这个单参数的函数就叫 Thunk 函数

// 正常版本的readFile(多参数版本)
fs.readFile(fileName, callback)

// Thunk版本的readFile(单参数版本)
var Thunk = function (fileName) {
  return function (callback) {
    return fs.readFile(fileName, callback)
  }
}

var readFileThunk = Thunk(fileName)
readFileThunk(callback)

# 对象迭代

ES6 对象的属性主要分为几种类别: 自身的继承来的可枚举的不可枚举的普通属性Symbol属性。 而这些属性涉及到的遍历API主要有如下几种:

  • for...in 自身的 + 继承的 + 可枚举的 + 普通属性

    for...in 循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。

  • Object.keys(obj) 自身的 + 可枚举的 + 普通属性

    Object.keys 返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名。

  • Object.getOwnPropertyNames(obj) 自身的 + 可枚举的 + 不可枚举的 + 普通属性

    Object.getOwnPropertyNames 返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。

  • Object.getOwnPropertySymbols(obj) 自身的 + 可枚举的 + 不可枚举的 + Symbol属性

    Object.getOwnPropertySymbols 返回一个数组,包含对象自身的所有 Symbol 属性的键名。

  • Reflect.ownKeys(obj) 自身的 + 可枚举的 + 不可枚举的 + 普通属性 + Symbol属性

    Reflect.ownKeys 返回一个数组,包含对象自身的所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。

# Generator

  • 可以暂停执行和恢复执行
  • 函数体内外的数据交换
  • 错误处理机制

# 执行流程

Generator 调用返回的是一个 Iterator 迭代器对象,只有调用该对象的 next 方法才 开始执行, 同时遇到 yield 将停下, 返回一个值对象; 每次 next 调用返回的是 yield 后面的值, 而 yield 整个表达式的值默认为 undefined; next 方法传入参数, 该参数可以将 上一次 的 yield 表达式的值设置为该值

function* hw() {
  yield 123 + 456
  var r = yield 888
  console.log(r)
  yield 'end'
}

//执行1   先返回一个Iterator迭代器对象
var p = hw()
/* 执行2
 *        使用next启动Generator的执行,遇见yield就停止,同时返回
 *        yield后面的计算值 { value: '579', done: false }
 */
p.next()
/* 执行3
 *        开始下一次执行,从上次yield表达式停下的地方,一直执行到下
 *        一个yield表达式,遇见yield就停止,又返回yield后的计算值
 *        { value: '888', done: false }
 */
p.next()
/* 执行4
 *        将 yield 888整个表达式的值设置为next传入的参数,并赋值给r
 *        打印r的值为  "自定义传参"
 *        { value: 'end', done: false }
 */
p.next('自定义传参')
// 执行5   结束Generator的执行  { value: 'undefined', done: true }
p.next()

# 自动执行

注意

await 后面的函数可以是promise 对象也可以是普通 function,而 yield 关键字后面必须得是 thunk 函数或 promise 对象

目前自动执行的方案主要有以下两种:

  • Thunk 函数 将异步操作包装成 Thunk 函数,在回调函数里面交回执行权
  • Promise 对象 异步操作包装成 Promise 对象,用 then 方法交回执行权
  • 完善版 同时支持异步与同步处理

# Thunk 函数实现

function run(fn) {
  var gen = fn()

  function next(err, data) {
    var result = gen.next(data)
    if (result.done) return
    result.value(next)
  }

  next()
}

function* g() {
  // ...
}

run(g)

# Promise 对象实现

function run(gen) {
  var g = gen()

  function next(data) {
    var result = g.next(data)
    if (result.done) return result.value
    result.value.then(function (data) {
      next(data)
    })
  }

  next()
}

run(gen)

// promise简单实现
function run(gen) {
  var gen = gen()

  return new Promise(function (resolve, reject) {
    function next(data) {
      try {
        var result = gen.next(data)
      } catch (e) {
        return reject(e)
      }

      if (result.done) {
        return resolve(result.value)
      }

      var value = toPromise(result.value)

      value.then(
        function (data) {
          next(data)
        },
        function (e) {
          reject(e)
        }
      )
    }

    next()
  })
}

function isPromise(obj) {
  return 'function' == typeof obj.then
}

// obj可以是promise也可以是回调函数,所以统一转化成promise
function toPromise(obj) {
  if (isPromise(obj)) return obj
  if ('function' == typeof obj) return thunkToPromise(obj)
  return obj
}

function thunkToPromise(fn) {
  return new Promise(function (resolve, reject) {
    fn(function (err, res) {
      if (err) return reject(err)
      resolve(res)
    })
  })
}

module.exports = run

综合来看, 需要处理两种情况, 即 yield 后面是回调函数或者 promise 等两种情况

// 回调函数的情况
function fetchData(url) {
  return function (cb) {
    setTimeout(function () {
      cb({ status: 200, data: url })
    }, 1000)
  }
}

function* gen() {
  var r1 = yield fetchData('https://api.github.com/users/github')
  var r2 = yield fetchData('https://api.github.com/users/github/followers')

  console.log([r1.data, r2.data].join('\n'))
}

# 完善版

function run(gen) {
  return new Promise(function (resolve, reject) {
    if (typeof gen == 'function') gen = gen()

    // 如果 gen 不是一个迭代器
    if (!gen || typeof gen.next !== 'function') return resolve(gen)

    onFulfilled()

    function onFulfilled(res) {
      var ret
      try {
        ret = gen.next(res)
      } catch (e) {
        return reject(e)
      }
      next(ret)
    }

    function onRejected(err) {
      var ret
      try {
        ret = gen.throw(err)
      } catch (e) {
        return reject(e)
      }
      next(ret)
    }

    function next(ret) {
      if (ret.done) return resolve(ret.value)
      var value = toPromise(ret.value)
      if (value && isPromise(value)) return value.then(onFulfilled, onRejected)
      return onRejected(
        new TypeError(
          'You may only yield a function, promise ' +
            'but the following object was passed: "' +
            String(ret.value) +
            '"'
        )
      )
    }
  })
}

function isPromise(obj) {
  return 'function' == typeof obj.then
}

function toPromise(obj) {
  if (isPromise(obj)) return obj
  if ('function' == typeof obj) return thunkToPromise(obj)
  return obj
}

function thunkToPromise(fn) {
  return new Promise(function (resolve, reject) {
    fn(function (err, res) {
      if (err) return reject(err)
      resolve(res)
    })
  })
}

更多可以了解 TJ 大神的CO模块 (opens new window)