二、看《JavaScript高级程序设计》笔记

125 阅读14分钟

生成器

生成器是什么?

生成器的形式是一个函数, 它拥有在函数代码块中暂停(yield关键字)和恢复代码块(next函数)执行的能力

/**
 * 生成器定义的方式
 * @returns {Generator<*, void, *>}
 */
function* generatorFn01() {
   
}

let generatorFn02 = function* () {
   
}

let foo = {
   * generatorFn03() {
   }
}

class Bar {
   static* generatorFn04() {
   }
}

生成器使用 星号(*) 加上函数的方式定义一个生成器

注意: 箭头函数不能定义生成器函数

注意: 生成器函数的左右边空格无所谓多少

// 等价的生成器函数
function* generatorFnA() {}
function *generatorFnB() {}
function * generatorFnC() {}  
// 等价的生成器方法:
class Foo {
   *generatorFnD() {}
   * generatorFnE() {}
}

生成器函数会产生一个生成器对象, 生成器对象一开始处于暂停执行(suspend)的状态, 与迭代器相同, 生成器也实现了 iterator 的方式, 同样具有 next 函数, 而next函数的功能会让生成器开始执行或者恢复执行

next 函数起到两个功能,

  1. 启动生成器(特别注意第一次调用next函数, 它仅仅是启动生成器, 所以传递给next函数的参数是无效的)
  2. 恢复yield暂停的代码块

而 next 的返回值和 iterator 相似, 同样也有 value 和 done 属性, 最后一次调用 next 函数 done 就会变成 true , 而 value 的值默认是undefined

生成器只会在初次调用 next 的时候开始执行

生成器对象实现了 iterator 接口, 它默认迭代器是自引用的

function* generatorFn() {
}
console.log(generatorFn());
console.log(generatorFn()[Symbol.iterator]());
console.log(generatorFn()[Symbol.iterator]);
let g = generatorFn();
console.log(g === g[Symbol.iterator]()); // true

通过 yield 中断执行

yield关键字可以让生成器停止和开始执行, 也是生成器最有用的地方。 生成器函数在遇到yield 关键字之前会正常执行。遇到这个关键字后,执行会停止,函数作用域的状态会被保留。停止执行的生 成器函数只能通过在生成器对象上调用next()方法来恢复执行:

function * generatorFn() {
    yield
}

let generatorFn1 = generatorFn();
console.log(generatorFn1.next())
console.log(generatorFn1.next())

yield 类似于 return 但又不是 return, 而是类似于 不直接结束函数的 return , 他会把 yield 之后的元素返回出去, 然后函数暂时被挂起, 函数的状态被保存, 直到我们调用 next 函数接收 yield 的返回值之后, 则继续恢复先前调用 yield 的函数中继续执行

function * generatorFn() {
    yield 10
    yield 20
    return 30
}

let generatorFn1 = generatorFn();
/*
{ value: 10, done: false }
{ value: 20, done: false }
{ value: 30, done: true }
 */
console.log(generatorFn1.next())
console.log(generatorFn1.next())
console.log(generatorFn1.next())

在生成器函数内部, 借助 yield 临时返回的对象, 内部有一个 done: false 的属性, 如果 yield 是最后一个该属性就会变成done: true

每个生成器对象有属于自己的作用域空间, 生成器对象和生成器对象之间存在独立性. 不会相互影响

yield 只能在生成器函数内部使用, 不能在其他普通函数中使用, 如果使用则会报错

yield只能在生成器函数中调用

如果把生成器对象当成可迭代对象, 那么使用起来会更方便

在需要自定义迭代对象时, 这样使用生成器对象会特别有用

我们需要定义一个可迭代对象, 而它会产生一个迭代器, 这个迭代器会执行指定的次数。

function* nTimes(n) {  
   while(n--) {
      yield; 
   }
}

我们知道迭代器内部有个next函数,所以for of时会调用next函数,而生成函数也是,也会调用next进行循环

for (let _ of nTimes(3)) {
	console.log('foo');
} // foo // foo // foo

这内部的nTime生成器函数只会被调用一次进行初始化,后面全部调用的隐藏的next函数

使用yield实现输入和输出

yield关键字,挂起函数执行,等待对应的generator对象执行next

让 生成器函数暂停的yield关键字会接收到传给next()方法的第一个值。

第一次调用next()传入的值不会被使用,因为这一次调用是为了开始执行生成器函数

第一次调用next的功能是启动生成器,后面几次就不是了,后面几次给next传递参数时,值会反应到生成器函数的挂起点

function* generatorFn(initial) {
   console.log(initial);
   console.log(yield);
   console.log(yield);
}

