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

几种继承的细微区别

虽然上述提到的三种方法都可以达到继承Date的目的-混合法严格说不能算继承,只不过是另类实现。

于是,将所有能打印的主要信息都打印出来,分析几种继承的区别,大致场景是这样的:

可以参考:(
请进入调试模式)https://dailc.github.io/fe-interview/demo/extends_date.html

从上往下,1, 2, 3, 4四种继承实现分别是:(排出了混合法)

  • ES6的Class大法
  • 经典组合寄生继承法
  • 本文中的取巧做法,Date构造实例,然后更改__proto__的那种
  • ES6的Class大法,Babel打包后的实现(无法正常调用的)

~~以下是MyDate们的prototype~~~ Date {constructor: ƒ, getTest: ƒ}
Date {constructor: ƒ, getTest: ƒ} Date {getTest: ƒ, constructor: ƒ} Date
{constructor: ƒ, getTest: ƒ} ~~以下是new出的对象~~~ Sat Jan 13
2018 21:58:55 GMT+0800 (CST) MyDate2 {abc: 1} Sat Jan 13 2018 21:58:55
GMT+0800 (CST) MyDate {abc: 1}
~~以下是new出的对象的Object.prototype.toString.call~~~ [object
Date] [object Object] [object Date] [object Object]
~~以下是MyDate们的__proto__~~~ ƒ Date() { [native code] }
ƒ () { [native code] } ƒ () { [native code] } ƒ Date() { [native
code] } ~~以下是new出的对象的__proto__~~~ Date
{constructor: ƒ, getTest: ƒ} Date {constructor: ƒ, getTest: ƒ} Date
{getTest: ƒ, constructor: ƒ} Date {constructor: ƒ, getTest: ƒ}
~~以下是对象的__proto__与MyDate们的prototype比较~~~ true
true true true

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
~~~~以下是MyDate们的prototype~~~~~~~~~
Date {constructor: ƒ, getTest: ƒ}
Date {constructor: ƒ, getTest: ƒ}
Date {getTest: ƒ, constructor: ƒ}
Date {constructor: ƒ, getTest: ƒ}
 
~~~~以下是new出的对象~~~~~~~~~
Sat Jan 13 2018 21:58:55 GMT+0800 (CST)
MyDate2 {abc: 1}
Sat Jan 13 2018 21:58:55 GMT+0800 (CST)
MyDate {abc: 1}
 
~~~~以下是new出的对象的Object.prototype.toString.call~~~~~~~~~
[object Date]
[object Object]
[object Date]
[object Object]
 
~~~~以下是MyDate们的__proto__~~~~~~~~~
ƒ Date() { [native code] }
ƒ () { [native code] }
ƒ () { [native code] }
ƒ Date() { [native code] }
 
~~~~以下是new出的对象的__proto__~~~~~~~~~
Date {constructor: ƒ, getTest: ƒ}
Date {constructor: ƒ, getTest: ƒ}
Date {getTest: ƒ, constructor: ƒ}
Date {constructor: ƒ, getTest: ƒ}
 
~~~~以下是对象的__proto__与MyDate们的prototype比较~~~~~~~~~
true
true
true
true

看出,主要差别有几点:

  1. MyDate们的__proto__指向不一样
  2. Object.prototype.toString.call的输出不一样
  3. 对象本质不一样,可以正常调用的1, 3都是Date构造出的,而其它的则是MyDate构造出的

我们上文中得出的一个结论是:由于调用的对象不是由Date构造出的实例,所以不允许调用,就算是自己的原型链上有Date.prototype也不行

但是这里有两个变量:分别是底层构造实例的方法不一样,以及对象的Object.prototype.toString.call奥门永利误乐域,的输出不一样
(另一个MyDate.__proto__可以排除,因为原型链回溯肯定与它无关)

万一它的判断是根据Object.prototype.toString.call来的呢?那这样结论不就有误差了?

于是,根据ES6中的,Symbol.toStringTag,使用黑魔法,动态的修改下它,排除下干扰:

// 分别可以给date2,date3设置 Object.defineProperty(date2,
Symbol.toStringTag, { get: function() { return “Date”; } });

1
2
3
4
5
6
// 分别可以给date2,date3设置
Object.defineProperty(date2, Symbol.toStringTag, {
    get: function() {
        return "Date";
    }
});

