最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 面试官:谈谈你对原型和原型链的理解

    正文概述 掘金(Tz)   2021-02-08   806

    金三银四的旺季即将拉开序幕,这段时间有朋友陆陆续续去面试,和她们沟通发现,最高频出现问题就是:谈谈你对原型和原型链的理解?

    一个看似最最最基础的面试题,但是这送分题,回答的不好可能就变成了送命题。接下来就让我们一起来探讨探讨~

    基于原型的语言?

    JavaScript 常被描述为一种基于原型的语言 (prototype-based language)——每个实例对象( object )都有一个私有属性(称之为 proto )指向它的构造函数的原型对象(prototype )。该原型对象也有一个自己的原型对象( proto ) ,层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

    几乎所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例。

    在传统的面向对象 中,首先定义“类”,此后创建对象实例时,类中定义的所有属性和方法都被复制到实例中。在 JavaScript 中并是这样,而是在对象实例和它的构造器之间建立一个链接(它是**proto属性,是从构造函数的prototype**属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法。

    注意: 理解对象的原型(可以通过Object.getPrototypeOf(obj)或者已被弃用的__proto__属性获得)与构造函数的prototype属性之间的区别是很重要的。前者是每个实例上都有的属性,后者是构造函数的属性。也就是说,Object.getPrototypeOf(new Foobar())Foobar.prototype指向着同一个对象。


    以上描述很抽象;我们先看一个例子。

    在javascript中,每个函数都有一个特殊的属性叫作原型(**prototype**) ,正如下面所展示的。请注意,下面的代码是独立的一段(在网页中没有其他代码的情况下,这段代码是安全的)。为了最好的学习体验,你最好打开一个控制台 (在Chrome和Firefox中,可以按Ctrl+Shift+I来打开)切换到"控制台" 选项卡, 复制粘贴下面的JavaScript代码,然后按回车来运行

    function coolFn(){}
        console.log( coolFn.prototype );
      	 // 无论你怎么声明函数,函数总是有一个默认值的prototype属性
      	 
        var coolFn = function(){}; 
        console.log( coolFn.prototype );
    

    正如上面所看到的, coolFn 函数有一个默认的原型属性,它在控制台上面呈现了出来. 运行这段代码之后,控制台上面应该出现了像这样的一个对象.

        {
            constructor: ƒ coolFn(),
            __proto__: {
                constructor: ƒ Object(),
                hasOwnProperty: ƒ hasOwnProperty(),
                isPrototypeOf: ƒ isPrototypeOf(),
                propertyIsEnumerable: ƒ propertyIsEnumerable(),
                toLocaleString: ƒ toLocaleString(),
                toString: ƒ toString(),
                valueOf: ƒ valueOf()
            }
        }
    

    现在,我们可以添加一些属性到 coolFn 的原型上面,如下所示.

        function coolFn(){}
        coolFn.prototype.foo = "bar";
        console.log( coolFn.prototype );
    

    结果:

        {
            foo: "bar",
            constructor: ƒ coolFn(),
            __proto__: {
                constructor: ƒ Object(),
                hasOwnProperty: ƒ hasOwnProperty(),
                isPrototypeOf: ƒ isPrototypeOf(),
                propertyIsEnumerable: ƒ propertyIsEnumerable(),
                toLocaleString: ƒ toLocaleString(),
                toString: ƒ toString(),
                valueOf: ƒ valueOf()
            }
        }
    

    然后,我们可以使用 new 运算符来在现在的这个原型基础之上,创建一个 coolFn 的实例。正确使用 new 运算符的方法就是在正常调用函数时,在函数名的前面加上一个 new 前缀. 通过这种方法,在调用函数前加一个 new ,它就会返回一个这个函数的实例化对象. 然后,就可以在这个对象上面添加一些属性:

    	   function coolFn(){}
        coolFn.prototype.foo = "bar"; // 在原型prototype添加属性
        var coolInstancing = new coolFn();
        coolInstancing.prop = "some value"; // 在对象上添加一个属性
        console.log( coolInstancing );
    

    结果:

        {
            prop: "some value",
            __proto__: {
                foo: "bar",
                constructor: ƒ coolFn(),
                __proto__: {
                    constructor: ƒ Object(),
                    hasOwnProperty: ƒ hasOwnProperty(),
                    isPrototypeOf: ƒ isPrototypeOf(),
                    propertyIsEnumerable: ƒ propertyIsEnumerable(),
                    toLocaleString: ƒ toLocaleString(),
                    toString: ƒ toString(),
                    valueOf: ƒ valueOf()
                }
            }
        }
    

      

    就像上面看到的coolInstancing__proto__ 属性就是**coolFn.prototype.** 但是这又有什么用呢?

    好吧,当你访问 coolInstancing 的一个属性, 浏览器首先查找 coolInstancing 是否有这个属性. 如果 coolInstancing 没有这个属性, 然后浏览器就会在 coolInstancing__proto__ 中查找这个属性(也就是 coolFn.prototype). 如果 coolInstancing 的 __proto__ 有这个属性, 那么 coolInstancing 的 __proto__ 上的这个属性就会被使用. 否则, 如果 coolInstancing 的 __proto__ 没有这个属性, 浏览器就会去查找 coolInstancing 的 __proto____proto__ ,看它是否有这个属性.


    默认情况下, 所有函数的原型属性的 __proto__ 就是 window.Object.prototype. 所以 coolInstancing 的 __proto____proto__ (也就是 coolFn.prototype 的 __proto__ (也就是 Object.prototype)) 会被查找是否有这个属性. 如果没有在它里面找到这个属性, 然后就会在 coolInstancing 的 __proto____proto____proto__ 里面查找. 然而这有一个问题: coolInstancing 的 __proto____proto____proto__ 不存在. 最后, 原型链上面的所有的 __proto__ 都被找完了, 浏览器所有已经声明了的 __proto__ 上都不存在这个属性,然后就得出结论,这个属性是 undefined.

    function coolFn(){}
        coolFn.prototype.foo = "bar";
        var coolInstancing = new coolFn();
        coolInstancing.prop = "some value";
        console.log("coolInstancing.prop:      " + coolInstancing.prop);
        console.log("coolInstancing.foo:       " + coolInstancing.foo);
        console.log("coolFn.prop:           " + coolFn.prop);
        console.log("coolFn.foo:            " + coolFn.foo);
        console.log("coolFn.prototype.prop: " + coolFn.prototype.prop);
        console.log("coolFn.prototype.foo:  " + coolFn.prototype.foo);
    

    结果:

        coolInstancing.prop:      some value
        coolInstancing.foo:       bar
        coolFn.prop:           undefined
        coolFn.foo:            undefined
        coolFn.prototype.prop: undefined
        coolFn.prototype.foo:  bar
    

    理解原型对象

    让我们回到 Person() 构造器的例子。请把下面代码例子依次写入浏览器控制台。。

    本例中我们首先将定义一个构造器函数:

    function Person(first, last, age, gender, interests) {
      
     // 属性与方法定义
      
    };
    
    

      

    然后在控制台创建一个对象实例:

    var person1 = new Person('Bob', 'Smith', 32, 'male', ['music', 'skiing']);
    
    

      

    在 JavaScript 控制台输入 "person1.",你会看到,浏览器将根据这个对象的可用的成员名称进行自动补全:

    面试官:谈谈你对原型和原型链的理解

    在这个列表中,你可以看到定义在 person1 的原型对象、即 Person() 构造器中的成员—— nameagegenderinterestsbiogreeting。同时也有一些其他成员—— watchvalueOf 等等——这些成员定义在 Person() 构造器的原型对象、即 Object 之上。下图展示了原型链的运作机制。

    面试官:谈谈你对原型和原型链的理解

    那么,调用 person1 的“实际定义在 Object 上”的方法时,会发生什么?比如:

    person1.valueOf()
    
    

    这个方法仅仅返回了被调用对象的值。在这个例子中发生了如下过程:

    • 浏览器首先检查,person1 对象是否具有可用的 valueOf() 方法。

    • 如果没有,则浏览器检查 person1 对象的原型对象(即 Person构造函数的prototype属性所指向的对象)是否具有可用的 valueof() 方法。

    • 如果也没有,则浏览器检查 Person() 构造函数的prototype属性所指向的对象的原型对象(即 Object构造函数的prototype属性所指向的对象)是否具有可用的 valueOf() 方法。这里有这个方法,于是该方法被调用。

    注意:必须重申,原型链中的方法和属性没有被复制到其他对象——它们被访问需要通过前面所说的“原型链”的方式。

    注意:没有官方的方法用于直接访问一个对象的原型对象——原型链中的“连接”被定义在一个内部属性中,在 JavaScript 语言标准中用 **[[prototype]]** 表示(参见 ECMAScript)。然而,大多数现代浏览器还是提供了一个名为 **__proto__** (前后各有2个下划线)的属性,其包含了对象的原型。你可以尝试输入 **person1.__proto__** **person1.__proto__.__proto__**,看看代码中的原型链是什么样的!

    prototype 属性

    那么,那些继承的属性和方法在哪儿定义呢?如果你查看 Object 参考页,会发现左侧列出许多属性和方法——大大超过我们在 person1 对象中看到的继承成员的数量。某些属性或方法被继承了,而另一些没有——为什么呢?

    原因在于,继承的属性和方法是定义在 prototype 属性之上的(你可以称之为子命名空间 (sub namespace) )——那些以 Object.prototype. 开头的属性,而非仅仅以 **Object**. 开头的属性。prototype 属性的值是一个对象,我们希望被原型链下游的对象继承的属性和方法,都被储存在其中。

    于是 Object.prototype.watch()、``Object.prototype.valueOf() 等等成员,适用于任何继承自 Object() 的对象类型,包括使用构造器创建的新的对象实例。

    Object.is()Object.keys(),以及其他不在 prototype 对象内的成员,不会被“对象实例”或“继承自 Object() 的对象类型”所继承。这些方法/属性仅能被 Object() 构造器自身使用。

    注意:这看起来很奇怪——构造器本身就是函数,你怎么可能在构造器这个函数中定义一个方法呢?其实函数也是一个对象类型**。**

    你可以检查已有的prototype 属性。回到先前的例子,在 JavaScript 控制台输入:

      Person.prototype
    
    
      2. 输出并不多,毕竟我们没有为自定义构造器的原型定义任何成员。缺省状态下,构造器的 `prototype`属性初始为空白。现在尝试:
    
     Object.prototype
    
    

    你会看到 Objectprototype 属性上定义了大量的方法;如前所示,继承自 Object 的对象都可以使用这些方法。

    JavaScript 中到处都是通过原型链继承的例子。比如,你可以尝试从 StringDateNumberArray 全局对象的原型中寻找方法和属性。它们都在原型上定义了一些方法,因此当你创建一个字符串时:

    var myString = 'This is my string.';
    
    

      

    myString 立即具有了一些有用的方法,如 split()indexOf()replace() 等。

    重要:prototype 属性大概是 JavaScript 中最容易混淆的名称之一。你可能会认为,this 关键字指向当前对象的原型对象,其实不是(还记得么?原型对象是一个内部对象,应当使用 __proto__ 访问)。prototype 属性包含(指向)一个对象,你在这个对象中定义需要被继承的成员。

    create()方法

    Object.create() 方法可以创建新的对象实例。

    例如,在上个例子的 JavaScript 控制台中输入:

      ```
      var person2 = Object.create(person1);
      ```
    

    create() 实际做的是从指定原型对象创建一个新的对象。这里以person1 为原型对象创建了person2 对象。在控制台输入:

      ```
      person2.__proto__
      ```
    

    结果返回对象person1

    constructor 属性

    每个实例对象都从原型中继承了一个constructor属性,该属性指向了用于构造此实例对象的构造函数。

    1. 例如,继续在控制台中尝试下面的指令:

      person1.constructor
      person2.constructor
       
      

        

      都将返回 Person() 构造器,因为该构造器包含这些实例的原始定义。

      一个小技巧是,你可以在 constructor 属性的末尾添加一对圆括号(括号中包含所需的参数),从而用这个构造器创建另一个对象实例。毕竟构造器是一个函数,故可以通过圆括号调用;只需在前面添加 new 关键字,便能将此函数作为构造器使用。

      在控制台中输入:

        var person3 = new person1.constructor('Karen', 
                      'Stephenson', 26,
                      'female', 
                      ['playing drums', 'mountain climbing']);
         
      

    ​    ​
    现在尝试访问新建对象的属性,例如: ​

        person3.name.first
        person3.age
        person3.bio()
    

    正常工作。通常你不会去用这种方法创建新的实例;但如果你刚好因为某些原因没有原始构造器的引用,那么这种方法就很有用了。

    此外,constructor 属性还有其他用途。比如,想要获得某个对象实例的构造器的名字,可以这么用:

    instanceName.constructor.name
    
    

    具体地,像这样:

    person1.constructor.name
    
    

    如何修改原型

    从我们从下面这个例子来看一下如何修改构造器的 prototype 属性。

    向构造器的 prototype 添加了一个新的方法:

    function Person(first, last, age, gender, interests) {
    
          // 属性与方法定义
    
        };
    
        var person1 = new Person('Tammi', 'Smith', 32, 'neutral', ['music', 'skiing', 'kickboxing']);
    
        Person.prototype.farewell = function() {
          alert(this.name.first + ' has left the building. Bye for now!');
    }
    
    

      

    但是 farewell() 方法仍然可用于 person1 对象实例——旧有对象实例的可用功能被自动更新了。这证明了先前描述的原型链模型。这种继承模型下,上游对象的方法不会复制到下游的对象实例中;下游对象本身虽然没有定义这些方法,但浏览器会通过上溯原型链、从上游对象中找到它们。这种继承模型提供了一个强大而可扩展的功能系统。

    你很少看到属性定义在 prototype 属性中,因为如此定义不够灵活。比如,你可以添加一个属性:

    Person.prototype.fullName = 'Bob Smith';
    
    

      

    但这不够灵活,因为人们可能不叫这个名字。用 name.firstname.last 组成 fullName 会好很多:

    Person.prototype.fullName = this.name.first + ' ' + this.name.last;
    
    

      

    然而,这么做是无效的,因为本例中 this 引用全局范围,而非函数范围。访问这个属性只会得到 undefined undefined。但这个语句若放在 先前定义在 prototype 上的方法中则有效,因为此时语句位于函数范围内,从而能够成功地转换为对象实例范围。你可能会在 prototype 上定义常属性 (constant property) (指那些你永远无需改变的属性),但一般来说,在构造器内定义属性更好。

    **:关于** **this** 关键字指代(引用)什么范围/哪个对象,这个问题超出了本文讨论范围。事实上,这个问题有点复杂,如果现在你没能理解,也不用担心。

    事实上,一种极其常见的对象定义模式是,在构造器(函数体)中定义属性、在 **prototype** **属性上定义方法。**如此,构造器只包含属性定义,而方法则分装在不同的代码块,代码更具可读性。例如:

    // 构造器及其属性定义
    
    function Test(a,b,c,d) {
          // 属性定义
        };
    
        // 定义第一个方法
    
        Test.prototype.x = function () { ... }
    
        // 定义第二个方法
    
        Test.prototype.y = function () { ... }
    // 等等……
    
    

    参考资料

    • 继承与原型链-MDN

    • 之前看过的一篇外国友人的基于JS原型的理解,由于久远链接遗失

    结尾

    以上就是关于JS原型、原型链的理解,不足之处请大家指出,一起交流,希望对大家有所帮助。

    快要过年了,同学们应该都开始陆陆续续的回家啦,祝大家一路顺风,新的一年暴富、脱单~


    起源地下载网 » 面试官:谈谈你对原型和原型链的理解

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元