JS原型链的理解 & JS如何实现继承

269 阅读3分钟

谈谈你对原型链的理解

  • 原型对象和构造函数有何关系?

在JavaScript中,每当定义一个函数数据类型(普通函数、类)的时候,这个函数就会自带一个prototype属性,这个属性指向函数的原型对象

另外,这个原型对象会有一个constructor属性指回函数本身

当函数被new调用的时候,这个函数就称为了构造函数

new调用会创建一个实例对象,这个实例对象会自带一个__proto__属性,这个属性指向构造函数的原型对象

o.__proto__ === Foo.prototype

image.png
  • 描述一下原型链?

JavaScript对象通过__proto__指向父类原型对象,直到指向Object对象为止,这样就形成了一个原型指向的链条,即原型链

原型链最顶层的原型对象就是Object的原型对象

另外,检查对象自身是否包含某属性,可以使用 hasOwnProperty()

使用 in 的话,即使自身没有,但是原型链中有,也会返回true

绿色箭头,原型链

o.__proto__.__proto__.__proto__

image.png

JS如何实现继承?

方式1:借助原型链

// 父类
function Person() {
    this.name = 'pName'
    this.age = 18
    this.friends = []
}

Person.prototype.sayHello = function() {
    console.log('Hello');
}

// 子类
function Student() {
    this.height = 1.88
}

let p = new Person() // 获得Person类的属性方法
console.log(p);

// 我们让Student的原型指向p
Student.prototype = p

let stu = new Student()
console.log(stu.name); // pName
stu.sayHello() // Hello
// 可见我们已经拿到了父类的属性和方法

这种实现方法存在的问题:

1.当我们打印实例stu的时候,继承的属性不能直观看到,只能在__proto__属性中看到

console.log(stu)// Student {height: 1.88}
image.png

2.另外,如果我们创建两个实例对象,并改变friends属性

stu1.friends.push('aaa')
console.log(stu2.friends); // ['aaa']

明明改变的是stu1的属性,为什么stu2也变了?

因为我们它们的隐式原型指向的是同一个对象,p

3.前面实现的过程都没有传递参数

方式2:借助构造函数

使用call调用构造函数,传入的this为当前实例,并且可以把参数传给父类处理

// 父类
function Person(name, age) {
    this.name = name
    this.age = age
    this.friends = []
}

Person.prototype.sayHello = function() {
    console.log('Hello');
}

// 子类
function Student(name, age, height) {
    Person.call(this, name, age) // 这里可以获得父类的属性
    this.height = height
}

let p = new Person() // 依然需要这里来获得方法
Student.prototype = p

let stu1 = new Student('aaa', 18, 1.78)
let stu2 = new Student('bbb', 20, 1.88)

stu1.friends.push('aaa')
console.log(stu1);
console.log(stu2);
stu1.sayHello() // Hello

构造函数的方法可以解决原型链方式的问题,但是依然存在弊端

  1. Person 至少被调用两次(一开始new Person一次,后面Person.call又会调用Person)
  2. stu的原型对象上会多出一些属性, 但是这些属性是没有存在的必要(new Person的时候的)
image.png

方式3:寄生组合式继承(最终方案)

**Object.create()**方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。

Object.create(XXX) 也就是传入的对象XXX,作为新对象的原型

// 父类
function Person(name, age, friends) {
    this.name = name
    this.age = age
    this.friends = friends
}

Person.prototype.sayHello = function() {
    console.log('Hello');
}

// 子类
function Student(name, age, friends, height) {
    // 获取父类中的属性和方法
    Person.call(this, name, age, friends)
    this.height = height
}

// 再获取一份父类的prototype中的属性和方法
Student.prototype = Object.create(Person.prototype)
Student.prototype.constructor = Student

let stu = new Student('aaa', 18, ['fre1'], 1.88)
console.log(stu);
image.png

当然,我们也可以封装一个方法来继承父类prototype中的属性和方法

function inheritPrototype(SubType, SuperType) {
  SubType.prototype = Object.create(SuperType.prototype)

  // 当然子类的prototype还需要有constructor指向子构造函数本身
  Object.defineProperty(SubType.prototype, "constructor", {
    enumerable: false,
    configurable: true,
    writable: true,
    value: SubType
  })
}
...
inheritPrototype(Student, Person)