let generatorObject = generatorFn('foo');
generatorObject.next('bar'); // foo
// generatorObject.next('baz'); // baz
// generatorObject.next('qux'); // qux

这段代码很有意思, 但也比较难以理解, 我先说说怎么理解

yield 的功能有两个, 第一个是暂停函数直到generator对象调用next函数, 第二个是接受next函数参数的返回值

如果不理解接受 next 参数的值, 我们可以把代码改造下

function* generatorFn() {
   let tmp = yield;
   console.log(tmp);
}

let generatorObject = generatorFn();
// 这是启动生成器, 所以不会有任何的打印
generatorObject.next('zhazha');
generatorObject.next('qux'); // qux

看上面generatorFn这个函数的改造, 非常好理解了

function* generatorFn() {
   return yield 'foo';
}

let generatorObject = generatorFn();
console.log(generatorObject.next()); // { done: false, value: 'foo' }
console.log(generatorObject.next('bar')); // { done: true, value: 'bar' }

因为函数必须对整个表达式求值才能确定要返回的值,所以它在遇到yield关键字时暂停执行并计算出要产生的值:"foo"。下一次调用next()传入了"bar",作为交给同一个yield的值。然后这 个值被确定为本次生成器函数要返回的值。

这段可以帮助理解使用yield实现输入和输出

yield关键字并非只能使用一次

可以使用星号增强yield的行为,让它能够迭代一个可迭代对象

*的作用就是申明一个生成器,而生成器类似于迭代器

// 等价的 generatorFn
// function* generatorFn() {
//     for (const x of [1, 2, 3]) {
//        yield x;
//     }
// }
function* generatorFn() {
   yield* [1, 2, 3];
}
// 这里返回一个生成器, 然后被当成了迭代器
let generatorObject = generatorFn();
// 这里遍历迭代器
for (const x of generatorFn()) {
   console.log(x);
} // 1 // 2 // 3

生成器类似于迭代器, 是可以迭代的, 唯一的区别就是多了个挂起的功能

使用yield*实现递归算法

yield其实很想一个临时return,但底层不是这样,底层是生成器对象,其本质是一个迭代器,只要遇到yield,就暂停当前函数保留场景上下文,然后在等待对象被调用next函数,只要调用了next函数,就可以重新启动yield,此时yield还会判断是否有next参数值的输入,如果有则yield返回值

function* nTimes(n) {
   if (n > 0) {
      yield* nTimes(n - 1);
      yield n - 1;
   }
}

for (const x of nTimes(3)) {
   console.log(x);
} // 0 // 1 // 2

不断地yield进入nTime函数然后等到没值的时候则执行下面的yield n - 1

生成器作为默认迭代器

for-of循环调用了默认迭代器(它恰好又是一个生成器函数)并产生了一个生成器对象。 这个生成器对象是可迭代的,所以完全可以在迭代中使用。

生成器对象除了有这两个方法,还有第三 个方法:throw()

return()方法会强制生成器进入关闭状态

与迭代器不同,所有生成器对象都有return()方法,只要通过它进入关闭状态,就无法恢复了。 后续调用next()会显示done: true状态,而提供的任何返回值都不会被存储或传播

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

throw()方法会在暂停的时候将一个提供的错误注入到生成器对象中

console.log(g); // generatorFn {<suspended>}
try {
   g.throw('foo');
} catch (e) {
   console.log(e); // foo 
}
console.log(g); // generatorFn {<closed>}

不过,假如生成器函数内部处理了这个错误,那么生成器就不会关闭,而且还可以恢复执行。错误 处理会跳过对应的yield,因此在这个例子中会跳过一个值

function* generatorFn() {
   for (const x of [1, 2, 3]) {
      try {
         yield x;
      } catch (e) {
      }
   }
}

const g = generatorFn();
console.log(g.next()); // { done: false, value: 1} g.throw('foo'); 
console.log(g.next()); // { done: false, value: 3}

在这个例子中,生成器在try/catch块中的yield关键字处暂停执行。在暂停期间,throw()方 法向生成器对象内部注入了一个错误:字符串"foo"。这个错误会被yield关键字抛出。 因为错误是在 生成器的try/catch块中抛出的, 所以仍然在生成器内部被捕获。 可是, 由于yield抛出了那个错误, 生成器就不会再产出值2。此时,生成器函数继续执行,在下一次迭代再次遇到yield关键字时产出了 值3。

如果生成器对象还没有开始执行, 那么调用throw()抛出的错误不会在函数内部被 捕获,因为这相当于在函数块外部抛出了错误。

person.name = "Nicholas"; 
person.age = 29; 
person.job = "Software Engineer"; 
person.sayName = function() {  console.log(this.name); };

以前创建对象的方式

let person = {
   name: "Nicholas", age: 29, job: "Software Engineer", sayName() {
      console.log(this.name);
   }
};

