一、翻越前端的三座大山——继承/原型链

文章目录

  • 原型专题
    • 前言
    • 什么是原型?
    • 实例和原型的关系
    • 什么是原型链?
    • Object 和 Function 的继承关系
    • 原型运用例子:
      • 手写instanceof
      • 手写call,apply
      • 手写new
      • 六大继承方式
        • 原型链继承
        • 构造函数继承
        • 原型链 + 构造函数
        • 优化(原型链 + 构造函数)
        • 再次优化(原型链 + 构造函数)
        • ES6中的class继承
    • 总结

原型专题

前言

这是一篇详细介绍Javascript中原型和继承的内容,从里到外透析继承在Javascript中的秘密,文中会参杂一些常见有关继承的面试题目,耐心看完相信会有收获。

什么是原型?

这是一个比较抽象的概念。

在Javascript中的各种官方定义好的对象如,Object和Function等其实都是由一个源对象创建出来的,这是所有数据对象的起源,我们自己定义的构造函数等其实本质上都来源于这个源对象。

实例和原型的关系

首先我们要理顺三个概念,构造函数,实例,原型这三者直接的关系。所有Javascript有关原型的内容其实都是在这三者间反复横跳。

function Person() {
}
let person = new Person()

在上面的代码中,Person就是我们的构造函数,person 就是构造函数new出来的实例。那么原型是?

从上面的原型概念已经说了,一个对象的原型是来源于创建时关联的对象。那我们应该如何找到这个对象呢?

这里需要借助两个东西,prototype 和 proto

我们构造函数可以借助prototype找到原型,而实例可以借助proto找到原型。

console.log(Person.prototype) 
console.log(Person.prototype === person.__proto__) // true

同样的原型也可以找到他生成的构造函数的,利用 constructor

console.log(Person.prototype === Person.prototype.constructor) // true

介绍到这里我们应该大致了解的原型,构造函数,实例之间的关系了。

用一张图来表示就是这样。

一、翻越前端的三座大山——继承/原型链_第1张图片

到这里,我们来正式的介绍一下prototype,proto和constructor三者。

  • prototype:构造函数用来指向原型对象,也是我们new出一个实例的关键属性。
  • proto:这是每一个对象都会有的属性(null除外),实例用来指向原型对象的属性。
  • constructor:原型对象用来指向构造函数的属性。

什么是原型链?

首先我们要明白为什么要有原型链。

我们的原型之所以重要,很重要一点是每个实例都会继承原型的属性,重点在于继承这个功能。

当我们读取一个实例的属性,如果找不到该属性,我们就可以顺着原型链去寻找这个实例的原型有没有该属性,如果还没有则一直找下去,直到找到最顶层。

举个例子:

function Person() {
}
Person.prototype.name = 'lyy';
person.name = 'LYY';
console.log(person.name) // LYY

delete person.name;
console.log(person.name) // lyy

在这个例子中,我们最开始会找实例有没有name这个属性,如果找得到就直接输出,如果找不到就去找原型的name。

通过上面这个例子,应该知道原型链大概是个什么东西了,相互关联的原型组成的链状结构就是原型链,也就是蓝色的这条线。

一、翻越前端的三座大山——继承/原型链_第2张图片

在源对象后面就是null。

Object 和 Function 的继承关系

既然我们定义一个函数function,我们依托这个函数生成的实例必然会继承一个对象,那么function继承的对象是否就是Object?

我们构造函数的Function和Object到底是什么关系?

举一个例子:

function Person() {
}
var obj = {
	 name: 'lyy',
	 age: 23,
}

如果我们对Object深挖下去的话, 可以发现对于Object来说,他确实是离源对象最近的一个对象。

而且直接用var obj出来的对象其实就是Javascript内置Object构造函数的一个实例。

因此我们不难发现,obj确实是继承链条上的顶点了。

console.log(Object.prototype)  // 源对象
console.log(Object.__proto__)  // f()
console.log(Object.__proto__.__proto__)  // 源对象
console.log(Object.__proto__.__proto__.__proto__) // null
console.log(Object.prototype === obj.__proto__)  // true

那么对于function呢?

Funtion的prototype指向的是和Object.proto一样的东西,我们通过打印出来会发现他其实是一个函数fn

这是啥?我相信很多人看了都会疑问

console.log(Function.prototype === Object.__proto__) // true
console.log(Function.__proto__ == Function.prototype)  // true
console.log(Person.__proto__.__proto__ === Object.prototype) // true

为了形象表述,我将整个流程大概画出来,如下图。
一、翻越前端的三座大山——继承/原型链_第3张图片

