经典的继承法有何问题

先看看本文最开始时提到的经典继承法实现,如下:

/** * 经典的js组合寄生继承 */ function MyDate() { Date.apply(this,
arguments); this.abc = 1; } function inherits(subClass, superClass) {
function Inner() {} Inner.prototype = superClass.prototype;
subClass.prototype = new Inner(); subClass.prototype.constructor =
subClass; } inherits(MyDate, Date); MyDate.prototype.getTest =
function() { return this.getTime(); }; let date = new MyDate();
console.log(date.getTest());

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 经典的js组合寄生继承
*/
function MyDate() {
    Date.apply(this, arguments);
    this.abc = 1;
}
 
function inherits(subClass, superClass) {
    function Inner() {}
    
    Inner.prototype = superClass.prototype;
    subClass.prototype = new Inner();
    subClass.prototype.constructor = subClass;
}
 
inherits(MyDate, Date);
 
MyDate.prototype.getTest = function() {
    return this.getTime();
};
 
 
let date = new MyDate();
 
console.log(date.getTest());

就是这段代码⬆,这也是JavaScript高程(红宝书)中推荐的一种,一直用,从未失手,结果现在马失前蹄。。。

我们再回顾下它的报错:

图片 1

再打印它的原型看看:

图片 2

怎么看都没问题,因为按照原型链回溯规则,Date的所有原型方法都可以通过MyDate对象的原型链往上回溯到。
再仔细看看,发现它的关键并不是找不到方法,而是this is not a Date object.

嗯哼,也就是说,关键是:由于调用的对象不是Date的实例,所以不允许调用,就算是自己通过原型继承的也不行

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);

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

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

ES6大法

当然,除了上述的ES5实现,ES6中也可以直接继承(自带支持继承Date),而且更为简单:

class MyDate extends Date { constructor() { super(); this.abc = 1; }
getTest() { return this.getTime(); } } let date = new MyDate(); //
正常输出,譬如1515638988725 console.log(date.getTest());

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyDate extends Date {
    constructor() {
        super();
        this.abc = 1;
    }
    getTest() {
        return this.getTime();
    }
}
 
let date = new MyDate();
 
// 正常输出,譬如1515638988725
console.log(date.getTest());

对比下ES5中的实现,这个真的是简单的不行,直接使用ES6的Class语法就行了。

而且,也可以正常输出。

注意:这里的正常输出环境是直接用ES6运行,不经过babel打包,打包后实质上是转化成ES5的,所以效果完全不一样

5.寄生式继承

核心思想:创建一个仅用于封装继承过程的函数,该函数在内部使用某种方式增强对象

function createAnother(original){

var clone=object(original);

clone.name=”ahaha”;

return clone;

}

为什么无法被继承?

首先,看看MDN上的解释,上面有提到,JavaScript的日期对象只能通过JavaScript Date作为构造函数来实例化。

图片 3

然后再看看stackoverflow上的回答:

图片 4

有提到,v8引擎底层代码中有限制,如果调用对象的[[Class]]不是Date,则抛出错误。

总的来说,结合这两点,可以得出一个结论:

要调用Date上方法的实例对象必须通过Date构造出来,否则不允许调用Date的方法

实例属性

直接写。

class MyClass {

myProp = 42;

constructor() {

console.log(this.myProp); // 42

}

}

stackoverflow上早就有答案了!

先说说结果,再浏览一番后,确实找到了解决方案,然后回过头来一看,惊到了,因为这个问题的提问时间是6 years, 7 months ago
也就是说,2011年的时候就已经有人提出了。。。

感觉自己落后了一个时代>_。。。

图片 5

而且还发现了一个细节,那就是viewed:10,606 times,也就是说至今一共也才一万多次阅读而已,考虑到前端行业的从业人数,这个比例惊人的低。
以点见面,看来,遇到这个问题的人并不是很多。

2.构造函数继承

核心思想:借用apply和call方法在子对象中调用父对象

function Son(){Father.call(this);}

[[Class]]与Internal slot

这一部分为补充内容。

前文中一直提到一个概念:Date内部的[[Class]]标识

