文章目录
第7章:迭代器与生成器
1. 迭代器模式
- 把实现了正式的Iterable接口的某些结构称为“可迭代对象”(iterator),可以通过迭代器Iterator消费。
- 可迭代对象:元素有限且具有无歧义的遍历顺序。
- 可迭代协议(实现Iterator接口)
- 要求同时具备两种能力:支持迭代的自我识别能力和创建实现Iterator接口的对象的能力。
- 必须暴露一个属性作为“默认迭代器”,而且这个属性必须使用特殊的Symbol.iterator作为键。
- 默认迭代器属性必须引用一个迭代器工厂函数,调用这个工厂函数必须返回一个新迭代器。
- 不需要显式调用工厂函数来生成迭代器。接收可迭代对象的原生语言特性包括:
- for-of循环
- 数组解构(不懂,看下面例子有点明白了)
// 数组解构
let [a, b, c] = arr;
console.log(a, b, c); // foo, bar, baz
- 扩展操作符
- Array.from()
- 创建集合
- 创建映射
- Promise.all()接收由期约组成的可迭代对象(期约是什么见第11章)
- Promise.race()接收由期约组成的可迭代对象
- yield*操作符,在生成器中使用(这个暂时也不懂)
- 迭代器协议
- 迭代器API使用next()方法在可迭代对象中遍历数据。
- next()方法返回迭代器对象IteratorResult包含两个属性:done和value。done是一个布尔值,表示是否还可以再次调用next()取得下一个值;value包含可迭代对象的下一个值。
- 自定义迭代器
- 提前终止迭代器
2. 生成器
- 生成器基础
- 生成器的形式是一个函数,函数名称前面加一个星号(*)表示它是一个生成器。
- 标识生成器函数的星号不受两侧空格的影响。
- 生成器函数声明:
function* generatorFn() { }
- 调用生成器函数会产生一个生成器对象。
- 通过yield中断执行
- 生成器可以用来作为默认迭代器。
- 可以提前终止生成器。可选的return()方法用于提前终止迭代器。
- 与迭代器不同,所有的生成器对象都有return()方法,只要通过它进入关闭状态,就无法恢复了。后续调用next()会显示done:true状态。
- throw()方法会在暂停的时候将一个提供的错误注入到生成器对象中。如果错误未被处理,生成器就会关闭。如果生成器对象还没有开始执行,那么调用throw()抛出的错误不会在函数内部被捕获,因为这相当于在函数块外部抛出了错误。
第8章:对象、类与面向对象编程
1. 理解对象
- 属性的类型:[[ ]]
- 数据属性
- 包含一个保存数据值的位置。值会从这个位置读取,也会写入到这个位置。
- 四个特性:[[Configurable]]、[[Enumerable]]、[[Writable]]、[[Value]]
- 修改属性默认值方法:Object.defineProperty(),实际情况中不太用得到。
- 访问器属性
- getter():读取访问器属性
- setter():设置访问器属性
- 四个特性:[[Configurable]]、[[Enumerable]]、[[Get]]、[[Set]]
- 访问器属性是不能直接定义的,必须使用Object.defineProperty()
- 数据属性
- 定义多个属性:Object.definedProperties()
let book = {};
Object.definedProperties(book,{
year_: {
value: 2023
},
edition: {
value: 1
}
})
- 读取属性的特性
- 使用Object.getOwnPropertyDescriptor()方法可以取得指定属性的属性描述符。这个方法接收两个参数:属性所在的对象和要取得其描述符的属性名。返回值是一个对象。
let descriptor = Object.getOwnPropertyDescriptor(book, "year_");
console.log(descriptor.value); // 2023
console.log(descriptor.configurable); // false
- Object.getOwnPropertyDescriptors()静态方法。这个方法实际上会在每个自有属性上调用Object.getOwnPropertyDescriptor()并在一个新对象中返回它们。
- 使用Object.getOwnPropertyDescriptor()方法可以取得指定属性的属性描述符。这个方法接收两个参数:属性所在的对象和要取得其描述符的属性名。返回值是一个对象。
- 合并对象:Object.assign()方法
- 对象标识及相等判定:Object.is()方法
- 增强的对象语法
- 属性值简写:只要使用变量名就会自动被解释为同名的属性键。
- 可计算属性
- 简写方法名
- 对象解构
- 嵌套解构
- 部分解构
- 参数上下文匹配
2. 创建对象
-
工厂模式
function createPerson(name, age, job) { let o = new Object(); o.name = name; o.age = age; o.job = job; o.sayName = function() { console.log(this.name); }; return o; }
-
构造函数模式
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.sayName = function() { console.log(this.name); }; }
- 构造函数也是函数,任何函数只要使用new操作符调用就是构造函数。如果没有使用new操作符调用,会将属性和方法添加到window对象。通过call()调用函数的调用方式,可以将特定对象指定为作用域。
- 构造函数的问题是,其定义的方法会在每个实例上都创建一遍。为解决这个问题,可以把函数定义转移到构造函数外部。新问题:全局作用域被搞乱了,导致自定义类型引用的代码不能很好地聚集在一起,解决方法是原型模式。
-
原型模式
function Person() {} Person.prototype = { name: "Nicholas", age: 29, job: "Software Engineer", sayName() { console.log(this.name); } };
- 每个函数都会创建一个prototype属性,叫原型对象。它上面定义的属性和方法可以被对象实例共享。
- 原来在构造函数中直接赋给对象实例的值,可以直接赋值给它们的原型。
- 注意理解:实例与构造函数原型之间有直接的联系,但实例与构造函数之间没有。
- 原型层级:通过对象访问属性时,会按照属性名称开始搜索,从对象实例开始搜索,如果没有找到,搜索会沿着指针进入原型对象,然后在原型对象上进行搜索。这就是原型用于在多个对象实例间共享属性和方法的原理。
- 只要给对象实例添加一个属性,这个属性就会遮蔽(shadow)原型对象上的同名属性,不会修改但会屏蔽对原型对象的访问。使用delete操作符删除实例上的这个属性,则可以继续搜索原型对象。
- 原型和in操作符
- in操作符,只要通过对象可以访问,就返回true。
- hasOwnProperty(),只有属性存在于实例上时才返回true。
- 属性枚举顺序
- for-in循环和Object.keys()的枚举顺序是不确定的。
- Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()和Objetc.assign()的枚举顺序是确定性的。
-
对象迭代
- 将对象内容转换为序列化的可迭代格式的两个静态方法:Object.values()和Object.entries()。注意:符号属性会被忽略。
- 其他原型语法:Person.prototype={};
- 即使实例在修改原型之前已经存在,任何时候对原型对象所做的修改也会在实例上反映出来。
- 实例只有指向原型的指针,没有指向构造函数的指针。
- 原生对象原型也可以修改,但是不建议这么做,推荐做法是创建一个自定义类,继承原生原型。
- 原型的问题主要体现在操作包含引用值的属性时。
3. 继承:依靠原型链实现
- 原型链
- 构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型有一个属性指回构造函数,而实例有一个内部指针指向原型。
- 当前原型是另一个原型的实例,这样就组成了一个原型链。
- 默认原型:默认情况下,所有引用类型都继承自Object。
- 原型与继承关系:原型与实例的关系可以通过instanceof操作符或者isPrototypeOf()方法来确定。
- 子类增加父类没有的方法,必须在原型赋值之后在添加到原型上。
- 注意:以字面量方式创建原型方法会破坏之前的原型链,因为这相当于重写。
- 原型链的问题:
- 包含引用值时
- 子类型实例化时不能给父类型构造函数传参
- 盗用构造函数(解决原型链的问题)
- 思路:在子类构造函数中调用父类构造函数。
- 使用call()或者apply()方法。
- 盗用构造函数的一个优点是可以在子类构造函数中向父类构造函数传参。
- 盗用构造函数的缺点:
- 必须在构造函数中定义方法,因此函数不能重用。
- 子类也不能访问父类原型上定义的方法,因此所有类型只能使用构造函数模式。
- 组合继承
- 原型式继承:Object.create(),第二个参数用来新增属性,新增的属性会遮蔽原型对象上的同名属性。
let person = { name:"wenshuo", friends:["yaya","xiaoxiao"] } let anotherPerson = Object.create(person,{ name:{ value:"Greg" } });
- 寄生式继承(首倡的一种模式)
function object(o){ function F(){} F.prototype = o; return new F(); } function createAnother(original){ let clone = object(original); clone.sayHi = function (){ console.log("Hi"); } return clone; }
- 寄生式组合继承(没看明白)
4. 类class
- 类定义
- 类声明:class Person { }
- 类表达式:const Animal = class { };
- 函数声明可以提升,类声明不可以提升
- 函数受函数作用域限制,类受块作用域限制。
- 类构造函数
- 构造函数的定义不是必需的
- 类实例化时传入的参数会用作构造函数的参数。如果不需要参数,则类名后面的括号也是可选的。
- 调用类构造函数必须使用new操作符
- 把类当成特殊函数,可以把类作为参数传递
- 实例、原型和类成员
- 实例成员:每次通过new调用类标识符时,都会执行类构造函数。
- 原型方法与访问器:为了在实例间共享方法,类定义语法把在类块中定义的方法作为原型方法。
- 静态类方法:用于执行不特定于实例的操作,也不要求存在类的实例。
- 非函数原型和类成员
class Person{ sayName(){ console.log(`${Person.greeting} ${this.name}`) } } // 在类上定义数据成员 Person.greeting = "My name is"; // 在原型上定义数据成员 Person.prototype.name = "Jake"; let p = new Person(); p.sayName();
- 迭代器与生成器方法
- 继承
- 继承基础
- 使用extends关键字,不仅可以继承一个类,也可以继承普通的构造函数。
- 构造函数、HomeObject和super():super的一些注意事项
- 抽象基类:可供其他类继承,但本身不会被实例化。
- 继承内置类型
- 类混入(好多js框架已经抛弃了混入模式,转入了组合模式。组合胜过继承。)
- 继承基础
第10章:函数
1. 函数定义的方式有:函数声明、函数表达式、箭头函数,Function构造函数。
2.箭头函数(=>)
- 不能使用arguments、super和new.target,也不能用作构造函数,也没有prototype属性。
3. 函数名:就是指向函数的指针。
- 一个函数可以有多个名称。
- 使用不带括号的函数名会访问函数指针,而不会执行函数。
- 所有函数对象都会暴露一个制度的name属性。如果函数是一个获取函数、设置函数,或者使用bind()实例化,那么标识符前面会加上一个前缀。
4. 理解参数
- ECMAScript函数既不关心传入的参数个数,也不关心这些参数的数据类型。
- 使用function关键字定义(非箭头)函数时,可以在函数内部访问arguments对象,从中取得传进来的每个参数值。
- 注意:ECMAScript中的所有参数都是按值传递的。不可能按引用传递参数。如果把对象作为参数传递,那么传递的值就是这个对象的引用。
5. 没有重载
- 如果在ECMAScript中定义了两个同名函数,则后定义的会覆盖先定义的。
6. 默认参数值
- 显式定义默认参数:在函数定义的参数后面用=就可以为参数赋一个默认值。
- 给参数传undefined相当于没有传值,可以通过这种方式利用默认值。
- 使用默认参数时,arguments对象的值不反映参数的默认值,只反映传给函数的参数。修改命名参数也不会影响arguments对象,它始终以调用函数时传入的值为准。
- 参数是按顺序初始化的,遵循“暂时性死区”规则,即前面定义的函数不能引用后面定义的。
7. 参数扩展与收集
- 扩展参数:扩展操作符可以用于调用函数时传参。
- 收集参数:扩展操作符也可以用于定义函数参数。把不同长度的独立参数组合为一个数组。
8. 函数声明与函数表达式
- 在任何代码执行之前,会先读取函数声明,并在执行上下文中生成函数定义。这个过程叫做函数声明提升。
- 函数表达式必须等到代码执行到它那一行,才会在执行上下文中生成函数定义。
9. 函数作为值
- 函数名在ECMAScript中就是变量,所以函数可以用在任何可以使用变量的地方。
10. 函数内部
- arguments:类数组对象。该对象有一个callee属性,指向arguments对象所在函数的指针。
// 让函数逻辑与函数名解耦 function factorial(num) { if (num <= 1) { return 1; } else { return num * arguments.callee(num - 1); } }
- this
- 在标准函数中,this引用的是把函数当成方法调用的上下文对象。
- 在箭头函数中,this引用的是定义箭头函数的上下文。
- caller:
- 函数的一个属性,引用的是调用当前函数的函数,如果是在全局作用域中调用的则为null。
- 如果要降低耦合度可以通过arguments.callee.caller来引用相同的值。
- new.target:函数的一个属性,检测函数是否使用new关键字调用的。
11. 函数属性与方法
- 每个函数都有两个属性:length和prototype。
- length:保存函数定义的命名参数的个数。
- prototype:是保存引用类型所有实例方法的地方。
- 函数的两个方法:apply()和call(),这两个方法都会以指定的this值来调用函数。
- apply():两个参数,函数内的this值和一个参数数组。
- call():与apply()作用一样,传参形式不同。第一个参数也是this值,剩下的要传给被调用函数的参数则是逐个传递的,也就是说必须将参数一个一个地列出来。
12. 函数表达式
- 函数表达式与函数声明最大的区别就是,函数声明可以提升,函数表达式不可以。
13. 递归
- 递归函数通常的形式是一个函数通过名称调用自己。
- 在编写递归函数时,arguments.callee是引用当前函数的首选。(严格模式下不可访问arguments.callee)
- 因此可以使用明明函数表达式(named function expression),严格模式和非严格模式下都可以使用。
const factorial = (function f(num){ if(num <= 1){ return 1; }else{ return num*f(num-1); } });
14. 尾调用优化
- 尾调用:外部函数的返回值是一个内部函数的返回值。
15. 闭包
- 参考链接
- 闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。
- 闭包可以让开发者从内部函数访问外部函数的作用域。
- 每个函数在调用时都会自动创建两个特殊变量:this和arguments。内部函数永远不可能直接访问外部函数的这两个变量。先把外部函数的this保存到变量that中再访问就可以了。let that = this;that.xxx
16. 立即调用的函数表达式
- 立即调用的匿名函数又被称作立即调用的函数表达式(IIFE, Immediately Invoked Function Expression)。类似于函数声明,但由于被包含在括号中,所以会被解释为函数表达式。使用IIFE可以模拟块级作用域。
(function(){ // 块级作用域 })();
17. 私有变量
- 任何定义在函数或块中的变量,都可以认为是私有的,因为在这个函数或块的外部无法访问其中的变量。
- 私有变量包括函数参数、局部变量,以及函数内部定义的其它函数。
- 特权方法是能够访问函数私有变量(及私有函数)的公有方法。特权方法可以在构造函数中实现,也可以使用静态私有变量来实现。
- 每次调用构造函数都会重新创建一套变量和方法,所以每个实例都会重新创建一遍新方法。
- 静态私有变量可以利用原型更好地重用代码,只是每个实例没有自自己的私有变量。
- 模块模式
-
单例对象
let singleton = { name:value, method(){ // 方法的代码 } };
-
模块模式是在单例对象基础上加以扩展,使其通过作用域链来关联私有变量和特权方法。
-
- 模块增强模式
- 另一个利用模块模式的做法是在返回对象之前先对其进行增强。
- 这适合单例对象需要是某个特定类型的实例,但又必须给它添加额外属性或方法的场景。