美文网首页
JavaScript 函数原型链解析

JavaScript 函数原型链解析

作者: 李牙刷儿 | 来源:发表于2017-12-09 19:34 被阅读10次

    JavaScript中,函数原型链是最强大也是最容易让人迷惑的特性。长期以来对于prototype__proto__的一知半解导致在实际开发中经常遇到难以排查的问题,所以有必要将JavaScript中的原型概念理解清楚。

    1. __proto__ vs prototype

    1.1 __proto__

    JavaScript中所有对象都拥有一个__proto__用来表示其原型继承,所谓的原型链也就是根据__proto__一层层向上追溯。JavaScript中有一个内置属性[[prototype]](注意不是prototype)来表征其原型对象,大多数浏览器支持通过__proto__来对齐进行访问。一个普通对象的__proto__Object.prototype:

    var a = {
        'h' : 1
    }
    
    // output: true
    a.__proto__ === Object.prototype
    

    1.2 prototype

    prototype是只有函数才有的属性。

    当创建函数时,JavaScript会自动给函数创建一个prototype属性,并指向原型对象functionname.prototype

    JavaScript可以通过prototype__proto__在两个对象之间建立一个原型关系,实现方法和属性的共享,从而实现继承。

    1.3 构造函数创建对象实例

    JavaScript中的函数对象有两个不同的内部方法:[[Call]]Construct

    如果不通过new关键字来调用函数(比如call,apply等),则执行[[Call]]方法,该种方式只是单纯地执行函数体,并不创建函数对象。

    如果通过new关键字来调用函数,执行的是[[Constrcut]]方法,该方法会创建一个实例对象,同时将该对象的__proto__属性执行构造函数的prototype也即functionname.prototype,从而继承该构造函数下的所有实例和方法。

    有了以上概念后,来看一个例子:

    function Foo(firstName, lastName){
        this.firstName = firstName;
        this.lastName = lastName; 
    }
    Foo.prototype.logName = function(){
        Foo.combineName();
        console.log(this.fullName);
    }
    Foo.prototype.combineName = function(){
        this.fullName = `${this.firstName} ${this.lastName}`
    }
    
    var foo = new Foo('Sanfeng', 'Zhang');
    foo.combineName();
    console.log(foo.fullName); // Sanfeng Zhang
    foo.logName(); // Uncaught TypeError: Foo.combineName is not a function
    

    明明声明了Foo.prototype.logName,但是Foo.combineName却出错,其原因在于原型链理解出错。

    首先来看下foo的原型链:

    var foo = new Foo('Sanfeng', 'Zhang'):

    通过new创建一个函数对象,此时JavaScript会给创建出来对象的__proto__赋值为functionname.protoye也即Foo.prototype,所以foo.combineName可以正常访问combineName。其完整原型链为:

    foo.__proto__ === Foo.prototype
    foo.__proto__.__proto__ === Foo.prototype.__proto__ === Object.prototype
    foo.__proto__.__proto__.__proto__ === Foo.prototype.__proto__.__proto__ === Object.prototype.__proto__ === null
    

    [图片上传失败...(image-21e9dd-1513432632315)]

    接下来看下Foo的原型链:

    直接通过Foo.combineName调用时,JavaScript会从Foo.__proto__找起,而Foo.__proto__指向Function.prototype,所以根本无法找到挂载在Foo.prototype上的combineName方法。

    其完整原型链为:

    Foo.__proto__ = Function.prototype;
    Foo.__proto__.__proto__ = Function.prototype.__proto__;
    Foo.__proto__.__proto__.__proto__ = Function.prototype.__proto__.__proto__ = Object.prototype.__proto__ = null;
    

    [图片上传失败...(image-8e415a-1513432632315)]

    接下来做一下变形:

    function Foo(firstName, lastName){
        this.firstName = firstName;
        this.lastName = lastName; 
    }
    
    Foo.__proto__.combineName = function() {
        console.log('combine name');
    }
    
    Foo.combineName(); // combine name
    Funciton.combineName(); // combine name
    var foo = new Foo('Sanfeng', 'Zhang');
    foo.combineName(); // foo.combineName is not a function
    

    这次是在Foo.__proto__上注册的combineName,所以实例对象foo无法访问到,但是Function Foo可以访问到,另外我们看到因为Foo.__proto__指向Function.prototype,所以可以直接通过Function.combineName访问。

    2 原型继承

    理解清楚了__proto__prototype的联系和区别后,我们来看下如何利用两者实现原型继承。首先来看一个例子:

    function Student(props) {
        this.name = props.name || 'unamed';
    }
    
    Student.prototype.hello = function () {
        console.log('Hello ' + this.name);
    }
    
    var xiaoming = new Student({name: 'xiaoming'}); // Hello xiaoming
    
    

    这个很好理解:

    xiaoming -> Student.prototype -> Object.prototype -> null
    

    接下来,我们来创建一个PrimaryStudent:

    function PrimaryStudent(props) {
       Student.call(this, props);
       this.grade = props.grade || 1;
    }
    

    其中Student.call(this, props);仅仅执行Student方法,不创建对象,参考1.3节中的[[Call]]

    此时的原型链为:

    new PrimaryStudent() -> PrimaryStudent.prototype -> Object.prototype -> null
    

    可以看到,目前PrimaryStudentStudent并没有任何关联,仅仅是借助Student.call(this, props);声明了name属性。

    要想继承Student必须要实现如下的原型链:

    new PrimaryStudent() -> PrimaryStudent.prototype -> Student.prototype -> Object.prototype -> null
    

    当然可以直接进行如下赋值:

    PrimaryStudent.prototype = Student.prototype
    

    但这样其实没有任何意义,如此一来,所以在PrimaryStudent上挂载的方法都是直接挂载到Student的原型上了,PrimaryStudent就显得可有可无了。

    那如何才能将方法挂载到PrimaryStudent而不是Student上呢?其实很简单,在PrimaryStudentStudent之间插入一个新的对象作为两者之间的桥梁:

    function F() {}
    F.prototype = Student.prototype;
    PrimaryStudent.prototype = new F();
    PrimaryStudent.prototype.constructor = PrimaryStudent;
    
    // 此时就相当于在new F()对象上添加方法
    PrimaryStudent.prototype.getGrade = function() {
        
    }
    

    如此一来就实现了PrimaryStudent与Student的继承:

    new PrimaryStudent() -> new PrimaryStudent().__proto__ -> PrimaryStudent.prototype -> new F() -> new F().__proto__ -> F.prototype -> Student.prototype -> Object.prototype -> null
    

    3 关键字new

    实际开发中,我们总是通过一个new来创建对象。那么为什么new可以创建一个我们需要的对象?其与普通的函数执行有什么不同呢?
    来看下下面这段代码:

    function fun() {
        console.log('fun');
    }
    fun();
    var f = new fun();
    

    其对应的输出都是一样的:

    fun
    fun
    

    但实际上,两者有着本质的区别,前者是普通的函数执行,也即在当前活跃对象执行环境内直接执行函数fun。
    new fun()的实质却是创建了一个fun对象,其含义等同于下文代码:

    function new(constructor) {
     var obj = {}
     Object.setPrototypeOf(obj, constructor.prototype);
     return constructor.apply(obj, [...arguments].slice(1)) || obj
    } 
    

    可以看到,当我们执行new fun()时,实际执行了如下操作:

    • 创建了一个新的对象。
    • 新对象的原型继承自构造函数的原型。
    • 以新对象的 this 执行构造函数。
    • 返回新的对象。如果构造函数返回了一个对象,那么这个对象会取代整个 new 出来的结果

    从中也可以看到,其实new关键字也利用了原型继承来实现对象创建。

    相关文章

      网友评论

          本文标题:JavaScript 函数原型链解析

          本文链接:https://www.haomeiwen.com/subject/rfepixtx.html