其实,严格来说,不能这样泛而称之(前文中只是用这个概念是为了降低复杂度,便于理解),它可以分为以下两部分:

  • 在ES5中,每种内置对象都定义了 [[Class]]
    内部属性的值,[[Class]] 内部属性的值用于内部区分对象的种类

    • Object.prototype.toString访问的就是这个[[Class]]
    • 规范中除了通过Object.prototype.toString,没有提供任何手段使程序访问此值。
    • 而且Object.prototype.toString输出无法被修改
  • 而在ES5中,之前的 [[Class]]
    不再使用,取而代之的是一系列的internal slot

    • Internal slot
      对应于与对象相关联并由各种ECMAScript规范算法使用的内部状态,它们没有对象属性,也不能被继承
    • 根据具体的 Internal slot
      规范,这种状态可以由任何ECMAScript语言类型或特定ECMAScript规范类型值的值组成
    • 通过Object.prototype.toString,仍然可以输出Internal slot值
    • 简单点理解(简化理解),Object.prototype.toString的流程是:如果是基本数据类型(除去Object以外的几大类型),则返回原本的slot,如果是Object类型(包括内置对象以及自己写的对象),则调用Symbol.toStringTag
    • Symbol.toStringTag方法的默认实现就是返回对象的Internal
      slot,这个方法可以被重写

这两点是有所差异的,需要区分(不过简单点可以统一理解为内置对象内部都有一个特殊标识,用来区分对应类型-不符合类型就不给调用)。

JS内置对象是这些:

“Arguments”, “Array”, “Boolean”, “Date”, “Error”, “Function”, “JSON”,
“Math”, “Number”, “Object”, “RegExp”, “String”

1
"Arguments", "Array", "Boolean", "Date", "Error", "Function", "JSON", "Math", "Number", "Object", "RegExp", "String"