然后在打印下看看,变成这样了:

[object Date] [object Date] [object Date] [object Object]

1
2
3
4
[object Date]
[object Date]
[object Date]
[object Object]

可以看到,第二个的MyDate2构造出的实例,虽然打印出来是[object Date],但是调用Date方法仍然是有错误

奥门永利误乐域 1

此时我们可以更加准确一点的确认:由于调用的对象不是由Date构造出的实例,所以不允许调用

而且我们可以看到,就算通过黑魔法修改Object.prototype.toString.call,内部的[[Class]]标识位也是无法修改的。
(这块知识点大概是Object.prototype.toString.call可以输出内部的[[Class]],但无法改变它,由于不是重点,这里不赘述)。

继承6种套餐

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

构造函数与实例对象

看到这里,不知道是否对上午中频繁提到的构造函数实例对象有所混淆与困惑呢?这里稍微描述下:

要弄懂这一点,需要先知道new一个对象到底发生了什么?先形象点说:

4.原型式继承

核心思想:返回一个临时类型的一个新实例,现提出了规范的原型式继承,使用Object.create()方法。

var person={name:”xiaoming”,age:16}

var anotherperson=Object.create(person,{name:”xiaowang”})

为什么无法被继承?

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

奥门永利误乐域 2

然后再看看stackoverflow上的回答:

奥门永利误乐域 3

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

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

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

super

刚才有说到构造函数里面有super(x,y),方法里面有super.toString(),也就是说super有两种意义

1,父类的构造函数

然而这个super方法是在子类构造函数里面使用的,所以它应当返回一个子类的实例,所以super里面的this应该指向子类。super()在这里相当于A.prototype.constructor.call(this)。

super()只能用在子类的构造函数之中,用在其他地方会报错。

2,与父类相关的对象

super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。

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

不一定,我们那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之类的分析时,我们还是得明白)。

我有特殊的继承技巧

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

奥门永利误乐域 4

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

暴力混合法

首先,说说说下暴力的混合法,它是下面这样子的:

奥门永利误乐域 5

说到底就是:内部生成一个Date对象,然后此类暴露的方法中,把原有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有一个提案,私有属性应在命名前加#号。

先说说如何快速快速寻求解答

遇到不会的问题,肯定第一目标就是如何快速寻求解决方案,答案是:

  • 先去stackoverflow上看看有没有类似的题。。。

于是,借助搜索引擎搜索了下,第一条就符合条件,点开进去看描述

奥门永利误乐域 6

1.原型链继承

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

Son.prototype=new Father();

[[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

5.寄生式继承

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

function createAnother(original){

var clone=object(original);

clone.name=”ahaha”;

return clone;

}

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即可)

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

奥门永利误乐域 7

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

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

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

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

倘若用的是中文搜索。

用中文搜索并不丢人(我遇到问题时的本能反应也是去百度)。结果是这样的:

奥门永利误乐域 8

嗯,看来英文关键字搜索效果不错,第一条就是符合要求的。然后又试了试中文搜索。
奥门永利误乐域 9

奥门永利误乐域 10效果不如人意,搜索前几页,唯一有一条看起来比较相近的(segmentfault上的那条),点进去看

奥门永利误乐域 11
奥门永利误乐域 12

怎么说呢。。。这个问题关注度不高,浏览器数较少,而且上面的问题描述和预期的有点区别,仍然是有人回答的。
不过,虽然说问题在一定程度上得到了解决,但是回答者绕过了无法继承这个问题,有点未竟全功的意思。。。

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

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

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

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

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

原文出处: 撒网要见鱼   

私有的,静态的,实例的

大纲

  • 先说说如何快速快速寻求解答
    • stackoverflow上早就有答案了!
    • 倘若用的是中文搜索。
  • 分析问题的关键
    • 经典的继承法有何问题
    • 为什么无法被继承?
  • 该如何实现继承?
    • 暴力混合法
    • ES5黑魔法
    • ES6大法
    • ES6写法,然后babel打包
  • 几种继承的细微区别
  • ES6继承与ES5继承的区别
  • 构造函数与实例对象
  • [[Class]]与Internal slot
  • 如何快速判断是否继承?
  • 写在最后的话

2.构造函数继承

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

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

发表评论

电子邮件地址不会被公开。 必填项已用*标注