JS继承 -> ES6的class和decorator
02月10日, 2019 联系我们 admin
02月10日, 2019
经典的继承法有何问题
先看看本文最开始时提到的经典继承法实现,如下:
/** * 经典的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高程(红宝书)中推荐的一种,一直用,从未失手,结果现在马失前蹄。。。
我们再回顾下它的报错:
再打印它的原型看看:
怎么看都没问题,因为按照原型链回溯规则,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
作为构造函数来实例化。
然后再看看stackoverflow上的回答:
有提到,v8
引擎底层代码中有限制,如果调用对象的[[Class]]
不是Date
,则抛出错误。
总的来说,结合这两点,可以得出一个结论:
要调用Date上方法的实例对象必须通过Date构造出来,否则不允许调用Date的方法
实例属性
直接写。
class MyClass {
myProp = 42;
constructor() {
console.log(this.myProp); // 42
}
}
stackoverflow上早就有答案了!
先说说结果,再浏览一番后,确实找到了解决方案,然后回过头来一看,惊到了,因为这个问题的提问时间是6 years, 7 months ago
。
也就是说,2011
年的时候就已经有人提出了。。。
感觉自己落后了一个时代>_。。。
而且还发现了一个细节,那就是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,这个方法可以被重写
- 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());
|
一眼看上去不知所措?没关系,先看下图来理解:(原型链关系一目了然)
可以看到,用的是非常巧妙的一种做法:
- 正常继承的情况如下:
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]]
1.原型链继承
核心思想:子类的原型指向父类的一个实例
Son.prototype=new Father();
写在最后的话
由于继承的介绍在网上已经多不胜数,因此本文没有再重复描述,而是由一道Date继承题引发,展开。(关键就是原型链)
不知道看到这里,各位看官是否都已经弄懂了JS中的继承呢?
另外,遇到问题时,多想一想,有时候你会发现,其实你知道的并不是那么多,然后再想一想,又会发现其实并没有这么复杂。。。
1 赞 1 收藏
评论
私有的,静态的,实例的
实例就一定是由对应的构造函数构造出的么?
不一定,我们那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
,
因此可以认为instance
是MyDate
的实例。
但是,实际上,instance
却是由Date
构造的
我们可以继续用ES6
中的new.target
来验证。
注意⚠️
关于new.target
,MDN
中的定义是:new.target返回一个指向构造方法或函数的引用。
嗯哼,也就是说,返回的是构造函数。
我们可以在相应的构造中测试打印:
class MyDate extends Date { constructor() { super(); this.abc = 1;
console.log(‘~new.target.name:MyDate‘);new.target.name:MyDate~~~~ // MyDate
console.log(new.target.name); } } // new操作时的打印结果是: //
~
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来说)
区别:(以SubClass
,SuperClass
,instance
为例)
- ES5中继承的实质是:(那种经典组合寄生继承法)
- 先由子类(
SubClass
)构造出实例对象this - 然后在子类的构造函数中,将父类(
SuperClass
)的属性添加到this
上,SuperClass.apply(this, arguments)
- 子类原型(
SubClass.prototype
)指向父类原型(SuperClass.prototype
) - 所以
instance
是子类(SubClass
)构造出的(所以没有父类的[[Class]]
关键标志) - 所以,
instance
有SubClass
和SuperClass
的所有实例属性,以及可以通过原型链回溯,获取SubClass
和SuperClass
原型上的方法
- 先由子类(
- ES6中继承的实质是:
- 先由父类(
SuperClass
)构造出实例对象this,这也是为什么必须先调用父类的super()
方法(子类没有自己的this对象,需先由父类构造) - 然后在子类的构造函数中,修改this(进行加工),譬如让它指向子类原型(
SubClass.prototype
),这一步很关键,否则无法找到子类原型(注,子类构造中加工这一步的实际做法是推测出的,从最终效果来推测) - 然后同样,子类原型(
SubClass.prototype
)指向父类原型(SuperClass.prototype
) - 所以
instance
是父类(SuperClass
)构造出的(所以有着父类的[[Class]]
关键标志) - 所以,
instance
有SubClass
和SuperClass
的所有实例属性,以及可以通过原型链回溯,获取SubClass
和SuperClass
原型上的方法
- 先由父类(
以上⬆就列举了些重要信息,其它的如静态方法的继承没有赘述。(静态方法继承实质上只需要更改下SubClass.__proto__
到SuperClass
即可)
可以看着这张图快速理解:
有没有发现呢: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__是每个对象都有的属性。
我的学习图,没有备注的箭头表示__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__=={}
原文地址: https://www.aszrew.com/archives/1159.html
转载时必须以链接形式注明原始出处及本声明。