现在创建对象的方式

对象

属性

属性分两种:数据属性和访问器属性。

内部特性

为了将某个特性标识为内部特性,规范会用 两个中括号把特性的名称括起来,比如[[Enumerable]]

数据属性

数据属性包含一个保存数据值的位置。值会从这个位置读取,也会写入到这个位置。数据属性有 4 个特性描述它们的行为。

  • [[Configurable]]:表示属性是否可以通过delete删除并重新定义,是否可以修改它的特 性,以及是否可以把它改为访问器属性。默认情况下,所有直接定义在对象上的属性的这个特 性都是true,如前面的例子所示。
  • [[Enumerable]]:表示属性是否可以通过for-in循环返回。默认情况下,所有直接定义在对 象上的属性的这个特性都是true,如前面的例子所示。
  • [[Writable]]:表示属性的值是否可以被修改。默认情况下,所有直接定义在对象上的属性的 这个特性都是true,如前面的例子所示。
  • [[Value]]:包含属性实际的值。这就是前面提到的那个读取和写入属性值的位置。这个特性 的默认值为undefined

let person = { name: "Nicholas" };

这里, 我们创建了一个名为name的属性, 并给它赋予了一个值"Nicholas"。 这意味着[[Value]] 特性会被设置为"Nicholas",之后对这个值的任何修改都会保存这个位置。

要修改属性的默认特性,就必须使用Object.defineProperty()方法。这个方法接收 3 个参数: 要给其添加属性的对象、属性的名称和一个描述符对象。最后一个参数,即描述符对象上的属性可以包 含:configurableenumerablewritablevalue,跟相关特性的名称一一对应

let person = {};
Object.defineProperty(person, "name", {
   writable: false, value: "Nicholas"
});
console.log(person.name); // "Nicholas"
person.name = "Greg";
console.log(person.name); // "Nicholas"

属性的内部特性

在非严格模式下尝试给这个属性重新赋值会被忽略。在严格模式下,尝试修改只读属性 的值会抛出错误。

这个例子把configurable设置为false,意味着这个属性不能从对象上删除。非严格模式下对 这个属性调用delete没有效果,严格模式下会抛出错误。

一个属性被定义为不可配置之后,就 不能再变回可配置的了

在调用Object.defineProperty()时,configurableenumerablewritable的值如果不 指定,则都默认为false

访问器属性

访问器属性不包含数据值

它们包含一个获取(getter)函数和一个设置(setter)函数,不 过这两个函数不是必需的

在读取访问器属性时,会调用获取函数,这个函数的责任就是返回一个有效 的值。在写入访问器属性时,会调用设置函数并传入新值,这个函数必须决定对数据做出什么修改

出什么修改。访 问器属性有 4 个特性描述它们的行为。

  • [[Configurable]]:表示属性是否可以通过delete删除并重新定义,是否可以修改它的特 性,以及是否可以把它改为数据属性。默认情况下,所有直接定义在对象上的属性的这个特性 都是true
  • [[Enumerable]]:表示属性是否可以通过for-in循环返回。默认情况下,所有直接定义在对 象上的属性的这个特性都是true
  • [[Get]]:获取函数,在读取属性时调用。默认值为undefined
  • [[Set]]:设置函数,在写入属性时调用。默认值为undefined

访问器属性是不能直接定义的,必须使用Object.defineProperty()

获取函数和设置函数不一定都要定义。只定义获取函数意味着属性是只读的,尝试修改属性会被忽 略。在严格模式下,尝试写入只定义了获取函数的属性会抛出错误。类似地,只有一个设置函数的属性 是不能读取的,非严格模式下读取会返回undefined,严格模式下会抛出错误。

定义多个属性

在一个对象上同时定义多个属性的可能性是非常大的。 为此, ECMAScript 提供了Object.defineProperties()方法

使用Object.getOwnPropertyDescriptor()方法可以取得指定属性的属性描述符

返回值是一个对象,对于访问器属性包含 configurableenumerablegetset属性,对于数据属性包含configurableenumerablewritablevalue属性

console.log(`name = ${name}, age = ${age}, job = ${job}`);
let descriptor = Object.getOwnPropertyDescriptor(book, "year_");
console.log(descriptor.value); // 2017
console.log(descriptor.configurable); // false
console.log(typeof descriptor.get); // "undefined"
let descriptor = Object.getOwnPropertyDescriptor(book, "year"); 
console.log(descriptor.value); // undefined
console.log(descriptor.enumerable); // false
console.log(typeof descriptor.get); // "function"

ECMAScript 2017 新增了Object.getOwnPropertyDescriptors()静态方法。这个方法实际上 会在每个自有属性上调用Object.getOwnPropertyDescriptor()并在一个新对象中返回它们

