关于javascript:一次性精通javascript原型继承构造函数类的原理下

40次阅读

共计 6537 个字符,预计需要花费 17 分钟才能阅读完成。

上篇咱们解说了构造函数和原型等前端概念的原理,晓得了实例之间如何通过构造函数的 prototype 来共享方法,下篇咱们次要看下用 es6 的 class 怎么来实现,以及 class 的继承等。同上篇一样,重点在于背地的原理,只有懂得了为什么要这么设计,咱们能力真正的说【精通】。

如果下篇你看得很辛苦,那阐明你对原型的把握还不够啊喂,请务必先熟读了解上篇。

ES6 的 class

回顾下之前 es5 的写法:

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 的写法:

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) 时:

  1. 创立了一个新对象zac
  2. constructor办法主动运行一次,同时把传参 'zac', 28 赋值给新对象

所以这个 class 到底是什么呢?其实 class 就是个函数而已。

console.log(typeof User) // function

那么 class 背地到底是怎么运作的呢?

  1. 首先创立了一个叫做 User 的函数
  2. 而后把 class 的 constructor 外面的代码一成不变的放到 User 函数里
  3. 最初将 class 的办法,如 grow,sing 放到 User.prototype 里

完事,看到了吗?javascript 的 class 只是构造函数的语法糖而已(当然 class 还做了一些其余的小工作)

es6 为咱们引进了类 class 的概念,看起来更靠近其余面向对象编程的语言了,但不同于其余 oop 语言的类继承,javascript 的继承依然是通过原型来实现的。

ES6 的 extend

咱们接下来看 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 提供给咱们的。次要有两个用法:

  1. super(...)是用来调用父类的 constructor 办法(只能在 constructor 里这么调用)
  2. super.method(...)是用来调用父类的办法

覆写父类的 constructor

咱们当初离开来看这两点,先解决第一个问题:为什么要在子类的 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 有一个本质区别:

  1. 前者会先创立一个空对象,而后把这个空对象赋值给 this
  2. 后者则间接不做这件事,因为它只须要等本人的父类做这个事就行了

所以,子类必须在本人的 construtor 里调用 super() 来让它的父类去执行父类的 constructor,否则 this 就不会被创立,而后如上例所示咱们就失去一个 error。

在 react 里为什么要写 super(props)?

顺便说一句,当初你应该能了解为什么咱们在写 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,因而:

  1. 如果是this.__proto__.sing(),调用者是 this.__proto__,相当于 admin.__proto__, 也就是 user 对象,所以最初打印进去的是:User is singing.
  2. 如果是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

运行下面的代码,马上就报错了,报错通知咱们超过了最大调用栈的范畴,这个错个别阐明咱们的代码呈现里有限循环调用。咱们再来逐层解析:

  1. 首先咱们来看调用办法是:superAdmin.sing(),所以运行第 (**) 句时,this=superAdmin,因而:
this.__proto__.sing.call(this) //(**)
// 相当于
superAdmin.__proto__.sing.call(this)
// 相当于
admin.sing.call(this) // 相当于咱们去执行 admin 里的 sing 办法时,this 依然是 superAdmin
  1. 而后就运行到了第 (*) 句,这时候 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

上面咱们比照的来看下,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 = User



const zac = new Admin('zac', 28, 'China')

好,先写到这里,剩下的早晨再更新 …

正文完
 0