前言
通过上篇文章后我们了解到了原型的概念,在下篇中将上篇剩下的 原型链 和 class 补充完整。
什么是原型链?
原型链 顾名思义就是由多个原型对象组成。每个原型对象都有 __proto__ 属性并指向创建该原型对象的构造函数 prototype 原型,多个原型之间通过 __proto__ 连接形成原型链,这就是JavaScript实现继承和共享属性的方式。
__proto__和prototype要分清,prototype只存在于构造函数中,同时构造函数拥有__proto__,实例不存在prototype。
下面先来看看如何实现原型链:
function Father () {}
Father.prototype.surname = '李'
Father.prototype.saySurname = function () {
return this.surname
}
function Son () {}
// 继承Father
Son.prototype = new Father()
Son.prototype.name = '小俊'
Son.prototype.sayName = function () {
return this.name
}
const baby = new Son()
console.log('surname is ' + baby.saySurname()) // surname is 李
console.log('name is ' + baby.sayName()) // name is 小俊
上面的例子分别是定义了 Father 和 Son 两个构造函数,Father 原型上具有 surname 和 saySurname 属性和方法,定义了 Son 后,通过创建一个 Father 实例并将该实例整体替换了 Son 原有的 prototype,实际上就是重写 Son 原型的操作,此时 Son 拥有了 Father 实例中的属性和方法,这就是原型继承机制。在实现了继承关系后修改从 Son 的新原型里添加了 name 和 sayName,这使得 Son 同时具有 Father 的属性和方法,也有自己的方法。
最后通过 new 和 Son 创建的 baby 实例,打印这个实例之后可以看到它的结构如下:
通过上图可以明显的看出 baby 的结构,__proto__ 内部又有 __proto__,第一层的 __proto__ 实际上是 Son 的 prototype,在继续下一层是 Father 的 prototype,到了最底层就是 Object,这个说明了“Object 是原型链的最顶层原型对象”,同时也说明了 “万物皆对象”这句话。
可能在看上面的结构的时候会不理解为什么会是这样子的结构,不慌,只要理解了 Son 在继承 Father 的时候,JavaScript究竟做了什么就行。
// 继承Father
// Son.prototype = new Father()
// JavaScript干了下面这些事
Son.prototype = {}
Son.prototype.__proto__ = Father.prototype
Father.apply(Son.prototype)
上面就是在继承时,JavaScript所做的事,我们来看看 Son 的原型在继承前和继承后分别是什么样子的。
不难看出,继承前,Son的原型链是 Son — Object的,继承之后就变成了 Son — Father — Object,通过断链重连的方式来实现继承。
Son.prototype.__proto__ === Father.prototype
Father.prototype.__proto__ === Object.prototype
Object.prototype.__proto__ === null
原型链的属性查找机制:当查找对象的属性时,如果在对象中找不到该属性时,会沿着该对象的原型链上的原型继续寻找,即对象的 __proto__ 和 构造函数的 prototype,若在原型链中的某一处原型找到则会返回该属性并停止寻找,若直到原型链的最顶层的原型对象 Object.prototype 都找不到则返回 null。
ES6的 class
在ES6之前,如果要定义一个类,都要定义一个构造函数,并设置这个构造函数的 prototype,如果我们不需要在构造方法中做任何操作的话,就必须要定义一个空函数,这样子来定义一个类,感觉会比较多余且复杂,甚至会混淆了函数和类的概念,所以ES6的开发者为了改善类的定义和写法,新增了像Java那样子的 class。
如何声明一个类
ES6在声明一个类是非常的简单,同时并不会改变JavaScript的原型机制,并在原有的基础上新增了一些语法糖,如果学过Java的话,那么就应该很熟悉ES6的class。
class Person {
// 构造方法
constructor (name) {
// 私有属性
this.name = name
}
// 静态属性
static sex = '男'
// 原型上的方法
sayName () {
console.log(this.name)
}
// 原型上的属性
get age () {
return 18
}
}
与ES6之前不同的是,定义一个类,将构造方法、原型和静态属性都写在类的结构中,这时候类的定义语法就简单明了得多,上面的代码在ES6之前的写法是这样子的:
// 构造方法
function Person (name) {
// 私有属性
this.name = name
}
// 静态属性
Person.sex = '男'
// 原型上的方法
Person.prototype.sayName = function () {
consle.log(this.name)
}
// 原型上的属性
Person.prototype.age = 18
构造方法:在创建实例中调用的方法。
私有属性:创建实例后实例的私有属性,只在创建实例时调用构造函数才创建,不会出现在类的原型上,与原型上的属性不同,修改私有属性不会影响其他通过该类创建的实例。
静态属性:不会被创建的实例继承,只能通过类名直接调用。
函数声明和类声明的区别
- 类声明内部的代码运行是强行在严格模式下运行的。
- 函数和类生命在
typeof后都是返回function,函数声明可以直接调用构造方法,但类声明只能通过new关键字调用构造方法constructor,不能直接调用。 - 函数声明可以提升,但类不能提升,存在暂时性死区。
- 通过函数声明来创建实例,修改
实例.__proto__的属性会影响其他通过该函数创建的实例的属性;在类声明中无法直接从实例修改类的原型。 - 如果不需要在构造方法中做任何处理时,函数声明就必须要声明一个空函数,而类声明则可以不用写
constructor构造方法,因为会默认有一个空的构造方法。 - 类声明的原型上,方法和属性都是只读和不可枚举的,除非通过
Object.defineProperty来设置原型的属性类型。
类的成员属性名称可以是变量
在定义类的成员名称时,可以使用 [] 包裹一个表达式来实现定义成员名称,如下代码:
let actionName = 'sayHellow'
let staticName = 'hahaha'
let getName = 'age'
class Person {
static [staticName] = 666;
[actionName] () {
console.log('hellow word')
}
get [getName] () {
return 18
}
}
let person = new Person()
console.log(person.age) // 18
person.sayHellow() // hellow word
// 注意如果这时候修改变量的话,类和实例的属性名是不会一起修改的
actionName = 'sayHellow1'
person.sayHellow() // hellow word
person.sayHellow1() // [TypeError: person.sayHellow1 is not a function]
类的静态成员
类中定义静态成员只需要在属性名前加 static 关键字即可,static 修饰的成员无法被实例继承,只能通过类名来访问。如下:
class Person {
static sayHellow () {
console.log('I am static sayHellow')
}
}
let p = new Person()
Person.sayHellow() // I am static sayHellow
p.sayHellow() // [TypeError: p.sayHellow is not a function]
// ----- 这是条分割线 ----- //
// 函数声明式定义静态成员
function Person () {}
Person.sayHellow = function () {
console.log('I am static sayHellow')
}
类中可以定义同名的静态成员和公共成员,没有
static修饰的成员可以被实例继承。
class Person {
static sayHellow () {
console.log('I am static sayHellow')
}
sayHellow () {
console.log('I am prototype sayHellow')
}
}
let p = new Person()
Person.sayHellow() // I am static sayHellow
p.sayHellow() // I am prototype sayHellow
类的表达式
类可以像函数表达式那样子写类表达式,也可以写成IIFE(立即调用表达式)。
// 类表达式
let Person = class {
sayName () {
console.log('啊俊俊')
}
}
let person = new Person()
person.sayName() // 啊俊俊
// IIFE
let person = new class {
constructor (name, sex) {
[this.name, this.sex] = [name, sex]
}
say () {
console.log(`My name is ${this.name},I am a ${this.sex}`)
}
}('啊俊俊', 'boy')
person.say() // My name is 啊俊俊,I am a boy
类的继承
ES6中类的继承和Java相似,都是通过 extends 实现继承,这样子比通过“断链重连”的语法要更加清晰明了。在子类继承父类之后,会将父类的 prototype 和 static 静态属性都一起继承。
class Father {}
class Son extends Father {}
如何在子类中调用父类的构造方法
通过 extends 继承父类,会将父类的原型和静态属性都一起继承,如果要在子类中调用父类的构造方法时就要调用 super() 方法,同时 super() 只能在子类中进行调用,不能从父类进行调用。
class Person {
constructor (name, sex) {
[this.name, this.sex] = [name, sex]
}
}
class Child extends Person {
constructor (name, sex, girlfriend) {
super(name, sex)
this.girlfriend = girlfriend
}
say () {
console.log(`My name is ${this.name}, I am a ${this.sex}, my girlfriend is ${this.girlfriend}`)
}
}
let me = new Child('啊俊俊', 'boy', '君君')
me.say() // My name is 啊俊俊, I am a boy, my girlfriend is 君君
super 的调用方式
super 关键字既可以当做方法使用,也可以当做对象使用,但使用方式是有区别的。
super 用作函数调用时
super()是调用父类的构造方法constructor,同时只能在子类的constructor中调用。super()在子类的constructor里必须先于this调用。
// 错误一,super() 不在子类的 constructor 中调用
class Father {}
class Son extends Father {
sayName () {
super() // [SyntaxError: 'super' keyword unexpected here]
}
}
// 错误二,子类的 constructor 内调用 this 在 super() 之前。
class Father {}
class Son extends Father {
constructor () {
this.name = 1
super() // [ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor]
}
}
let s = new Son()
super 用作对象调用时
当 super 用作对象调用的时候,在子类的原型方法内,调用时 super 指向父类原型,在子类的静态方法内,指向父类。
class Father {
static fatherSay () {
console.log('我是父类的static')
}
fatherSay () {
console.log('我是父类的prototype')
}
}
class Son extends Father {
static sonSay () {
super.fatherSay()
}
sonSay () {
super.fatherSay()
}
}
let son = new Son()
Son.sonSay() // 我是父类的static
son.sonSay() // 我是父类的prototype
关于 super 时的this指向
- 在子类的原型方法中通过
super调用父类的方法时,super会将this指向子类。
class Father {
constructor () {
this.name = '爸爸'
}
sayName () {
console.log(this.name)
}
}
class Son extends Father {
constructor () {
super()
this.name = '宝贝儿子'
}
say () {
super.sayName()
}
}
let son = new Son()
son.say() // 宝贝儿子
上面的代码在调用 son 实例的 say 时,say调用super来调用父类,同时会通过call来绑定this指向,super.sayName.call(this)。
- 通过
super.xxx = xxx来对某个super的某个属性进行赋值,由于super绑定了this的原因,这种方式的赋值都会变成子类实例的属性。
class Father {
constructor () {
this.name = '爸爸'
}
}
class Son extends Father {
constructor () {
super()
this.name = '宝贝儿子'
}
changeSuper () {
super.age = 18
super.name = '宝贝女儿'
}
say () {
console.log(super.age) // undefined
console.log(this.name) // 宝贝女儿
console.log(this.age) // 18
}
}
let son = new Son()
son.changeSuper()
son.say()
上面的代码中,在 changeSuper 方法里对 super 的属性进行赋值,但这里的赋值实际上是 this.age = 18 和 this.name = '宝贝女儿',从而让子类拥有了 age 这个属性,如果是获取 super.age 的话就是获取 Father.prototype.age,由于父类并没有这个属性,所以返回 undefined。
原型属性重写
在子类继承父类之后,可以直接在子类重写父类的原型属性,直接看下面的代码:
class Father {
say () {
console.log('我是父类的方法')
}
}
class Son extends Father {
say () {
console.log('我是子类的方法')
}
}
let son = new Son()
son.say() // 我是子类的方法
判断实例的类型
在创建实例之后,通过 instanceof 来判断类型的时,若右侧的类型在该实例的原型链上存在,判断的结果都会是 true,所以在使用 instanceof 来判断类型时要注意一下。
class Father {}
class Son extends Father {}
let f = new Father()
let s = new Son()
console.log(f instanceof Father)// true
console.log(f instanceof Son) // false
console.log(f instanceof Object) // true
console.log(s instanceof Father) // true
console.log(s instanceof Son) // true
console.log(s instanceof Object) // true
总结
不得不说,原型和原型链所涉及的范围都比较大,同时也是JavaScript的基础,在面试或笔试的时候面试官都非常喜欢问,在这两篇文章中,先从上篇的原型中的 函数对象、构造函数、实例、new、prototype、__proto__、contructor 入手理解原型,在到下篇的 原型链 和 class 进一步了解整个原型相关的概念,其中可能会有部分的知识点遗漏,但也希望能给你带来收获。