console.log(Object.getOwnPropertyDescriptors(book));

合并对象提供了Object.assign()方法。 这个方法接收一个目标对象和一个 或多个源对象作为参数, 然后将每个源对象中可枚举 (Object.propertyIsEnumerable()返回true) 和自有(Object.hasOwnProperty()返回true)属性复制到目标对象。以字符串和符号为键的属性 会被复制。对每个符合条件的属性,这个方法会使用源对象上的[[Get]]取得属性的值,然后使用目标 对象上的[[Set]]设置属性的值。

在一条语句中使用嵌套数据实现一个或多个赋值操作

对象解构

// 使用对象解构
let person = {name: 'Matt', age: 27};
let {name: personName, age: personAge} = person;
console.log(personName); // Matt
console.log(personAge); // 27
let person = {name: 'Matt', age: 27};
let {name, age} = person;
console.log(name); // Matt
console.log(age); // 27

解构赋值不一定与对象的属性匹配

赋值的时候可以忽略某些属性,而如果引用的属性不存在,则 该变量的值就是undefined

let person = {name: 'Matt', age: 27};
let {name, job} = person;
console.log(name); // Matt
console.log(job); // undefined

在解构赋值的同时定义默认值,这适用于前面刚提到的引用的属性不存在于源对象中的 情况

let person = {name: 'Matt', age: 27};
let {name, job = 'Software engineer'} = person;
console.log(name); // Matt
console.log(job); // Software engineer
let {length} = 'foobar';
console.log(length); // 6
let {constructor: c} = 4;
console.log(c === Number); // true
let {_} = null; // TypeError
let {_} = undefined; // TypeError
let person = {name: 'Matt', age: 27, job: {title: 'Software engineer'}};
// 声明 title 变量并将 person.job.title 的值赋给它
let {job: {title}} = person;
console.log(title); // Software engineer

嵌套解构

在外层属性没有定义的情况下不能使用嵌套解构

如果一个解构表达式涉及 多个赋值,开始的赋值成功而后面的赋值出错,则整个解构赋值只会完成一部分

let person = {
   name: "Matt",
   age: 22
}

function printPerson(foo, {name, age}, bar) {
   console.log(arguments)
   console.log(name, age)
}

function printPerson02(foo, {name: personName, age: personAge}, bar) {
   console.log(arguments)
   console.log(personName, personAge)
}

printPerson('1st', person, '2nd');
printPerson02('1st', person, '2nd');

创建对象

工厂模式

/**
 * 工厂模式?
 * @param name
 * @param age
 * @param job
 * @returns {{}}
 */
function createPerson(name, age, job) {
   // let object = new Object();
   let object = {};
   object.name = name;
   object.age = age;
   object.job = job;
   object.sayName = () => {
      console.log(this.name)
   }
   return object;
}

let person1 = createPerson("zhazha", 11, "xx");
let person1 = createPerson("xixi", 22, "yy");

构造函数模式

// 1. 构造函数
function Person(name, age, address) {
   this.name = name;
   this.age = age;
   this.address = address;
   return this;
}
let person = new Person("haha", 11, "ffff");
console.log(person); // Person { name: 'haha', age: 11, address: 'ffff' }
console.log(person.constructor === Person); // true
console.log(person instanceof Object); // true
console.log(person instanceof Person); // true

有意思的调用方法

function Person(name, age, address) {
   this.name = name;
   this.age = age;
   this.address = address;
   return this;
}
let o1 = {};
Person.call(o1, "name", 1, "address");
console.log(o1)
let o2 = {}
Person.apply(o2, ['name', 2, 'address']);
console.log(o2)

let Person = function (name, age, address) {
   this.name = name;
   this.age = age;
   this.address = address;
   return this;
}

let person = new Person("a", 1, "b");
console.log(person)

构造函数也是普通函数

构造函数和普通函数的唯一区别在于调用方式, 构造函数调用方式使用的是new, 而普通函数是直接调用

let p1 = new Person(xxxx);
Person(xxxx)
console.log(window.name)

前面我们可以直接使用 window调用Personname属性, 是因为, 如果我们没有明确指定 this 指向什么对象, 默认指向则是 Global 对象, 这段代码放在浏览器中就是 window 对象, 所以会出现 上面的情况

构造函数存在问题

构造函数的问题在于多次调用, 如果该构造函数有方法, 比如:

let Person = (name, age, address) => {
    this.name = name
    this.age = age
    this.address = address
    this.sayName = function() {
        console.log(this.name)
    }
}

这里面的函数 sayName 在多次调用构造函数时, 方法sayName 也会跟着创建多个方法, 多个 不同的 Function 实例