ES6新增的一些,这里未提到:(如Promise对象可以输出[object Promise]

而前文中提到的:

Object.defineProperty(date, Symbol.toStringTag, { get: function() {
return “Date”; } });

1
2
3
4
5
Object.defineProperty(date, Symbol.toStringTag, {
    get: function() {
        return "Date";
    }
});

它的作用是重写Symbol.toStringTag,截取date(虽然是内置对象,但是仍然属于Object)的Object.prototype.toString的输出,让这个对象输出自己修改后的[object Date]

但是,仅仅是做到输出的时候变成了Date,实际上内部的internal slot值并没有被改变,因此仍然不被认为是Date

继承6种套餐

参照红皮书,JS继承一共6种

ES5黑魔法

然后,再看看ES5中如何实现?

// 需要考虑polyfill情况 Object.setPrototypeOf = Object.setPrototypeOf ||
function(obj, proto) { obj.__proto__ = proto; return obj; }; /**
* 用了点技巧的继承,实际上返回的是Date对象 */ function MyDate() { //
bind属于Function.prototype,接收的参数是:object, param1, params2… var
dateInst = new(Function.prototype.bind.apply(Date,
[Date].concat(Array.prototype.slice.call(arguments))))(); //
更改原型指向,否则无法调用MyDate原型上的方法 //
ES6方案中,这里就是[[prototype]]这个隐式原型对象,在没有标准以前就是__proto__
Object.setPrototypeOf(dateInst, MyDate.prototype); dateInst.abc = 1;
return dateInst; } // 原型重新指回Date,否则根本无法算是继承
Object.setPrototypeOf(MyDate.prototype, Date.prototype);
MyDate.prototype.getTest = function getTest() { return this.getTime();
}; let date = new MyDate(); // 正常输出,譬如1515638988725
console.log(date.getTest());

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 需要考虑polyfill情况
Object.setPrototypeOf = Object.setPrototypeOf ||
function(obj, proto) {
    obj.__proto__ = proto;
 
    return obj;
};
 
/**
* 用了点技巧的继承,实际上返回的是Date对象
*/
function MyDate() {
    // bind属于Function.prototype,接收的参数是:object, param1, params2…
    var dateInst = new(Function.prototype.bind.apply(Date, [Date].concat(Array.prototype.slice.call(arguments))))();
 
    // 更改原型指向,否则无法调用MyDate原型上的方法
    // ES6方案中,这里就是[[prototype]]这个隐式原型对象,在没有标准以前就是__proto__
    Object.setPrototypeOf(dateInst, MyDate.prototype);
 
    dateInst.abc = 1;
 
    return dateInst;
}
 
// 原型重新指回Date,否则根本无法算是继承
Object.setPrototypeOf(MyDate.prototype, Date.prototype);
 
MyDate.prototype.getTest = function getTest() {
    return this.getTime();
};
 
let date = new MyDate();
 
// 正常输出,譬如1515638988725
console.log(date.getTest());

一眼看上去不知所措?没关系,先看下图来理解:(原型链关系一目了然)

图片 6

可以看到,用的是非常巧妙的一种做法:

  • 正常继承的情况如下:
    • new MyDate()返回实例对象date是由MyDate构造的
    • 原型链回溯是:
      date(MyDate对象)->date.__proto__->MyDate.prototype->MyDate.prototype.__proto__->Date.prototype
  • 这种做法的继承的情况如下:
    • new MyDate()返回实例对象date是由Date构造的
    • 原型链回溯是:
      date(Date对象)->date.__proto__->MyDate.prototype->MyDate.prototype.__proto__->Date.prototype

可以看出,关键点在于:

  • 构造函数里返回了一个真正的Date对象(由Date构造,所以有这些内部类中的关键[[Class]]标志),所以它有调用Date原型上方法的权利
  • 构造函数里的Date对象的[[ptototype]](对外,浏览器中可通过__proto__访问)指向MyDate.prototype,然后MyDate.prototype再指向Date.prototype

所以最终的实例对象仍然能进行正常的原型链回溯,回溯到原本Date的所有原型方法

  • 这样通过一个巧妙的欺骗技巧,就实现了完美的Date继承。不过补充一点,MDN上有提到尽量不要修改对象的[[Prototype]],因为这样可能会干涉到浏览器本身的优化。

如果你关心性能,你就不应该在一个对象中修改它的 [[Prototype]]

图片 7

1.原型链继承

核心思想:子类的原型指向父类的一个实例

Son.prototype=new Father();

写在最后的话

由于继承的介绍在网上已经多不胜数,因此本文没有再重复描述,而是由一道Date继承题引发,展开。(关键就是原型链)

不知道看到这里,各位看官是否都已经弄懂了JS中的继承呢?

另外,遇到问题时,多想一想,有时候你会发现,其实你知道的并不是那么多,然后再想一想,又会发现其实并没有这么复杂。。。

1 赞 1 收藏
评论

图片 8

私有的,静态的,实例的

实例就一定是由对应的构造函数构造出的么?

不一定,我们那ES5黑魔法来做示例

function MyDate() { // bind属于Function.prototype,接收的参数是:object,
param1, params2… var dateInst =
new(Function.prototype.bind.apply(Date,
[Date].concat(Array.prototype.slice.call(arguments))))(); //
更改原型指向,否则无法调用MyDate原型上的方法 //
ES6方案中,这里就是[[prototype]]这个隐式原型对象,在没有标准以前就是__proto__
Object.setPrototypeOf(dateInst, MyDate.prototype); dateInst.abc = 1;
return dateInst; }

1
2
3
4
5
6
7
8
9
10
11
12
function MyDate() {
    // bind属于Function.prototype,接收的参数是:object, param1, params2…
    var dateInst = new(Function.prototype.bind.apply(Date, [Date].concat(Array.prototype.slice.call(arguments))))();
 
    // 更改原型指向,否则无法调用MyDate原型上的方法
    // ES6方案中,这里就是[[prototype]]这个隐式原型对象,在没有标准以前就是__proto__
    Object.setPrototypeOf(dateInst, MyDate.prototype);
 
    dateInst.abc = 1;
 
    return dateInst;
}

我们可以看到instance的最终指向的原型是MyDate.prototype,而MyDate.prototype的构造函数是MyDate
因此可以认为instanceMyDate的实例。

但是,实际上,instance却是由Date构造的

我们可以继续用ES6中的new.target来验证。

注意⚠️

关于new.targetMDN中的定义是:new.target返回一个指向构造方法或函数的引用

嗯哼,也就是说,返回的是构造函数。

我们可以在相应的构造中测试打印:

class MyDate extends Date { constructor() { super(); this.abc = 1;
console.log(‘~new.target.name:MyDate‘);
console.log(new.target.name); } } // new操作时的打印结果是: //
~
new.target.name:MyDate~~~~ // MyDate

1
2
3
4
5
6
7
8
9
10
11
12
class MyDate extends Date {
    constructor() {
        super();
        this.abc = 1;
        console.log(‘~~~new.target.name:MyDate~~~~’);
        console.log(new.target.name);
    }
}
 
// new操作时的打印结果是:
// ~~~new.target.name:MyDate~~~~
// MyDate

然后,可以在上面的示例中看到,就算是ES6的Class继承,MyDate构造中打印new.target也显示MyDate
但实际上它是由Date来构造(有着Date关键的[[Class]]标志,因为如果不是Date构造(如没有标志)是无法调用Date的方法的)。
这也算是一次小小的勘误吧。

所以,实际上new.target是无法判断实例对象到底是由哪一个构造构造的(这里指的是判断底层真正的[[Class]]标志来源的构造)

再回到结论:实例对象不一定就是由它的原型上的构造函数构造的,有可能构造函数内部有着寄生等逻辑,偷偷的用另一个函数来构造了下,
当然,简单情况下,我们直接说实例对象由对应构造函数构造也没错(不过,在涉及到这种Date之类的分析时,我们还是得明白)。

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(){}});