实际上我们发现Funtion和Object其实也没什么太大联系,也不是我们之间谁继承谁,只是他们的源对象都是一致,并且为Funtion的prototype指向的是和Object.proto一样。

通过查阅资料发现:

Function.prototype 对象是一个函数对象(它的 [[Class]] 是 Function),Function.prototype 可以被调用,它接受任何参数,并且返回 undefined。
但是 Function.prototype 的原型是 Object.prototype。

翻译过来也就是说:

  • Function.prototype 是一个函数
  • 但是 Function.prototype.proto === Object.prototype

太奇葩了,而官方解释是:

The Function prototype object is specified to be a function object to ensure compatibility with ECMAScript code that was created prior to the ECMAScript 2015 specification.

翻译:这么做是为了兼容之前的 ECMAScript 代码。

所以function和Object并不是相互继承的关系,他们之间的fn函数是来源于历史遗留问题。

原型运用例子:

下面写一些依据原型继承的常见面试八股吧

手写instanceof

instanceof是判断构造函数和实例对象在原型链上是否有关系,有就返回true,否则就返回false

这个就是典型的利用原理链去比对即可,我们知道构造函数可以用prototype找到原型,而实例对象也可以靠proto找到原型,还能在此基础上不断向上搜索。

function myInstanceof(target,origin) {
    while(target) {
        if (target.__proto__ === origin.prototype) return true;
        target = target.__proto__;
    }
    return false;
}

手写call,apply

call 和 apply都是用来继承的,写一个方法,让一个新对象来继承他的方法。

call作用:提供新的 this 值给当前调用的函数/方法。更简单点的理解,call把this换成了obj的this,并且给调用的函数用了。

手写call:
call 需要完成两个点:

  • 改变this指向,将之变为传入的对象this
  • 可以传入一个不定长的参数。
Function.prototype.call2 = function (obj) {
    obj = obj || window;  // 如果传进来为null,则为函数应该挂到window上
    obj.fn = this;

    var args = [];
    for (let i = 1; i < arguments.length; i ++ ) {
        args.push('arguments[' + i  +']');
    }
    var res = eval('obj.fn(' + args + ')');

    delete obj.fn;
    return res;
}

手写apply:
apply和call实现一样,唯一区别他会给出一个数组参数。

Function.prototype.apply2 = function(obj,arr) {
    var obj = obj || window;
    obj.fn = this;

    var res,args = [];
    for (let i = 0; i < arr.length; i ++ ) {
        args.push('arr[ ' + i + ']');
    }

    res = eval('obj.fn(' + args + ')');

    delete obj.fn
    return res;
}

手写new

new的作用是利用构造函数来创建一个实例。

实际上,我们创建一个对象非常容易,直接var obj就可以立即创建,这是第一步。

第二步,我们创建的实例是依据于对应的构造函数的,所以从上面的原型链学习就已经知道了,实例的proto指向必须指向原型,也就是和构造函数的prototype一致。

第三步,我们要继承构造函数的属性和方法,所以继承的手段我们直接用call或者apply即可。

function mynew(fn, ...args) {
    // 1. 创建一个新对象
    const obj = {};
    // 2. 为新对象添加属性__proto__,将该属性链接至构造函数的原型对象
    obj.__proto__ = fn.prototype;
    // 3. 执行构造函数,this被绑定在新对象上
    const res = fn.call(obj, ...args);
    // 4. 确保返回一个对象
    return res instanceof Object ? res : obj;
}

在这里还是说明一下最后一步为什么要来判断吧。

call的返回值和构造函数的返回值有关,如果构造函数有返回值就会返回构造函数的返回值,如果没有的话就会返回undefined。

所以call让obj继承完后,需要判断一下构造函数是否有返回值。

例如:

/// 1.如果在mynew中打印res,结果是undefined
function Person(name, age) {
    this.name = name;
    this.age = age;
}

///2.如果在mynew中打印res,结果就是实例对象了。
function Person(name, age) {
    this.name = name;
    this.age = age;
    return this
}

六大继承方式

继承是我们面向对象最重要的性质之一。另A对象通过继承B对象,获取B对象的属性和方法来提高代码复用性。

下面来介绍Javascript的六大继承。为了方便,我们先在这里举个例子,下面的各种继承都以这个例子进行操作,下文代码不会重复写。

//父类型
function Person(name, age) {
    this.name = name,
    this.age = age,
    this.play = [1, 2, 3]
    this.setName = function () { }
}
Person.prototype.setAge = function () { }
//子类型
    function Student(price) {
    this.price = price
    this.setScore = function () { }
}

