JS 原型与继承

245 阅读3分钟

前言

面向对象的三大特性:继承、封装、多态。JavaScript 作为一种面向对象的弱类型语言,与传统的面向对象语言的不同,JavaScript 是通过原型来实现继承的。

原型链

在学习 JavaScript 的继承之前,我们先了解 JavaScript 的原型链。

创建对象有几种方法

// 字面量
var obj = { name: 'zhangsan', age: 18 }
// new Object
var obj = new Object({ name: 'zhangsan', age: 18 })
// 通过构造函数创建对象
function M(name, age) {
  this.name = name
  this.age = age
}
var obj = new M('zhangsan', 18)
// Object.create
var obj = Object.create({ name: 'zhangsan', age: 18 })

构造函数、实例和原型之间的关系

我们从下图开始分析:

通过分析这个图,我们可以知道:

  • 构造函数通过 new 关键字,生成实例对象
  • 构造函数上有个 prototype 属性,指向构造函数的原型
  • 实例对象上有个 __proto__ 属性,指向构造函数的原型
  • 原型对象上有个 constructor 属性,指向构造函数

原型链的概念

当我们获取一个对象上的属性时,首先会在该对象本身的属性上查找,如果没有找到,则会去对象的 __proto__ 原型上查找,如果还没找到,会继续去原型的 __proto__ 上查找,直到为 null 时才会停止查找。这样一层一层往上的链式查找过程,我们称之为 原型链

instanceof 的原理

我们从图片分析:

当实例对象的 __proto__ 属性和构造函数的 prototype 属性相等时,instanceof 返回 true。 如果不相等,则会继续把原型的 __proto__ 属性与构造函数的 prototype 属性作比较,在原型链上只要有相等的,instanceof 返回 true,否则返回 false

举个🌰:

function M (name) {
  this.name = name
}
var m = new M('zhangsan')
console.log(m instanceof M)  // true
console.log(m.__proto__ === M.prototype) // true
console.log(m instanceof Object) // true
console.log(m.__proto__.__proto__ === Object.prototype) // true

new 运算符

var foo = function() {}
var obj = new foo()
  1. 一个新对象被创建,它继承自 foo.prototype
  2. 构造函数 foo 被执行。执行时,相应的传参会被传入,同时上下文(this)会被指定为这个新实例
  3. 如果构造函数返回了一个对象,则这个对象会取代整个 new 出来的结果返回。如果没有,则返回步骤1创建的对象

继承

类的声明和生成实例

// ES5 中类的声明
function Animal(name) {
  this.name = name
}

// ES6中类的声明
class Animal2 {
  constructor(name) {
    this.name = name
  }
} 

// 生成实例
var cat = new Animal('cat')
var dog = new Animal2('dog')

类的继承

第一种:借助构造函数继承

function Parent() {
  this.name = 'parent'
}
function Child() {
  Parent.call(this)
  this.type = 'child'
}
var child = new Child()
console.log(child.name)  // parent
console.log(child.type)  // child

缺点:只继承了父类构造函数的属性,没有继承父类原型上的属性

第二种:原型链实现继承

function Parent() {
  this.name = 'parent'
}
function Child() {
  this.type = 'child'
}
Child.prototype = new Parent()
var child = new Child()
console.log(child.name)  // parent
console.log(child.type)  // child
// 缺点:
var child2 = new Child()
child1.__proto__.name = 'haha'
console.log(child2.name)  // haha

缺点:

  1. 新实例无法向父类构造函数传参
  2. 共用一个原型对象,一个实例修改了原型属性,另一个实例原型上的属性也会被修改

第三种:组合继承

function Parent() {
  this.name = 'parent'
  this.arr = [1, 2, 3]
}
function Child() {
  Parent.call(this)
  this.type = 'child'
}
Child.prototype = new Parent()
// 解决了共用一个原型对象属性的问题
var child1 = new Child()
var child2 = new Child()
child1.arr.push(4)
console.log(child1.arr)  // [1, 2, 3, 4]
console.log(child2.arr)  // [1, 2, 3]

缺点:调用了两次父类构造函数,消耗内存

组合继承的优化1

function Parent() {
  this.name = 'parent'
}
function Child() {
  Parent.call(this)
  this.type = 'child'
}
Child.prototype = Parent.prototype
// 问题:child 原型上的构造函数并不是 Child
var child = new Child()
child.__ptoto__.constructor === Parent // true

缺点:无法判断实例的构造函数

组合继承的优化2

function Parent() {
  this.name = 'parent'
}
function Child() {
  Parent.call(this)
  this.type = 'child'
}
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
var child = new Child()
console.log(child instanceof Child) // true
console.log(child instanceof Parent) // true
console.log(child.constructor) // Child

优点:修复了组合继承的问题,是目前最常用的继承方式


欢迎关注我的前端公众号:抬头仰望星空

qrcode_for_gh_5735ed60e806_258 (1).jpg