如何继承 Date 对象?由一道题彻底弄懂 JS 继承

2018/01/25 · JavaScript
· Date,
继承

原文出处: 撒网要见鱼   

私有方法,私有属性

类的特性是封装,在其他语言的世界里,有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有一个提案,私有属性应在命名前加#号。

ES6继承与ES5继承的区别

从上午中的分析可以看到一点:ES6的Class写法继承是没问题的。但是换成ES5写法就不行了。

所以ES6的继承大法和ES5肯定是有区别的,那么究竟是哪里不同呢?(主要是结合的本文继承Date来说)

区别:(以SubClassSuperClassinstance为例)

  • ES5中继承的实质是:(那种经典组合寄生继承法)
    • 先由子类(SubClass)构造出实例对象this
    • 然后在子类的构造函数中,将父类(SuperClass)的属性添加到this上,SuperClass.apply(this, arguments)
    • 子类原型(SubClass.prototype)指向父类原型(SuperClass.prototype
    • 所以instance是子类(SubClass)构造出的(所以没有父类的[[Class]]关键标志)
    • 所以,instanceSubClassSuperClass的所有实例属性,以及可以通过原型链回溯,获取SubClassSuperClass原型上的方法
  • ES6中继承的实质是:
    • 先由父类(SuperClass)构造出实例对象this,这也是为什么必须先调用父类的super()方法(子类没有自己的this对象,需先由父类构造)
    • 然后在子类的构造函数中,修改this(进行加工),譬如让它指向子类原型(SubClass.prototype),这一步很关键,否则无法找到子类原型(注,子类构造中加工这一步的实际做法是推测出的,从最终效果来推测
    • 然后同样,子类原型(SubClass.prototype)指向父类原型(SuperClass.prototype
    • 所以instance是父类(SuperClass)构造出的(所以有着父类的[[Class]]关键标志)
    • 所以,instanceSubClassSuperClass的所有实例属性,以及可以通过原型链回溯,获取SubClassSuperClass原型上的方法

以上⬆就列举了些重要信息,其它的如静态方法的继承没有赘述。(静态方法继承实质上只需要更改下SubClass.__proto__SuperClass即可)

可以看着这张图快速理解:

图片 9

有没有发现呢:ES6中的步骤和本文中取巧继承Date的方法一模一样,不同的是ES6是语言底层的做法,有它的底层优化之处,而本文中的直接修改__proto__容易影响性能

ES6中在super中构建this的好处?

因为ES6中允许我们继承内置的类,如Date,Array,Error等。如果this先被创建出来,在传给Array等系统内置类的构造函数,这些内置类的构造函数是不认这个this的。
所以需要现在super中构建出来,这样才能有着super中关键的[[Class]]标志,才能被允许调用。(否则就算继承了,也无法调用这些内置类的方法)

3.组合继承(1+2)(常用)

核心思想:1+2,但记得修正constructor

function Son(){Father.call(this);}

Son.prototype=new Father();

Son.prototype.constructor = Son;

分析问题的关键

借助stackoverflow上的回答

我有特殊的继承技巧

既然已经把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__是每个对象都有的属性。

图片 10

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

网站地图xml地图