【js】原型链

130 阅读4分钟

js 和 Java、C++ 等传统面向对象的编程语言不同,它是没有类(class,ES6 中的class也只不过是语法糖,并非真正意义上的类)的概念,而在 js 中,一切皆是对象(object)。在基于类的传统面向对象的编程语言中,对象由类实例化而来,实例化的过程中,类的属性和方法会被拷贝到这个对象中;对象的继承实际上是类的继承,在定义子类继承于父类时,子类会将父类的属性和方法拷贝到自身当中。这类语言中,对象创建和继承行为都是通过拷贝完成的。但在 js 中,对象的创建、对象的继承(更好的叫法是对象的代理,因为它并不是传统意义上的继承)是不存在拷贝行为的。因此类、继承这些特点都不属于 js

prototype 、 __proto__ 以及 constructor

__proto__constructor是每个对象都有的属性,而prototype是函数对象独有的属性

__proto__属性指向它们的原型对象(也可以理解为父对象),作用就是当访问一个对象的属性时,如果该对象内部不存在这个属性,那么就会去它的__proto__属性所指向的那个对象(父对象)里找,如此反复,直到__proto__属性的值为 null,然后返回 undefined,__proto__的终点是 null。通过__proto__属性将对象连接起来的这条链就是 原型链

prototype属性指向这个函数的原型对象,也就是这个函数创建的实例对象的原型对象,作用就是让该函数所实例化的对象们都可以找到公用的属性和方法。任何函数在创建的时候,其实会默认同时创建该函数的原型对象

constructor属性指向该对象的构造函数,每个对象都有构造函数(本身拥有或继承而来),Function对象比较特殊,它的构造函数就是自己,所以constructor属性的终点就是Function函数

function People(){
    ...
}
var people = new People();
console.log(people.__proto__ === People.prototype)//true
console.log(people.constructor === People.prototype.constructor)//true

关系图如下:

graph LR
prototype(原型对象)
constructorFunc(构造函数)
object(实例对象)
constructorFunc --new--> object
constructorFunc --prototype--> prototype
prototype --constructor--> constructorFunc
object -.constructor.-> constructorFunc
object --__proto__--> prototype

js 实现类

在 ES6 之前 js 还没有 class 关键字,可以使用一下几种方式模拟声明类:

1、工厂模式

function createPerson(name){
    var person = new Object();
    person.name = name;
    return person;
}
var person = createPerson('Leo')

2、构造函数法:

function Person(name){
    this.name = name;//this 代表新创建的实例对象
}
var person = new Person('Leo')

在 ES6 新增 class 关键字声明类(其实只是语法糖):

class People{
    name = 'default';
    constructor(name){
        this.name = name;
    }
}

js 实现继承

背景

js 是由 Brendan Eich 负责开发的,当时面向对象很兴盛,Brendan Eich 无疑也受到了影响,js 中一切皆为对象,所以必须有一种机制,将所有对象联系起来。但是,Brendan Eich 没引入"类"的概念,因为一旦有了"类",js 就是一种完整的面向对象编程语言了,过于正式了。想到 C++ 和 Java 使用 new 命令时,都会调用"类"的构造函数 constructor。他就做了一个简化的设计,在 js 中,new 命令后面跟的不是类,而是构造函数

构造函数继承

说明:直接利用 call 或者 apply 方法将父类构造函数的 this 绑定为子类构造函数的 this 就可以

缺点:无法继承父类原型链上的属性与方法

function Parent(){
    this.name = "parent's name"
}
Parent.prototype.say = function(){
	console.log(this.name)
}
function Child(){
  Parent.call(this)  //构造函数继承核心代码
}
let child = new Child()
console.log(child.name) // parent
console.log(child.say()) // child.say is not a function
原型继承

说明:将子类的原型挂载到父类上

缺点:子类 new 出来的实例,父类的属性没有隔离,会相互影响

function Parent() {
  this.arr = [1, 2, 3]
}
function Child() {
}
Child.prototype = new Parent() //原型继承核心代码
let child1 = new Child()
let child2 = new Child()
child1.arr.push(4)
console.log(child1.arr, child2.arr) // [1, 2, 3, 4]  [1, 2, 3, 4]
组合继承

说明:组合上面的构造函数与原型继承的功能

缺点:call() 方法已经拿到父类所有的属性 ,后面再使用原型时也会有父类所有属性

function Parent() {
  this.name = 'parent'
}
Parent.prototype.say = function () {
	console.log(this.name)
}
function Child() {
  Parent.call(this)
}
Child.prototype = new Parent()
let child = new Child();
child:
	Child {name: "parent"}
	name: "parent"
    __proto__: Parent
        name: "parent" // 重复的属性
        __proto__:
            say: ƒ ()
            constructor: ƒ Parent()
            __proto__: Object
寄生式组合继承

说明:解决组合继承重复属性的问题,直接将子类的原型等于父类的原型,或者是用 Object.create 继承原型但不执行父类构造函数(目前最完美的继承实现方式)

function Parent() {
  this.name = 'parent'
}
Parent.prototype.say = function () {
	console.log(this.name)
}
function Child() {
  Parent.call(this)
  this.type = 'child'
}
Child.prototype = Parent.prototype
Child.prototype.constructor = Child
//修复重写子类原型导致子类constructor属性被修改
Class 继承

说明:ES6 新增,class 是一个语法糖,基于寄生组合继承来实现

class Parent{
	constructor(){
        this.name = 'parent'
        this.arr = [1, 2, 3]
    }
    say(){
  	    console.log(this.name)
    }
}
class Child extends Parent{
    constructor(){
        super() // 通过 super() 调用父类构造函数
    }
}