上篇咱们讲解了构造函数和原型等前端概念的原理,知道了实例之间如何经过构造函数的prototype来共享方法,下篇咱们主要看下用es6的class怎么来实现,以及class的继承等。同上篇同样,重点在于背后的原理,只有懂得了为何要这么设计,咱们才能真正的说【精通】。javascript
假以下篇你看得很辛苦,那说明你对原型的掌握还不够啊喂,请务必先熟读理解上篇。前端
一次性精通javascript原型/继承/构造函数/类的原理(上)java
回顾下以前es5的写法:react
function User(name, age) { this.name = name this.age = age } User.prototype.grow = function(years) { this.age += years console.log(`${this.name} is now ${this.age}`) } User.prototype.sing = function(song) { console.log(`${this.name} is now singing ${song}`) } const zac = new User('zac', 28)
es6的写法:es6
class User { constructor(name, age) { this.name = name this.age = age } grow(years) { this.age += years console.log(`${this.name} is now ${this.age}`) } sing(song) { console.log(`${this.name} is now singing ${song}`) } } const zac = new User('zac', 28)
当咱们调用new User('zac', 28)
时:编程
zac
constructor
方法自动运行一次,同时把传参'zac', 28
赋值给新对象因此这个class究竟是什么呢?其实class就是个函数而已。segmentfault
console.log(typeof User) // function
那么class背后究竟是怎么运做的呢?闭包
完事,看到了吗?javascript的class只是构造函数的语法糖而已(固然class还作了一些其余的小工做)函数
es6为咱们引进了类class的概念,看起来更接近其余面向对象编程的语言了,但不一样于其余oop语言的类继承,javascript的继承仍然是经过原型来实现的。oop
咱们接下来看class之间的继承要怎么实现,es6为咱们提供了一个extends的方法:
class User { constructor(name, age) { this.name = name this.age = age } grow(years) { this.age += years console.log(`${this.name} is now ${this.age}`) } sing(song) { console.log(`${this.name} is now singing ${song}`) } } class Admin extends User { constructor(name, age, address) { super(name, age) //to call a parent constructor this.address = address } grow(years) { super.grow(years) // to call a parent method console.log(`he is admin, he lives in ${this.address}`) } } const zac = new User('zac', 28)
这里咱们重点看下两次super
的调用,首先要明确的是super
关键词是class
提供给咱们的。主要有两个用法:
super(...)
是用来调用父类的constructor方法(只能在constructor里这么调用)super.method(...)
是用来调用父类的方法咱们如今分开来看这两点,先解决第一个问题:为何要在子类的constructor里调用一下super()
?缘由很简单,由于javascript规定了:经过继承(extends)来而的class,必须在constructor里调用super()
,不然在constructor里调用this
会报错!
不信你看:
class Admin extends User { constructor(name, age, address) { this.name = name this.age = age this.address = address } ... } const zac = new admin('zac', 28, 'China') // VM1569:3 Uncaught ReferenceError: // Must call super constructor in derived class before accessing 'this' or returning from derived constructor
简单的解释下,javascript为何这么设计:
由于当使用new
来实例化class时,直接建立的class和经过extends来建立的class有一个本质区别:
因此,子类必须在本身的construtor里调用super()
来让它的父类去执行父类的constructor,不然this
就不会被建立,而后如上例所示咱们就获得一个error。
顺便说一句,如今你应该能理解为何咱们在写react组件时,为何要写这句super(props)
了吧?
class Checkbox extends React.Component { constructor(props) { // 如今你还没法使用this super(props); // 如今你可使用this啦 this.state = { isOn: true }; } // ... }
固然这里还有有个小问题,假如我不传props会怎样super()
?
// React内部的代码 class Component { constructor(props) { this.props = props; // ... } } // 咱们本身代码 class Checkbox extends React.Component { constructor(props) { super(); console.log(this.props); // undefined // }
这个不难理解吧?你不把props传给父组件,天然没法在constructor里调用this.props咯。但其实你在其余地方仍是能够正常调用this.props的,由于react帮咱们多作了一件事:
// React内部的代码 const instance = new YourComponent(props); instance.props = props;
相对来讲,super.method()
就好理解多了。咱们的父类User里有一个grow的方法,咱们的子类Admin也想有这个方法,同时可能还想在这个方法上再加点别的操做。因此呢,它就先经过class提供的super来先调用一遍父类的grow方法,而后再添加本身的逻辑。
这里有一些爱思考的同窗可能就会想了,为何我能够经过super来调用父类的方法呢?为何我能够写super.method()
呢?若是你在考虑这个问题,说明你真的很爱思考,你很棒!
简单来理解,既然super.method()
是调用父类的方法,而咱们的子类又是经过继承父类而来的,结合以前讲过的原型的知识,那super.method()
是否是就应该至关于this.__proto__.method
呢?直观上来讲确实应该如此,咱们作个简单的实验来看下:
let user = { name: "User", sing() { console.log(`${this.name} is singing.`) } } let admin = { __proto__: user, name: "Admin", sing() { this.__proto__.sing.call(this) //(*) console.log('calling from admin') } } admin.sing(); // Admin is singing. calling from admin
能够看到,user对象是admin对象的原型,主要看下(*)
这句话,咱们在当前对象的上下文(this)里调用了原型对象user的sing方法。注意我用了.call(this)
,若是没有这个的话,咱们执行this.__proto__.sing()
时是在原型对象user的上下文里执行的,因此执行this.name
时this指向的是user对象:
... let admin = { __proto__: user, name: "Admin", sing() { this.__proto__.sing() console.log('calling from admin') } } admin.sing(); // User is singing. calling from admin
这里顺便解释下this
,敲黑板了,无论你是在对象里仍是原型了发现了this
,它永远是点(.)左边的那个对象。假如是user.sing()
那this是(.)左边的user;假如admin.sing()
那this就是(.)左边的admin。
而后咱们再看下上面的例子,咱们调用的方法是admin.sing()
,因此运行admin中的sing方法时,this就是admin,所以:
this.__proto__.sing()
,调用者是this.__proto__,至关于admin.__proto__, 也就是user对象,因此最后打印出来的是:User is singing.this.__proto__.sing.call(this)
,这时候咱们经过call手动将调用者改成admin了,因此最后打印出来是:Admin is singing.好了,有点扯远了,咱们再回来。刚刚的例子好像确实证实了super.method()
至关于this.__proto__.method
,咱们再看下面的代码:
let user = { name: "User", sing() { console.log(`${this.name} is singing.`) } } let admin = { __proto__: user, name: "Admin", sing() { this.__proto__.sing.call(this) //(*) console.log('calling from admin') } } let superAdmin = { __proto__: admin, name: "SuperAdmin", sing() { this.__proto__.sing.call(this) //(**) console.log('calling from superAdmin') } } superAdmin.sing(); // VM1900:12 Uncaught RangeError: Maximum call stack size exceeded
运行上面的代码,立刻就报错了,报错告诉咱们超过了最大调用栈的范围,这个错通常说明咱们的代码出现里无限循环调用。咱们再来逐层解析:
superAdmin.sing()
,因此运行第(**)
句时,this=superAdmin,所以:this.__proto__.sing.call(this) //(**) //至关于 superAdmin.__proto__.sing.call(this) //至关于 admin.sing.call(this) //至关于咱们去执行admin里的sing方法时,this仍然是superAdmin
(*)
句,这时候this=superAdmin,所以:this.__proto__.sing.call(this) //(*) //至关于 superAdmin.__proto__.sing.call(this) //至关于 admin.sing.call(this) //又回到了这里
而后,结局你就知道,admin.sing不断循环地调用者本身。因此啊,单纯的经过this是没法解决这个问题的。javascript为了解决这个问题设计了一个新的内部属性[[HomeObject]]
,每当一个函数被指定为一个对象的方法时,这个方法就有了一个属性[[HomeObject]]
,这个属性固定的指向这个对象:
let user = { name: "User", sing() { console.log(`${this.name} is singing.`) } } //admin.sing.[[HomeObject]] == admin let admin = { __proto__: user, name: "Admin", sing() { super.sing() console.log('calling from admin') } } // admin.sing.[[HomeObject]] == admin let superAdmin = { __proto__: admin, name: "SuperAdmin", sing() { super.sing() console.log('calling from superAdmin') } } superAdmin.sing() // SuperAdmin is singing. // calling from admin // calling from superAdmin
ok,当咱们运行superAdmin.sing()
时,也就是执行super.sing()
,每当super
关键词出现,javascript引擎就会去找当前方法的[[HomeObject]]
对象,而后去找这个对象的原型,最后在这个原型上调用相应的方法。
因此当咱们调用superAdmin.sing()
时,至关于执行:
const currentHomeObject = this.sing.[[HomeObject]] const currentPrototype = Object.getPrototypeOf(currentHomeObject) currentPrototype.sing.call(this)
下面咱们对比的来看下,es5是怎么实现extends语法的:
function User(name, age) { this.name = name this.age = age } User.prototype.grow = function(years) { this.age += years console.log(`${this.name} is now ${this.age}`) } User.prototype.sing = function(song) { console.log(`${this.name} is now singing ${song}`) } function Admin(name, age, address) { User.call(this, name, age) //(*) this.address = address } Admin.prototype = Object.create(User.prototype) //(**) Admin.prototype.grow = function(years) { User.prototype.grow.call(this, years) console.log(`he is admin, he lives in ${this.address}`) } Admin.prototype.constructor = Admin //(***) const zac = new Admin('zac', 28, 'China')
若是你完整的吸取理解了上下篇的内容,上面的代码应该很好理解了吧?我仍是带着你们再来解析一次:
(*)
句,咱们但愿Admin构造函数可以拥有User构造函数的属性name和age,因此咱们用当前的上下文this去执行一遍User构造函数(**)
句,咱们将Admin.prototype设置为以User.prototype为原型的新对象。这里你可能有疑问为何要用Object.create,而不是直接把User.prototype赋值给Admin?缘由就是咱们不但愿Admin和User共用同一个prototype啊,这是咱们为何要使用继承的初衷啊(***)
句作的事好,写到这里我以为差很少了,咱们从建立一个对象讲起,因为想要批量建立对象咱们讲到了构造函数,又由于对象之间想要共享方法从而讲到了原型,最后瓜熟蒂落讲到了对象的继承。整个脉络应该是比较清晰的。
这是《前端原理系列》的初篇,下一篇按计划应该是讲调用栈/执行上下文/闭包/事件循环机制这个主题,你们记得关注,下期再会。