我只是以当前的认知去看这本书,下面是我觉得有必要记录的点,也算是自己对每一章节内容的浓缩总结!
其实js也可以实现java里面的接口、抽象等面向对象方式,然后在执行代码的时候进行校验,如果担心这种运行时校验消耗性能,那么把这种校验直接放到编译的时候,类似java中的强类型校验,然后再留一个开关,在编译时候可以取消或者开启这种强校验。
和同事讨论他们都没有反驳我的观点,那我目前就认为自己说的是对的:
1 子类的原型是父类的一个实例
2 类里面的super是父类的超集或者说是父类的扩展对象,父类上面有的它都有,父类都做的事情它也能做。
上面的观点完全是错的,然后我就去知乎上提问了,大佬论证且说服了我,我上面观点完全是错的。
不懂的地方:
这里的[[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 混入模式
在调用 Object.defineProperty()时,configurable、enumerable 和 writable 的值如果不指定,则都默认为 false
new创建对象过程
实例与构造函数原型之间有直接的联系,但实例与构造函数之间没有。
JS继承为了最大还原java中的继承特性,实例有自己的私有化属性和方法,父类属性和方法被实例共享。
如果一个实例的原型链中出现过相应的构造函数的原型,则 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
缺点:
为了解决原型包含引用值导致的继承问题,一种叫作“盗用构造函数”(constructor stealing)的技术)。基本思路很简单:在子类构造函数中调用父类构造函数。因为毕竟函数就是在特定上下文中执行代码的简单对象,所以可以使用apply()和 call()方法以新创建的对象为上下文执行构造函数。
案例
function SuperType() {
this.colors = ["red", "blue", "green"];
}
function SubType() {
// 继承 SuperType
SuperType.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"
我理解就是在子类中执行一次父类,相当于浅复制了父类的实例属性和方法。
优点:
缺点:
组合继承就是原型链继承加上盗用构造函数继承 组合继承弥补了原型链和盗用构造函数的不足,是 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
缺点:
浅薄的意见,我自己觉得这种方式没多大用处,就是为了理解下面的继承方式做铺垫。
其实就是把原型链继承放到函数里面,参数就是子类的原型对象。
案例
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
开始我以为ES6 Object.create()就是这种继承方式,其实也不准确,Object.create()更像是集成式继承(父类的引用属性值被实例一起共享,除非属性值都是基础类型)。
原型式继承非常适合不需要单独创建构造函数,但仍然需要在对象间共享信息的场合。但要记住,属性中包含的引用值始终会在相关对象间共享,跟使用原型模式是一样的。(我觉得写代码应该不会用到这方式)
原型式继承的加强版。
案例
function createAnother(original) {
let clone = object(original); // 通过调用函数创建一个新对象
clone.sayHi = function () {
// 以某种方式增强这个对象
console.log("hi");
};
return clone; // 返回这个对象
}
寄生式继承同样适合主要关注对象,而不在乎类型和构造函数的场景。object()函数不是寄生式继承所必需的,任何返回新对象的函数都可以在这里使用。(我觉得写代码应该不会用到这方式)
注意 通过寄生式继承给对象添加函数会导致函数难以重用,与构造函数模式类似。
寄生式组合继承通过盗用构造函数继承属性,但使用混合式原型链继承方法。基本思路是不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。说到底就是使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。
案例
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了一次实例对象。
也就是优化了组合式继承中子类原型赋值父类实例的地方。
类和函数的区别:
类的继承子类的原型对象是父类的一个实例。(自己的观点)
使用 new 调用类的构造函数会执行如下操作:
为了在实例间共享方法,类定义语法把在类块中定义的方法作为原型方法。
class Person {
constructor() {
// 添加到 this 的所有内容都会存在于不同的实例上
this.locate = () => console.log("instance");
}
// 在类块中定义的所有内容都会定义在类的原型上
locate() {
console.log("prototype");
}
}
let p = new Person();
p.locate(); // instance
Person.prototype.locate(); // prototype
ES6 类支持单继承。使用 extends 关键字,就可以继承任何拥有[[Construct]]和原型的对象。
很大程度上,这意味着不仅可以继承一个类,也可以继承普通的构造函数(保持向后兼容):
2.1派生类的方法可以通过 super 关键字引用它们的原型。这个关键字只能在派生类中使用,而且仅限于类构造函数、实例方法和静态方法内部。在类构造函数中使用 super 可以调用父类构造函数。
2.2在静态方法中可以通过 super 调用继承的类上定义的静态方法
注意 ES6 给类构造函数和静态方法添加了内部特性[[HomeObject]],这个特性是一个
指针,指向定义该方法的对象。这个指针是自动赋值的,而且只能在 JavaScript 引擎内部
访问。super 始终会定义为[[HomeObject]]的原型。(这块其实没理解这个[[HomeObject]])
在使用 super 时要注意几个问题:
通过在实例化时检测 new.target 是不是抽象基类,可以阻止对抽象基类的实例化。
另外,通过在抽象基类构造函数中进行检查,可以要求派生类必须定义某个方法。因为原型方法在
调用类构造函数之前就已经存在了,所以可以通过 this 关键字来检查相应的方法 (个人观点通过这种检测可以模拟实现java里面的接口、抽象类等等)
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); // true
console.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); // true
console.log(a2 instanceof SuperArray); // false
实现多继承。
案例:
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)。”这个设计原则被
很多人遵循,在代码设计中能提供极大的灵活性。
1 子类的原型是父类一个实例 (错误)
根据寄生式组合继承以及
class Parent {}
class Child extends Parent {}
Child.prototype instanceof Parent // true
我就简单总结出这个观点,子类原型是父类new出来的。
class Parent {
foo = 1
}
class Child extends Parent {}
Child.prototype.foo // undefined
真正 new
出来的 Parent
的实例,肯定是会带有 foo
属性的。说明我的观点是错的。
[[HomeObject]]就是this也就是Child.property。 super就是this的原型。
在 SuperCall、SuperProperty 的读取、SuperProperty赋值这三种情况里,super 有三种不同的取值逻辑。
SuperCall:super指父类 .例如:super.foo()
SuperProperty读取:super 指父类的原型对象. 例如:super.foo
SuperProperty赋值:super指this. 例如:super.foo=10
class Parent {
aa = 300;
foo() {
console.log("parent foo");
}
}
class Child extends Parent {
foo() {
console.log(super.aa);
}
}
const child = new Child();
child.foo();
console.log(Child.prototype === child.__proto__);
console.log(Child.prototype.__proto__ === Parent.prototype);
console.log(child.__proto__.__proto__ === Parent.prototype);
//true
//true
//true
子类的原型的原型等于父类的原型,根据寄生式组合继承,子类的原型是一个空函数的实例,然后这个空函数的原型对象指向了父类的原型,那么自然的子类的原型的原型就是父类的原型。
那么ES6类的实现方式就是根据寄生式组合继承方式来实现的。
上一章节