《JavaScript高级程序设计》读书笔记
理解对象
属性的类型
对象属性分为:数据属性和访问器属性
数据属性:
- configurable:表示属性是否可以通过delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认为true
- enumberable:表示属性是否可以通过for-in 循环返回。默认为true
- writable:表示属性的值是否可以被修改。默认为true
- value:包含属性实际的值
let person = {};
Object.defineProperty(person, "name", {
writable:false,
value: "Nicholas"
});
console.log(person.name); 'Nicholas'
访问器属性:
- configurable:表示属性是否可以通过delete 删除并重新定义,是否可以修改它的特性,以及是否可以把它改为数据属性。默认为true
- enumerable:表示属性是否可以通过for-in 循环返回。默认为true
- get:获取函数,在读取属性时调用
- set:设置函数,在写入属性时调用
访问器属性不能直接定义,必须使用 Object.defineProperty(obj, prop, descriptor)
let book = {
year_: 2017,
edition: 1
};
Object.defineProperty(book, "year",{
get(){
return this.year_;
},
set(newValue){
if(newValue > 2017){
this.year_ = newValue;
this.edition ++;
}
}
});
book.year = 2018;
book.edition; // 2
Object.defineProperty(obj, prop, descriptor) :修改属性默认特性
Object.defineProperties(obj, props) :一次定义多个属性默认特性
Object.getOwnPropertyDescriptor(obj, prop):返回指定对象上一个自有属性对应的属性描述
Object.getOwnPropertyDescriptors(obj):返回指定对象的所有自身属性的描述符
Object.assign(target, ...sources):将所有可枚举(Object.propertyIsEnumerable() 返回 true)和自有(Object.hasOwnProperty() 返回 true)属性从一个或多个源对象复制到目标对象,返回修改后的对象。浅复制。
Object.is(value1, value2):判断两个值是否为同一个值。考虑到了一些边界处理。如true 和 1
增强的对象语法
1. 属性值简写
let name = 'Matt';
let person = {
name: name
};
// 简写
let person = {
name
}
2. 可计算属性
中括号包围的对象属性将作为Js 表达式而不是字符串求值。
const nameKey = 'name';
let person = {};
person[nameKey] = 'Matt';
// 有了计算属性
let person = {
[nameKey]: 'Matt'
}
3. 简写方法名
let person = {
sayName: function(name){
console.log('kkk');
}
};
//简写
let person = {
sayName(){
console.log('kkk');
}
}
对象解构
- 解构在内部使用函数ToObject() 把源数据结构转化为对象,这意味着原始值会被当成对象。
- 如果是给事先声明的变量赋值,则赋值表达式必须包含在一对括号中。
- 在外层属性没有定义的情况下不能使用嵌套解构。
- 如果一个解构表达式涉及多个赋值,开始的赋值成功而后面的赋值失败,则整个解构赋值只会完成一部分。
创建对象
工厂模式
用于抽象创建特定对象的过程。这种工厂模式虽然可以解决创建多个类似对象的问题,但没有解决对象标识问题
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;
}
let person1 = createPerson("Nicholas", 29, "worker")
构造函数模式
创建特定类型对象。
与工厂模式区别:
- 没有显示地创建对象
- 属性和方法直接赋值给了this
- 没有return
要创建这样的实例,应该使用new 操作符。以这种方法调用构造函数会执行如下操作:
- 在内存中创建一个新对象
- 这个新对象内部 [[Prototype]] 特性被赋值为构造函数的prototype 属性
- 构造函数内部的this 被赋值为这个新对象(即this 指向新对象)
- 执行构造函数内部的代码(给新对象添加属性)
- 如果构造函数返回非空对象,即返回该对象;否则,返回刚刚创建的新对象。
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name);
}
};
let person1 = new Person('Bob', 18, 'worker');
let person2 = new Person('Alice', 23, 'writer');
console.log(person1.constructor == Person); // true
console.log(person2.constructor == Person); // true
console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true
1. 构造函数也是函数
构造函数与普通函数唯一区别就是调用方法不同。
// 作为构造函数
let person1 = new Person('Bob', 18, 'worker');
// 作为函数调用(在调用一个函数没有明确设置this值的情况下,this始终指向Global 对象,所以window出现了sayName() 方法)
Person('Greg', 27, "Doctor");
window.sayName(); // Greg
// 在另一个对象的作用域中调用
let o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); //Kristen
2. 构造函数的问题
其定义的方法都会在每个实例上都创建一遍。即person1 和 person2 都有名为sayName() 的方法,但这两个方法不是同一个Function 实例。因为都是做一样的事,所以没必要定义两个不同的Function 实例,要解决这个问题,可以把函数定义到构造函数外部,但造成了全局污染,这个问题可以通过原型模式来解决。
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = new Function("console.log(this.name)"); // 逻辑等价
};
原型模式
每个函数都会创建一个 prototype 属性,这个属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法。
function Person() {}
Person.prototype.name = 'Bob';
Person.prototype.age = 29;
Person.prototype.sayName = function() {
console.log(this.name);
};
let person1 = new Person();
person1.sayName(); //Bob
这里所有属性和sayName() 方法都直接添加到了Person 的prorotype 属性上,构造函数体中什么也没有。与构造函数模式不同,使用这种原型模式定义后的属性和方法是由所有实例共享的。
1. 理解原型
无论何时,只要创建一个函数,就会按照特定的规则为这个函数创建一个prototype 属性(指向原型对象)。默认情况下,所有原型对象自动获得一个名为constructor 的属性,指向与之关联的构造函数。(两者循环引用)
在自定义构造函数时,原型对象默认只会获得constructor 属性,其他的所有方法都继承自Object。每次调用构造函数创建一个新的实例,这个实例内部[[Prototype]] 指针就会被赋值为构造函数的原型对象,这个特性会在每个对象上暴露__proto__ 属性,通过这个属性可以访问对象的原型。实例与构造函数原型之间有直接的联系,但实例与构造函数之间没有。
function Person() {}
// 声明之后,构造函数就有了一个与之关联的原型对象
console.log(typeof Person.prototype); // Object
console.log(Person.prototype);
//{constructor: ƒ}
// constructor: ƒ Person()
// [[Prototype]]: Object
// 构造函数有一个prototype 属性引用其原型对象,而这个原型对象有个constructor 属性,
// 引用这个构造函数。两者循环引用
console.log(Person.prototype.constructor === Person); // true
// 正常的原型链都会终止与Object 的原型对象,Object 原型的原型是null
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Person.prototype.__proto__.constructor === Object); // true
console.log(Person.prototype.__proto__.__proto__); // null
let person1 = new Person(), person2 = new Person();
// 构造函数、原型对象、实例 是3个完全不同的对象
console.log(person1 !== Person); // true
console.log(person1 !== Person.prototype); // true
console.log(Person.prototype !== Person); // true
// 实例通过__proto__ 链接到原型对象,它实际上指向藏特性[[Prototype]]
// 构造函数通过prototype 属性链接到原型对象
// 实例与构造函数没有直接联系,与原型对象有直接联系
console.log(person1.__proto__ === Person.prototype); // true
console.log(person1.__proto__.constructor === Person); // true
// 同一构造函数创建的两个实例,共享同一个原型对象
console.log(person1.__proto__ === person2.__proto__); // true
// instanceof 检查实例的原型链中是否包含指定的构造函数的原型
console.log(person1 instanceof Person); // true
console.log(person1 instanceof Object); // true
console.log(Person.prototype instanceof Object); // true
- prototypeObj.isPrototypeOf(object) 方法用于测试一个对象是否存在于另一个对象的原型链上。
- Object.getPrototypeOf(object) 方法返回指定对象的原型(内部
[[Prototype]]
属性的值)。 - Object.setPrototypeOf(obj, prototype) 方法设置一个指定的对象的原型 ( 即,内部 [[Prototype]] 属性)到另一个对象或 null。(严重影响代码性能,推荐Object.create())
- Object.create(proto,[propertiesObject]) 方法创建一个新对象,使用现有的对象来提供新创建的对象的 __proto__。
2. 原型层级
在通过对象访问属性时,会从对象实例开始,要是没有找到继续沿着指针进入原型对象,如果找到则返回。
obj.hasOwnProperty(prop) 方法会返回一个布尔值,用于确定某个属性时在实例上还是在原型对象上。
3. 原型和in 操作符
两种方法使用:单独使用和在for-in 循环中使用。
单独使用时,in 操作符会在可以通过对象访问指定属性返回true,无论该属性是在实例上还是在原型上。
在for-in 循环中使用时,可以通过对象访问且可以被枚举的属性都会返回,包括实例属性和原型属性。要获得对象上所有可枚举的实例属性,可以使用Object.keys() 方法。如果想列出所有实例属性,无论是否可以枚举,可以使用 Object.getOwnPropertyNames() ,兄弟方法 Object.getOwnPropertySymbols() 方法,只针对符号。
4. 属性枚举顺序
for-in循环 和Object.keys() 的枚举顺序不确定,取决于js 引擎
Object.getOwnPropertyNames() 、Object.getOwnPropertySymbols() 和 Object.assign() 的枚举顺序是确定的。先以升序枚举数值键,然后以插入顺序枚举字符串和符号键。在对象字面中定义的键以它们逗号分隔的顺序插入。
对象迭代
- Object.values() 接收一个对象,返回对象值数组
- Object.entries() 接收一个对象,返回键/值对的数组
继承
实现继承:继承实际的方法
原型链
通过原型继承多个引用类型的属性和方法。每个构造函数都有一个原型对象,原型有一个属性指向构造函数,而实例有一个内部指针指向原型。原型本身有一个内部指针指向另一个原型,相应地另一个原型也有一个指针指向另一个构造函数,这样就在实例与原型之间构造了一条原型链。
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
}
function SubType() {
this.subproperty = false;
}
// 继承
SubType.prototype = new SuperType();
let instance = new SubType();
console.log(instance.getSuperValue());
1. 默认原型
默认情况下,所有引用类型都继承自Object ,这也是通过原型链实现的。任何函数的默认原型都是一个Object 的实例,这意味着这个实例有一个内部指针指向 Object.prototyep。
2. 原型与继承关系
- instanceof 如果一个实例的原型链中出现相应的构造函数,则返回true
- isProtoTypeOf 只要原型链中包含这个原型,则返回true
3. 关于方法
子类有时候需要覆盖父类的方法,或者增加父类没有的方法。为此,这些方法必须在原型赋值之后再添加到原型上。
以对象字面量方法创建原型方法会破坏之前的原型链,因为这相当于重写了原型链。
4. 原型链的问题
原型中包含的引用值会在所有实例间共享。
子类型在实例化时不能给父类型的构造函数传参。
盗用构造函数
为了解决原型包含引用值导致的继承问题。基本思路:在子类构造函数中调用父类构造函数。使用apple() 和 call() 方法以新创建的对象为上下文执行构造函数。
function SuperType(name) {
this.colors = ['red', 'blue'];
this.name = name;
}
function SubType() {
SuperType.call(this, "Bob");
}
let instance = new SubType();
instance.colors.push('balck');
console.log(instance.colors);
1. 传递参数
可以在子类构造函数中向父类构造函数传参
2. 盗用构造函数的问题
必须在构造函数中定义方法,因此函数不能重用。此外,子类也不能访问父类原型上定义的方法,因此所有类型只能使用构造函数模式
组合继承
也叫伪经典继承,基本思路:使用原型链继承原型上的属性和方法,通过盗用构造函数继承实例属性。既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。
function SuperType(name) {
this.colors = ['red', 'blue'];
this.name = name;
}
SuperType.prototype.sayName = function() {
console.log(this.name);
}
function SubType(name ,age){
// 继承属性
SuperType.call(this, name);
this.age = age;
}
// 继承方法
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function() {
console.log(this.age);
}
let instance = new SubType("Bob", 20);
instance.colors.push('black');
console.log(instance.colors); // ['red', 'blue', 'black']
instance.sayName(); // Bob
原型式继承
你有一个对象,想在它基础上在创建一个新对象。你需要把这个对象先传给object() ,然后再对返回的对象进行适当修改。本质上,object() 是对传入的对象执行了一次浅复制。
function object(o){ function F() {} F.prototype = o; return new F(); }
ECMAScript 5 通过增加Object.create() 方法将原型式继承的概念规范化。
寄生式继承
创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象。
function createAnother(original) {
let clone = object(original); // 通过调用函数创建一个新对象
clone.sayHi = functin() { // 以某种方式增强这个对象
console.log('hi');
};
return clone; // 返回这个对象
}
寄生式组合继承
不通过调用父类构造函数给予子类原型赋值,而是取得父类原型的一个副本。
function inheritPrototype(subType, superType) {
let prototype = object(superType.prototype); // 创建对象
prototype.constructor = subType; // 增强对象
subtype.prototype = prototype; // 赋值对象
}
类
类定义
class Person{} 、const Animal = class {}
类没有声明提升,受块作用域限制。
类可以包含构造函数方法、实例方法、获取函数、设置函数和静态方法。
类构造函数
constructor关键字用于在类定义块内部创建类的构造函数。方法名 constructor会告诉解释器在使用new 操作符创建类的新实例时,应该调用这个函数。
1. 实例化
使用new调用类的构造函数会执行如下操作:
- 在内存中创建一个新对象
- 这个新对象内部的[[Prototype]] 指向被赋值为构造函数的prototype 属性
- 构造函数内部的this 被赋值为这个新对象
- 执行构造函数内部的代码
- 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的对象
类实例化时传入的参数会用作构造函数的参数。默认情况下,类构造函数会在执行之后返回this对象。
类构造函数被实例化之后,会成为普通的实例方法。
2. 把类当成特殊函数
实例、原型和类成员
1. 实例成员
每次使用new,都会执行类构造函数(constructor),为这个实例添加自有属性。每个实例都对应一个唯一的成员对象,所有成员不会在原型上共享。
2. 原型方法与访问器
为了在实例间共享方法,会把在类块中定义的方法作为原型方法。类方法等同于对象属性,因此可以使用字符串、符号和计算的值作为键。
class Person {
constructor() {
// 添加到this的所有内容都会存在于不同的实例上
this.localte = ()=>console.log('localte');
}
// 类块中定义的所有内容都会定义在类的原型上
localte(){
console.log('prototype');
}
// 可以使用计算的值
['computed' + 'Key'](){
console.log('computedKey');
}
}
类定义也支持获取和设置访问器。
class Person{
set name(newName){
this.name_ = newName;
}
get name(){
return this.name_;
}
}
3. 静态类方法
这些方法通常用于执行不特定于实例的操作,也不要求存在类的实例。与原型成员类似,每个类上只能有一个静态成员。使用static 关键字为前缀,内部的this 引用类自身。
class Person {
constructor(age) {
this.age_ = age;
}
sayAge() {
console.log(this.age_);
}
static create(){
return new Person(Math.floor(Math.random()*100));
}
}
Person.create();
4. 非函数原型和类成员
类定义并不显式支持在原型或类上添加成员数据,但在类定义外部,可以手动添加。
class Person {}
// 在类上定义数据成员
Person.greetring = 'My name is Bob';
// 在原型上定义数据成员
Person.prototype.name = "Bob";
5. 迭代器与生成器方法
class Person {
constructor(){
this.nickname = ['Jack', 'Bob'];
}
// 在原型上定义生成器方法
*createNickname() {
yield 'Jack';
yield 'Bob';
}
// 在类上定义生成器方法
static *createname() {
yield 'Jack';
yield 'Bob';
}
// 添加默认的迭代器
*[Symbol.iterator]() {
yield *this.nickname.entries();
}
}
继承
1. 继承继承
使用extends 关键字,可以继承任何拥有[[Construct]] 和原型的对象。
class Vehicle {}
// 继承类
class Bus extends Vehicle{}
function Person() {}
// 继承普通构造函数
class D extends Person{}
2. 构造函数、HomeObject 和 super()
ES6 给类构造函数和静态方法添加了内部特性[[HomeObject]],这个特性是一个指针,指向定义该方法的对象。super 始终会定义为 [[HomeObject]] 的原型。
派生类的方法可以通过super 关键字引用它们的原型。只能在派生类中使用,而且仅限于类构造函数、实例方法和静态方法内部。
class Vehicle {
constructor() {
this.hasEngine = true;
}
static identify(){
console.log('identify');
}
}
class Bus extends Vehicle {
constructor() {
// 不要在super 之前引用this
super(); // 相当于super.constructor()
console.log(this);
}
static identify() {
super.identify();
}
}
使用super 注意的问题:
- super 只能在派生类构造函数和静态方法中使用
- 不能单独引用super 关键字,要么用它调用构造函数,要么用它引用静态方法
- 调用 super() 会调用父类构造函数,并将返回的实例赋值给this
- super() 的行为如同调用构造函数,如果需要给父类构造函数传参,需要手动传入
- 如果没有定义类构造函数,在实例化派生类时会调用super() ,而且会传入所有传给派生类的参数
- 在类构造函数中,不能再调用super() 之前引用this
- 如果派生类中显式定义了构造函数(constructor),则要么必须在其中调用super(),要么必须在其中返回一个对象
3. 抽象基类
有时候需要定义这样一个类,它可供其他类继承,但本身不会被实例化。new.target 在实例化时检测是不是抽象基类
class Vehicle {
constructor() {
console.log(new.target);
if(new.target === Vehicle) {
throw new Error("Vehicle 不能被实例化");
}
// 要求派生类必须定义某个方法
if(!this.foo){
throw new Error("未定义 foo()");
}
}
}
4. 继承内置类型
class SuperArray extends Array {
shuffile() {
console.log('扩展内置类型');
}
static get [Symbol.species]() {
console.log('覆盖默认行为')
return Array;
}
}
5. 类混入
class Vehicle{}
let A = (Superclass) => class extends Superclass{
foo() {
console.log('foo-a');
}
}
let B = (Superclass) => class extends Superclass{
bar() {
console.log('bar-b');
}
}
let C = (Superclass) => class extends Superclass{
baz() {
console.log('baz-c');
}
}
class Bus extends A(B(C(Vehicle))) {}
// 或者写一个辅助函数,可以把嵌套展开
function mix(BaseClass, ...Mixins){
return Mixins.reduce((item, current) => current(item), BaseClass);
}
class Bus extends mix(Vehicle, A, B, C){}