我只是以当前的认知去看这本书,下面是我觉得有必要记录的点,也算是自己对每一章节内容的浓缩总结!
其实js也可以实现java里面的接口、抽象等面向对象方式,然后在执行代码的时候进行校验,如果担心这种运行时校验消耗性能,那么把这种校验直接放到编译的时候,类似java中的强类型校验,然后再留一个开关,在编译时候可以取消或者开启这种强校验。
和同事讨论他们都没有反驳我的观点,那我目前就认为自己说的是对的:
1 子类的原型是父类的一个实例
2 类里面的super是父类的超集或者说是父类的扩展对象,父类上面有的它都有,父类都做的事情它也能做。
TODO
不懂的地方:
这里的[[HomeObject]]指的this吗?
目录
第八章 对象、类与面向对象编程
8.1 理解对象
8.1.1 数据属性和访问器属性分别四个特性
8.2 创建对象
8.2.3
8.2.4
8.3 继承
8.3.1 原型链
8.3.2 盗用构造函数(对象伪装或者经典继承)
8.3.3. 组合继承
8.3.4 原型式继承
8.3.5 寄生式继承
8.3.5 寄生式组合继承
8.4 类
8.4.1 类定义
8.4.2 类构造函数
8.4.3 实例、原型和类成员
2. 原型方法与访问器
8.4.4 继承
1. 继承基础
2.构造函数、HomeObject 和 super()
3. 抽象基类
4. 继承内置类型
5 混入模式
第八章 对象、类与面向对象编程
8.1 理解对象
8.1.1 数据属性和访问器属性分别四个特性
在调用 Object.defineProperty()时,configurable、enumerable 和 writable 的值如果不指定,则都默认为 false
8.2 创建对象
8.2.3
new创建对象过程
- 在内存中创建一个新对象。
- 这个新对象内部的[[Prototype]]特性被赋值为构造函数的 prototype 属性。
- 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。
- 执行构造函数内部的代码(给新对象添加属性)。
- 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
8.2.4
实例与构造函数原型之间有直接的联系,但实例与构造函数之间没有。
8.3 继承
JS继承为了最大还原java中的继承特性,实例有自己的私有化属性和方法,父类属性和方法被实例共享。
8.3.1 原型链
如果一个实例的原型链中出现过相应的构造函数的原型,则 instanceof 返回 true。
以对象字面量方式创建原型方法会破坏之前的原型链,因为这相当于重写了原型链。
案例:
function SuperType() {this.property = true;
}
SuperType.prototype.getSuperValue = function () {return this.property;
};
function SubType() {this.subproperty = false;
}
// 继承 SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function () {return this.subproperty;
};
let instance = new SubType();
console.log(instance.getSuperValue()); // true
缺点:
- 主要问题出现在原型中包含引用值的时候;所有属性和方法在子类共享,无法做到实例私有
- 子类型在实例化时不能给父类型的构造函数传参;
8.3.2 盗用构造函数(对象伪装或者经典继承)
为了解决原型包含引用值导致的继承问题,一种叫作“盗用构造函数”(constructor stealing)的技术)。基本思路很简单:在子类构造函数中调用父类构造函数。因为毕竟函数就是在特定上下文中执行代码的简单对象,所以可以使用apply()和 call()方法以新创建的对象为上下文执行构造函数。
案例
function SuperType() {this.colors = ["red", "blue", "green"];
}
function SubType() {// 继承 SuperTypeSuperType.call(this);
}
let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"
let instance2 = new SubType();
console.log(instance2.colors); // "red,blue,green"
我理解就是在子类中执行一次父类,相当于浅复制了父类的实例属性和方法。
优点:
- 传递参数 相比于使用原型链,盗用构造函数的一个优点就是可以在子类构造函数中向父类构造函数传参。
- 解决原型链中引用值继承问题
缺点:
- 盗用构造函数的主要缺点,也是使用构造函数模式自定义类型的问题:必须在构造函数中定义方法,因此函数不能重用。此外,子类也不能访问父类原型上定义的方法,因此所有类型只能使用构造函数模式。由于存在这些问题,盗用构造函数基本上也不能单独使用。
8.3.3. 组合继承
组合继承就是原型链继承加上盗用构造函数继承 (ES6的类,类似这种继承方式)组合继承弥补了原型链和盗用构造函数的不足,是 JavaScript 中使用最多的继承模式。而且组合继承也保留了 instanceof 操作符和 isPrototypeOf()方法识别合成对象的能力。
案例
function SuperType(name) {this.name = name;this.colors = ["red", "blue", "green"];
}
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 instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"
instance1.sayName(); // "Nicholas";
instance1.sayAge(); // 29
let instance2 = new SubType("Greg", 27);
console.log(instance2.colors); // "red,blue,green"
instance2.sayName(); // "Greg";
instance2.sayAge(); // 27
缺点:
- 最主要的效率问题就是父类构造函数始终会被调用两次,造成实例属性被复制2次。而子类原型对象是不需要父类实例属性的,下面的寄生式组合继承方式解决了这个问题(我觉得执行2次还好吧,ES6类 父类构造函数只执行一次)
- 原型的原型上有属性值是对象时候,还是会有修改之后全部实例被修改的情况。(这个缺点ES6类也没法解决,目前就只能接受,无解,解决它代价也很大,没必要。)
8.3.4 原型式继承
浅薄的意见,我自己觉得这种方式没多大用处,就是为了理解下面的继承方式做铺垫。
其实就是把原型链继承放到函数里面,参数就是子类的原型对象。
案例
function object(o) {function F() {}F.prototype = o;return new F();
}
开始我以为ES6 Object.create()就是这种继承方式,其实也不准确,Object.create()更像是集成式继承(父类的引用属性值被实例一起共享,除非属性值都是基础类型)。
原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合。但要记住,属性中包含的引用值始终会在相关对象间共享,跟使用原型模式是一样的。(我觉得写代码应该不会用到这方式)
8.3.5 寄生式继承
原型式继承的加强版。
案例
function createAnother(original) {let clone = object(original); // 通过调用函数创建一个新对象clone.sayHi = function () {// 以某种方式增强这个对象console.log("hi");};return clone; // 返回这个对象
}
寄生式继承同样适合主要关注对象,而不在乎类型和构造函数的场景。object()函数不是寄生式继承所必需的,任何返回新对象的函数都可以在这里使用。(我觉得写代码应该不会用到这方式)
注意 通过寄生式继承给对象添加函数会导致函数难以重用,与构造函数模式类似。
8.3.5 寄生式组合继承
ES6的类应该就是按照这种方式实现的。
寄生式组合继承通过盗用构造函数继承属性,但使用混合式原型链继承方法。基本思路是不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。说到底就是使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。
案例
function object(o) {function F() {}F.prototype = o;return new F();
}
function inheritPrototype(subType, superType) {let prototype = object(superType.prototype); // 创建对象prototype.constructor = subType; // 增强对象subType.prototype = prototype; // 赋值对象
}function SuperType(name) {this.name = name;this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function () {console.log(this.name);
};
function SubType(name, age) {SuperType.call(this, name);this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function () {console.log(this.age);
};
这种方式解决了组合继承父类构造函数调用2次的性能问题,父类构造函数是只执行了一次,代替的是执行了父类副本函数的构造函数,父类副本函数指的是一个空函数,原型对象等于父类的原型对象,就做了这个点的优化。(优化点就是把父类构造函数执行2次中的一次改成了执行空函数的构造函数)o为父类的原型对象。
也就是少了父类实例属性的初始化,性能是提升了,但是还是new了一次实例对象。
也就是优化了组合式继承中子类原型赋值父类实例的地方。
8.4 类
8.4.1 类定义
类和函数的区别:
- 函数声明可以提升,但类定义不能。
- 函数受函数作用域限制,而类受块作用域限制。
8.4.2 类构造函数
类的继承子类的原型对象是父类的一个实例。(自己的观点)
使用 new 调用类的构造函数会执行如下操作:
- 在内存中创建一个新对象。
- 这个新对象内部的[[Prototype]]指针被赋值为构造函数的 prototype 属性。
- 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。
- 执行构造函数内部的代码(给新对象添加属性)。
- 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象
8.4.3 实例、原型和类成员
2. 原型方法与访问器
为了在实例间共享方法,类定义语法把在类块中定义的方法作为原型方法。
class Person {constructor() {// 添加到 this 的所有内容都会存在于不同的实例上this.locate = () => console.log("instance");}// 在类块中定义的所有内容都会定义在类的原型上locate() {console.log("prototype");}
}
let p = new Person();
p.locate(); // instance
Person.prototype.locate(); // prototype
8.4.4 继承
1. 继承基础
ES6 类支持单继承。使用 extends 关键字,就可以继承任何拥有[[Construct]]和原型的对象。
很大程度上,这意味着不仅可以继承一个类,也可以继承普通的构造函数(保持向后兼容):
2.构造函数、HomeObject 和 super()
2.1派生类的方法可以通过 super 关键字引用它们的原型。这个关键字只能在派生类中使用,而且仅限于类构造函数、实例方法和静态方法内部。在类构造函数中使用 super 可以调用父类构造函数。
2.2在静态方法中可以通过 super 调用继承的类上定义的静态方法
注意 ES6 给类构造函数和静态方法添加了内部特性[[HomeObject]],这个特性是一个
指针,指向定义该方法的对象。这个指针是自动赋值的,而且只能在 JavaScript 引擎内部
访问。super 始终会定义为[[HomeObject]]的原型。(这块其实没理解这个[[HomeObject]])
在使用 super 时要注意几个问题:
- 如果没有定义类构造函数,在实例化派生类时会调用 super(),而且会传入所有传给派生类参数。
- 在类构造函数中,不能在调用 super()之前引用 this。
- 如果在派生类中显式定义了构造函数,则要么必须在其中调用 super(),要么必须在其中返一个对象。
3. 抽象基类
通过在实例化时检测 new.target 是不是抽象基类,可以阻止对抽象基类的实例化。
另外,通过在抽象基类构造函数中进行检查,可以要求派生类必须定义某个方法。因为原型方法在
调用类构造函数之前就已经存在了,所以可以通过 this 关键字来检查相应的方法 (个人观点通过这种检测可以模拟实现java里面的接口、抽象类等等)
4. 继承内置类型
ES6 类为继承内置引用类型提供了顺畅的机制,开发者可以方便地扩展内置类型:
class SuperArray extends Array {shuffle() {// 洗牌算法for (let i = this.length - 1; i > 0; i--) {const j = Math.floor(Math.random() * (i + 1));[this[i], this[j]] = [this[j], this[i]];}}
}
let a = new SuperArray(1, 2, 3, 4, 5);
console.log(a instanceof Array); // true
console.log(a instanceof SuperArray); // true
console.log(a); // [1, 2, 3, 4, 5]
a.shuffle();
console.log(a); // [3, 1, 4, 5, 2]
有些内置类型的方法会返回新实例。默认情况下,返回实例的类型与原始实例的类型是一致的:
class SuperArray extends Array {}let a1 = new SuperArray(1, 2, 3, 4, 5);let a2 = a1.filter(x => !!(x%2))console.log(a1); // [1, 2, 3, 4, 5]console.log(a2); // [1, 3, 5]console.log(a1 instanceof SuperArray); // trueconsole.log(a2 instanceof SuperArray); // true
如果想覆盖这个默认行为,则可以覆盖 Symbol.species 访问器,这个访问器决定在创建返回的
实例时使用的类:
class SuperArray extends Array {static get [Symbol.species]() {return Array;}
}let a1 = new SuperArray(1, 2, 3, 4, 5);let a2 = a1.filter((x) => !!(x % 2));console.log(a1); // [1, 2, 3, 4, 5]console.log(a2); // [1, 3, 5]console.log(a1 instanceof SuperArray); // trueconsole.log(a2 instanceof SuperArray); // false
5 混入模式
实现多继承。
案例:
class Vehicle {}
let FooMixin = (Superclass) =>class extends Superclass {foo() {console.log("foo");}};
let BarMixin = (Superclass) =>class extends Superclass {bar() {console.log("bar");}};
let BazMixin = (Superclass) =>class extends Superclass {baz() {console.log("baz");}};
function mix(BaseClass, ...Mixins) {return Mixins.reduce((accumulator, current) => current(accumulator),BaseClass);
}
class Bus extends mix(Vehicle, FooMixin, BarMixin, BazMixin) {}
let b = new Bus();
b.foo(); // foo
b.bar(); // bar
b.baz(); // baz
注意 很多 JavaScript 框架(特别是 React)已经抛弃混入模式,转向了组合模式(把方法
提取到独立的类和辅助对象中,然后把它们组合起来,但不使用继承)。这反映了那个众
所周知的软件设计原则:“组合胜过继承(composition over inheritance)。”这个设计原则被
很多人遵循,在代码设计中能提供极大的灵活性。
上一章节