生成器
生成器是什么?
生成器的形式是一个函数, 它拥有在函数代码块中暂停(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函数起到两个功能,
- 启动生成器(特别注意第一次调用
next函数, 它仅仅是启动生成器, 所以传递给next函数的参数是无效的)- 恢复
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 个参数: 要给其添加属性的对象、属性的名称和一个描述符对象。最后一个参数,即描述符对象上的属性可以包 含:configurable、enumerable、writable和value,跟相关特性的名称一一对应
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()时,configurable、enumerable和writable的值如果不 指定,则都默认为false。
访问器属性
访问器属性不包含数据值
它们包含一个获取(getter)函数和一个设置(setter)函数,不 过这两个函数不是必需的
在读取访问器属性时,会调用获取函数,这个函数的责任就是返回一个有效 的值。在写入访问器属性时,会调用设置函数并传入新值,这个函数必须决定对数据做出什么修改
出什么修改。访 问器属性有 4 个特性描述它们的行为。
[[Configurable]]:表示属性是否可以通过delete删除并重新定义,是否可以修改它的特 性,以及是否可以把它改为数据属性。默认情况下,所有直接定义在对象上的属性的这个特性 都是true。[[Enumerable]]:表示属性是否可以通过for-in循环返回。默认情况下,所有直接定义在对 象上的属性的这个特性都是true。[[Get]]:获取函数,在读取属性时调用。默认值为undefined。[[Set]]:设置函数,在写入属性时调用。默认值为undefined
访问器属性是不能直接定义的,必须使用Object.defineProperty()
获取函数和设置函数不一定都要定义。只定义获取函数意味着属性是只读的,尝试修改属性会被忽 略。在严格模式下,尝试写入只定义了获取函数的属性会抛出错误。类似地,只有一个设置函数的属性 是不能读取的,非严格模式下读取会返回undefined,严格模式下会抛出错误。
定义多个属性
在一个对象上同时定义多个属性的可能性是非常大的。 为此, ECMAScript 提供了Object.defineProperties()方法
使用Object.getOwnPropertyDescriptor()方法可以取得指定属性的属性描述符
返回值是一个对象,对于访问器属性包含 configurable、enumerable、get和set属性,对于数据属性包含configurable、enumerable、 writable和value属性
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调用Person的name属性, 是因为, 如果我们没有明确指定 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 实例