JavaScriptweb前端

JS继承 -> ES6的class和decorator

2018-01-23  本文已影响214人  灯不梨喵

继承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__是每个对象都有的属性。

我的学习图,没有备注的箭头表示__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);

修饰器函数一共可以接受三个参数。第一个是类的原型对象,第二个是要修饰的参数,第三个是修饰参数的数据属性对象

太累了,不想细说了,先写到这

上一篇下一篇

猜你喜欢

热点阅读