继承6种套餐
参照红皮书,JS继承一共6种
1.原型链继承
核心思想:子类的原型指向父类的一个实例
Son.prototype=new Father();
2.构造函数继承
核心思想:借用apply和call方法在子对象中调用父对象
function Son(){Father.call(this);}
3.组合继承(1+2)(常用)
核心思想:1+2,但记得修正constructor
function Son(){Father.call(this);}
Son.prototype=new Father();
Son.prototype.constructor = Son;
4.原型式继承
核心思想:返回一个临时类型的一个新实例,现提出了规范的原型式继承,使用Object.create()方法。
var person={name:"xiaoming",age:16}
var anotherperson=Object.create(person,{name:"xiaowang"})
5.寄生式继承
核心思想:创建一个仅用于封装继承过程的函数,该函数在内部使用某种方式增强对象
function createAnother(original){
var clone=object(original);
clone.name="ahaha";
return clone;
}
6.寄生组合继承
核心思想:3+5
function inheritPropertype(son,father){
var prototype=object(father.prototype);//创建
prototype.constructor=son;//增强
son.prototype=prototype;//指定
}
在阮一峰老师的解说下,他将继承分成了两种,构造函数的继承和非构造函数的继承
构造函数的继承:
1.apply或call
2.prototype,即子类原型属性指向父类实例
3.直接的prototype,子类原型=父类原型
4.利用空对象作为中介,这种方法类似寄生继承,但是会变成子类->中介->父类这样的继承关系。好处是当子类对原型进行变动时,对父类没有影响。
function extend(Child, Parent) {
var F = function(){};
F.prototype = Parent.prototype;//继承方法2
Child.prototype = new F();//继承方法1
Child.prototype.constructor = Child;//修正
Child.uber = Parent.prototype;//为子对象设一个uber属性,这个属性直接指向父对象的prototype属性。只是为
//了实现继承的完备性,纯属备用性质。
}
5.拷贝继承,将父对象的prototype对象中的属性,一一拷贝给Child对象的prototype对象。
function extend2(Child, Parent) {
var p = Parent.prototype;
var c = Child.prototype;
for (var i in p) {
c[i] = p[i];
}
c.uber = p;
}
非构造函数的继承:
1.原型式继承。
2.浅拷贝
function extendCopy(p) {
var c = {};
for (var i in p) {
c[i] = p[i];
}
c.uber = p;
return c;
}
子对象获得的只是一个内存地址,而不是真正拷贝,因此存在父对象被篡改的可能。
3.深拷贝
function deepCopy(p, c) {
var c = c || {};
for (var i in p) {
if (typeof p[i] === 'object') {
c[i] = (p[i].constructor === Array) ? [] : {};
deepCopy(p[i], c[i]);
} else {
c[i] = p[i];
}
}
return c;
}
ES6的class语法糖
不知道为什么标题都是跟吃的有关
可能是因为到了半夜吧(虚
在学ES6之前,我们苦苦背下JS继承的典型方法
学习ES6后,发现官方鸡贼地给我们一个语法糖——class。它可以看作是构造函数穿上了统一的制服,所以class的本质依然是函数,一个构造函数。
class是es6新定义的变量声明方法(复习:es5的变量声明有var function和隐式声明 es6则新增let const class import),它的内部是严格模式。class不存在变量提升。
例:
//定义类
classPoint{
constructor(x,y){
this.x=x;
this.y=y;
}
toString(){
return'('+this.x+', '+this.y+')';
}
}
constructor就是构造函数,不多说,跟c++学的时候差不多吧,this对象指向实例。
类的所有方法都定义在类的prototype属性上面,在类的内部定义方法不用加function关键字。在类的外部添加方法,请指向原型,即实例的__proto__或者类的prototype。
Object.assign方法可以很方便地一次向类添加多个方法。
Object.assign(Point.prototype,{toString(){},toValue(){}});
私有的,静态的,实例的
私有方法,私有属性
类的特性是封装,在其他语言的世界里,有private、public和protected来区分,而js就没有
js在es5的时代,尝试了一些委婉的方法,比如对象属性的典型的set和get方法,在我之前说的JS的数据属性和访问器属性
现在es6规定,可以在class里面也使用setter和getter:
class MyClass {
constructor() { // ... }
get prop() { return 'getter'; }
set prop(value) { console.log('setter: '+value); }
}
let inst = new MyClass();
inst.prop = 123; // setter: 123
inst.prop // 'getter'
那么在这次es6的class里面,如何正式地去表示私有呢?
方法有叁:
1,老办法,假装私有。私有的东西,命名前加个下划线,当然了这只是前端程序员的自我暗示,实际上在外部应该还是可以访问得到私有方法。
2,乾坤大挪移。把目标私有方法挪出class外,class的一个公有方法内部调用这个外部的“私有”方法。
class Widget {
foo (baz) { bar.call(this, baz); } // ...
}
function bar(baz) { return this.snaf = baz; }
3,ES6顺风车,SYMBOL。利用Symbol值的唯一性,将私有方法的名字命名为一个Symbol值。Symbol是第三方无法获取的,所以外部也就无法偷看私有方法啦。
const bar = Symbol('bar');
const snaf = Symbol('snaf');
export default class myClass{
// 公有方法
foo(baz) { this[bar](baz); }
// 私有方法
[bar](baz) { return this[snaf] = baz; }
// ... };
那属性怎么私有化呢?现在还不支持,但ES6有一个提案,私有属性应在命名前加#号。
静态方法,静态属性
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。如果静态方法包含this关键字,这个this指的是类,而不是实例。父类的静态方法,可以被子类继承。
ES6 明确规定,Class 内部只有静态方法,没有静态属性。
声明一个静态属性,目前只支持以下写法,定义在外部:
class Foo {
}
Foo.prop = 1;
Foo.prop // 1
ES6当然也有提案,静态属性的声明采用static关键字,不过也是只提案。
实例属性
直接写。
class MyClass {
myProp = 42;
constructor() {
console.log(this.myProp); // 42
}
}
我有特殊的继承技巧
既然已经把class明摆出来,当然就可以摆脱“私生子”的身份,光明正大继承了。
Class 可以通过extends关键字实现继承:
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 调用父类的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 调用父类的toString()
}
}
在这里Point是父类,ColorPoint是子类,在子类中,super关键字代表父类,而在子类的构造函数中必须调用super方法,通过super方法新建一个父类的this对象(子类自身没有this对象),子类是依赖于父类的。基于这个设计思想,我们在子类中需要注意:子类实例实际上依赖于父类的实例,是先有爹后有子,所以构造函数先super后用this;父类的静态方法是会被子类所继承的。
Class继承的原理:
class A { }
class B { }
// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);//B.prototype.__proto__=A.prototype
// B 的实例继承 A 的静态属性
Object.setPrototypeOf(B, A);//B.__proto__=A
const b = new B();
在这里我们重新擦亮双眼,大喊三遍:class的本质是构造函数class的本质是构造函数class的本质是构造函数
在之前的原型学习笔记里面,我学习到了prototype是函数才有的属性,而__proto__是每个对象都有的属性。
在上述的class实质继承操作中,利用了Object.setPrototypeOf(),这个方法把参数1的原型设为参数2。
所以实际上我们是令B.prototype.__proto__=A.prototype,转化为图像就是上图所示,Father.prototype(更正图上的Father)截胡,变为了Son.prototype走向Object.prototype的中间站。
那为什么还有第二步B.__proto__=A呢?在class出来以前,我们的继承操作仅到上一步为止。
但是既然希望使用class来取代野路子继承,必须考虑到方法面面,譬如父类静态属性的继承。
在没有这一步之前,我们看看原本原型链的意义:Son.__proto__==Function.prototype,意味着Son是Function 的一个实例。因为我们可以通过类比,一个类的实例的__proto__的确指向了类的原型对象(prototype)。
所以B.__proto__=A意味着B是A的一个实例吗?可以说有这样的意味在里面,所以假使将B看作是A的一个实例,A是一个类似于原型对象的存在,而A的静态属性在这里失去了相对性,可看作是一个实例属性,同时B还是A的子类,那么A的静态属性就是可继承给B的,并且继承后,B对继承来的静态对象如何操作都影响不到A,AB的静态对象是互相独立的。
当然,上述只是我一个弱鸡的理解,让我们看看在阮一峰大神的教程里是怎么解读的:
大多数浏览器的 ES5 实现之中,每一个对象都有__proto__属性,指向对应的构造函数的prototype属性。Class 作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。
(1)子类的__proto__属性,表示构造函数的继承,总是指向父类。
(2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。
经过上述的我个人推测和大神的准确解说,解除了我心中一个顾虑:一个类的原型毕竟指向函数的原型对象,如果我们把子类的原型指向父类,是否会对它函数的本质有一定的影响?
事实上我们可以把这个操作视为“子类降级”,子类不再直接地指向函数原型对象,它所具备的函数的一些方法特性等,会顺着原型链指向函数原型对象,当我们希望对某个子类实行一些函数特有的操作等,编译器自然会通过原型链寻求目标。这就是原型链的精妙之处。
在阮一峰老师的ES6教程的“extends的继承目标”一节中,讲解了三种特殊的继承,Object,不继承,null。从这里也可以看见Function.prototype和子类的原型指向在原型链的角色。
class A{
constructor(){}
}
console.log(A.prototype,A.__proto__,A.prototype.__proto__)
//A.prototype==A {}//A.__proto__==[Function]
//A.prototype.__proto__=={}
super
刚才有说到构造函数里面有super(x,y),方法里面有super.toString(),也就是说super有两种意义
1,父类的构造函数
然而这个super方法是在子类构造函数里面使用的,所以它应当返回一个子类的实例,所以super里面的this应该指向子类。super()在这里相当于A.prototype.constructor.call(this)。
super()只能用在子类的构造函数之中,用在其他地方会报错。
2,与父类相关的对象
super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
Decorator-修饰器
修饰器是一个对类进行处理的函数。修饰器函数的第一个参数,就是所要修饰的目标类。
例:
@testable class MyTestableClass {
// ...
}
function testable(target) {
target.isTestable = true;
}
MyTestableClass.isTestable // true
另外修饰器也可以修饰方法
class Math {
@log
add(a, b) { return a + b; }
}
function log(target, name, descriptor) {
var oldValue = descriptor.value; descriptor.value = function() {
console.log(`Calling ${name} with`, arguments);
return oldValue.apply(null, arguments);
};
return descriptor;
}
const math = new Math(); // passed parameters should get logged now
math.add(2, 4);
修饰器函数一共可以接受三个参数。第一个是类的原型对象,第二个是要修饰的参数,第三个是修饰参数的数据属性对象
太累了,不想细说了,先写到这