原型链继承

这是最简单粗暴的继承方式,直接让父类型的实例对象作为子类型的一个原型。

Student.prototype = new Person()

那这样,子类就可以借助于proto访问到父类的实例甚至是原型,实现了属性和方法的继承。

优点:

  • 子类可以访问到父类的全部属性和方法。

缺点:

  • 无法实现多继承(子类只能继承一个父类的属性和方法)
  • 由于是直接调用实例,因此实际上父类的属性和方法是给全部的子类实例共享。

构造函数继承

利用 call 继承,子类在构造函数中用 call 调用父类的构造函数,让子类的 this 继承到父类的构造函数的属性和方法。

function Student(name, age, price) {
    Person.call(this, name, age)  // 相当于: this.Person(name, age)
    this.age = age
    this.price = price
}

优点:

  • 可以实现多继承(call多个父类)
  • 解决了子类实例共享父类属性/方法的问题。
  • 可以传参

缺点:

  • 无法获取父类原型的属性和方法。

原型链 + 构造函数

研究上面两个例子,我们不难发现这两个继承方式是相辅相成的,因此我们对两个种继承方式各持所需。

通过调用父类的构造函数,继承父类的属性方法和传参的特点,在利用原型链继承父类原型。

function Student(name, age, price) {
    Person.call(this,name,age)   
    this.price = price
    this.setScore = function () { }
}
Student.prototype = new Person()  
Student.prototype.constructor = Student  // 构建完整的构造函数指向原型

这里解释一下最后一步的操作:这句话的作用是保存好构造函数和原型间的链条,因此在前面重置了Student的原型,所以constructor也需要指向新的构造函数。

优点:

  • 可以多继承
  • 可以传参
  • 不会出现子类实例共享父类方法属性

缺点:

  • 调用了两次构造函数,一次在new Person,一次在call上。

优化(原型链 + 构造函数)

对于上面的缺点,我们可以进行一个优化,改变子类原型转变成父类实例,变成直接指向父类原型。

function Student(name, age, price) {
    Person.call(this, name, age)
    this.price = price
    this.setScore = function () { }
}
Student.prototype = Person.prototype
Student.prototype.sayHello = function () { }

优点:

  • 解决了两次初始化实例

缺点:

  • 无法区分创建的实例时来自子类还是父类,因为他们原型都一致。

再次优化(原型链 + 构造函数)

借助Object.create() 来创建对象,var B = Object.create(A) 以 A 为原型,生成了B对象。

function Student(name, age, price) {
    Person.call(this, name, age)
    this.price = price
    this.setScore = function () {}
}
Student.prototype = Object.create(Person.prototype) //核心代码
Student.prototype.constructor = Student //核心代码

无暇的方案,子类继承了父类全部的属性和方法,最完美的形态诞生了!

ES6中的class继承

ES6中引入了class,class 可以借助 extends 关键字来继承。

虽然引入了class,但是本质上class是语法糖,实际上还是基于原型实现的。

class Person {
    //调用类的构造方法
    constructor(name, age) {
        this.name = name
        this.age = age
    }
    //定义一般的方法
    showName() {
        console.log("调用父类的方法")
        console.log(this.name, this.age);
    }
}
let p1 = new  Person('kobe', 39)
console.log(p1)
//定义一个子类
class Student extends Person {
    constructor(name, age, salary) {
        super(name, age)//通过super调用父类的构造方法
        this.salary = salary
    }
    showName() {//在子类自身定义方法
        console.log("调用子类的方法")
        console.log(this.name, this.age, this.salary);
    }
}
let s1 = new Student('wade', 38, 1000000000)
console.log(s1)
s1.showName()

ES5继承和ES6继承方式的区别:

ES5 是先创建出子类的this,再把父类的属性方法挂载到子类的this上。

而ES6正好反过来,先把父类的属性和方法挂载到子类this上(如在class必须向调用super) , 再用子类的构造函数去修改this。

总结

最后来个总结吧,在做项目的时候发现是时候该总结一下目前学的内容了,所以在此记录下这篇博客。这篇文章详细介绍了原型/继承,这个知识点算是Javascript的基础内功了,在实际开发过程中也会经常遇到,也是面试中非常常考的知识点,如果觉得不错来个点个吧。
一、翻越前端的三座大山——继承/原型链_第4张图片

参考链接:
https://github.com/mqyqingfeng/Blog/issues/11
https://github.com/mqyqingfeng/Blog/issues/2

你可能感兴趣的:(web前端开发,前端,原型模式,javascript)