[JS]带你通俗掌握原型链

333 阅读5分钟

最近想重新系统地整理一下前端的知识,因此写了这个专栏。我会尽量写一些业务相关的小技巧和前端知识中的重点内容,核心思想。

前言

要回顾JS知识,怎么能不提到原型链。原型链是面向对象的基础,这套的机制使得js的变量可以在不同实例之间继承访问,大大优化了开发设计的手段。

构造函数

所谓的构造函数,看起来与普通函数其实区别不大,当我们用new 的方式执行普通函数时,他就是构造函数了。同一个函数用普通方式执行和用new 执行,结果会不一样。

当我们执行构造函数的时候,也就是new一个函数的时候发生了什么?这也是一道很常见的面试题,下面我们就来看看这个过程:

  1. 先创建函数执行上下文
  2. 创建内部作用域(变量环境,词法环境)
  3. 创建当前类实例对象O
  4. 记录作用域链
  5. 初始化this 指向实例对象O
  6. 初始化入参arg
  7. 形参赋值
  8. 变量提升
  9. 代码执行 如果有this 的会修改实例对象
  10. 重点:如果没手动return 或者return 原始数据类型 会默认返回实例对象。

由此我们旧可以解释上面的代码,因为fun返回的是原始数据类型(字符串)所以,构造函数返回的是实例对象。如果把代码修改成下面的:

function fun(name){
    return {
        name
    };
}
console.log(fun('john'));
// john
console.log(new fun('john'));
// john

由于返回的是引用类型,所有构造函数和普通函数执行的效果是一样的。

实例

实例是由构造函数通过new方式执行之后返回的一个对象内容。根据上面的内容我们知道在执行new的时候,可以通过this给实例设置一些属性,这些属于实例的属性就是【私有属性】。

像上述代码中tom就是由Student构造函数生成的【实体】,实体中有自己的私有属性name和age。借助这种模式我们在编码时就可以利用,一个构造函数统一生产多个同类型的实例,这种思想就是面向对象的核心思想,由此也衍生出多种如简单工厂模式,抽象工厂模式等设计模式。而在这里的构造函数也可以理解为是一个【类】。

原型 prototype

我们以及了解过了构造函数和实例,接下来我们来看看【原型】。构造函数有一个特有的属性【prototype】,指向一个xxx.prototype。这个prototype就是原型。

constructor

constructor是原型的属性之一,指向这个原型的构造函数。

proto

任何实例或者原型都会有【proto】指向自己的上级原型。如果顺着__prop__一直往上找,就会发现Object是所有原型的始祖,而他的__proto__是null。

原型链

现在我们已经知道了构造函数,实例,原型,我们再回头来看看什么是原型链。我们先来看看下面的代码

这里tom是由我们的Student类生成的实例,在构造函数内我们并没有给this设置toString方法。但图中代码执行却能正常执行。既然私有属性中没有这个属性,那么这个toString的方法是从哪里来的呢?

原来当我们的实例访问属性内容的时候,会优先访问实例的私有属性,如果找不到的话,就会顺着__proto__往上找到上层原型,如果原型上有这个属性就会作为被访问的内容被返回。如果找不到就会一直往上找,找到Object原型(原型链的尽头)都找不到的话就会认为是undefined。上述的代码中最终的toString实际上访问的是Object原型的方法。

上图中这种基于__proto__ & prototype查找的机制,就是【原型链】 查找机制。

思考1:所有函数都能作为构造函数吗?

一般来说函数都能作构造函数,所以都会有prototype。相反但有些特殊例子是没有prototype 的,因此它们不能用作构造函数。有以下例子:

  1. 箭头函数
  2. class中的方法
  3. 对象中的便捷定义函数
const fn = () => {};

let obj = {
    fn1() {}, 
    fn2: function () {}  // 可以正常new
};

class Fn {
    sum() {

    }
}

new fn 
// Uncaught TypeError: fn is not a constructor
new obj.fn1()
// Uncaught TypeError: obj.fn1 is not a constructor
var ins = new Fn();
new ins.sum()
Uncaught TypeError: ins.sum is not a constructor

思考2:构造函数有__proto__ 吗?

这是一个很容易被忽略的细节。在JavaScript中"一切皆对象",无论所有的引用类型实例最后都会顺着原型链链找到Object.prototype。那么作为构造函数,本身有__proto__吗?

答案是有的,而且这个__proto__指向的其实就是内置构造函数Function的prototype。

Student.__proto__ === Function.prototype
// true

而构造函数Function的__proto__指向的就是Object.prototype原型

Function.prototype.__proto__ === Object.prototype
// true

总结

今天我们了解的原型链,希望大家都已经掌握了。所谓的原型链其实就是在实例属性访问时顺__proto__和prototype往上找属性的一套机制。难点在于大家要理清楚constructor,proto,prototype等这些概念。下一篇文章,我们就会讲述基于原型链的【继承】机制以及实现方式。