V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
4ark
V2EX  ›  前端开发

理解 JavaScript 中的原型

  •  
  •   4ark ·
    gd4ark · 2019-01-23 12:52:04 +08:00 · 2684 次点击
    这是一个创建于 2135 天前的主题,其中的信息可能已经有所发展或是发生改变。

    前言

    JavaScript 中的原型一直是我很惧怕的一个主题,理由很简单,因为真的不好理解,但它确实是 JavaScript 中很重要的一部分,而且是面试的必考题,就算现在不懂,以后迟早有一天要把它弄懂,不然的话永远都没办法把自己的技术能力往上提高一个层次,所以今天就来讲讲 JavaScript 中的原型。

    本文是这系列的第四篇,往期文章:

    1. 理解 JavaScript 中的作用域
    2. 理解 JavaScript 中的闭包
    3. 理解 JavaScript 中的 this

    什么是原型

    首先要说一下为什么会有原型这个东西,那是因为在 JavaScript 中并没有 “类” 的概念,它是靠原型和原型链实现对象属性的继承,即便在 ES6 中新出了class的语法,但那也只是一个语法糖,它的底层依然是原型。

    要理解原型(原型链),最重要的是理解两个属性以及它们之间的关系:

    • __proto__
    • prototype

    __proto__

    JavaScript 中,万物皆对象,所有的对象都有__proto__属性(nullundefined除外),而且指向创造这个对象的函数对象的prototype属性。

    var obj = {};
    console.log( obj.__proto__ === Object.prototype ); // true
    var arr = [];
    console.log( arr.__proto__ === Array.prototype ); // true
    var fn = function(){};
    console.log( fn.__proto__ === Function.prototype ); // true
    var str = "";
    console.log( str.__proto__ === String.prototype ); // true
    var num = 1;
    console.log( num.__proto__ === Number.prototype ); // true
    

    前面说了,在 JavaScript 中,一切皆对象(可以理解为它们都是从对象那里继承过来的),所以:

    console.log( Function.prototype.__proto__ === Object.prototype ); // true
    console.log( Array.prototype.__proto__ === Object.prototype ); // true
    console.log( String.prototype.__proto__ === Object.prototype ); // true
    

    而因为Object.prototype__proto__已经是终点了,所以它的指向是:

    console.log( Object.prototype.__proto__ === null ); // true
    

    注意,虽然大多数浏览器都支持通过__proto__来访问,但它并不是ECMAScript的标准,在 ES5 中可以通过Object.getPrototypeOf()来获取这个属性。

    var obj = {};
    console.log( Object.getPrototypeOf(obj) === Object.prototype ); // true
    

    prototype

    prototype是每个函数对象都具有的属性(它也有__proto__,因为函数也是对象),实例化创建出来的对象会共享此prototype里的属性和方法(通过__proto__)。

    在上面的例子中已经看到过prototype的身影,下面通过一个例子来讲述它的作用。

    现在我们有一个构造函数Person,并且对它进行实例化:

    function Person(name){
        this.name = name;
        this.sayName = function(){
            console.log("我的名字是:" + this.name);
        }
    }
    
    var a = new Person("小明");
    var b = new Person("小红");
    
    a.sayName(); // 我的名字是:小明
    b.sayName(); // 我的名字是:小红
    

    new 运算符的缺点

    但是,用构造函数生成实例对象,有一个缺点,那就是无法共享属性和方法。

    例如上面例子中的ab,它们都有sayName方法,虽然做的事相同,但它们却是独立的,这就会造成极大的资源浪费,因为每一个实例对象,都有自己的属性和方法的副本。

    prototype 属性的引入

    考虑到这一点,Brendan Eich 决定为构造函数设置一个prototype属性。

    这个属性包含一个对象,所有实例对象需要共享的属性和方法,都放在这个对象里面,而不需要共享属性和方法,就放在构造函数里面,这个对象就是prototype对象。

    实例对象一旦创建,将自动引用prototype对象的属性和方法。也就是说,实例对象的属性和方法,分成两种,一种是本地的,另一种是引用的。

    现在对上面的例子进行改写:

    function Person(name){
        this.name = name;
    }
    Person.prototype = {
        sayName : function(){
            console.log("我的名字是:" + this.name);
        }
    }
    
    var a = new Person("小明");
    var b = new Person("小红");
    a.sayName() // 我的名字是:小明
    b.sayName() // 我的名字是:小红
    

    现在无论Person被实例化多少次,它的实例对象都共享同一个sayName方法,这就是prototype最大的用处。

    原型链

    讲原型一个不可避免的概念就是原型链,原型链是通过__proto__来实现的。

    现在我们以Person的例子来讲整个原型链。

    var a = new Person("小明");
    
    // 实例化对象的 __proto__ 指针指向构造函数的原型
    console.log( a.__proto__ === Person.prototype )
    // 构造函数的原型是一个对象,它的 __proto__ 指向对象的原型
    console.log( Person.prototype.__proto__ === Object.prototype )
    // 函数也是一个对象,它的 __proto__ 指向 函数的原型
    console.log( Person.__proto__ === Function.prototype )
    // 函数的原型是一个对象,它的 __proto__ 指向对象的原型
    console.log( Function.prototype.__proto__ === Object.prototype )
    // 对象的原型的__proto__ 指向 null
    console.log( Object.prototype.__proto__ === null )
    

    以上就是a对象的整个原型链。

    属性查找机制

    当访问一个对象的属性时,Javascript 会从对象本身开始往上遍历整个原型链,直到找到对应属性为止。如果此时到达了原型链的顶部,也就是上例中的 Object.prototype,仍然未发现需要查找的属性,那么 Javascript 就会返回 undefined值。

    注:此文为原创文章,如需转载,请注明出处。

    6 条回复    2019-01-23 20:42:59 +08:00
    MinonHeart
        1
    MinonHeart  
       2019-01-23 13:35:38 +08:00 via iPhone
    最近正好整理了一下原型链相关的知识,仅供参考

    https://github.com/Aqours/Aqours/issues/9

    linxiaoziruo
        2
    linxiaoziruo  
       2019-01-23 15:31:54 +08:00   ❤️ 1
    TomatoYuyuko
        3
    TomatoYuyuko  
       2019-01-23 16:08:41 +08:00
    dalao 写的非常简单易懂了,提个小建议,this 那一章隐式丢失感觉可以详细讲讲?那个 setTimeout 有点摸不着头脑
    4ark
        4
    4ark  
    OP
       2019-01-23 17:49:47 +08:00
    @TomatoYuyuko 感谢你的建议,有空我把之前写的重新整理一遍
    yazoox
        5
    yazoox  
       2019-01-23 20:24:03 +08:00 via Android
    厉害👍
    rabbbit
        6
    rabbbit  
       2019-01-23 20:42:59 +08:00
    this 指向的个人理解
    有"点"指向"点"前,没有指向 windows.bind apply call 指向一参数,箭头函数没 this 往上找

    foo = {bar: function(){console.log(this)}}
    setTimeout(foo.bar, 100)
    看起来传进去的是 foo.bar
    其实传进去的是
    setTimeout(function(){console.log(this)}, 100)

    改成返回个箭头函数,就指向 foo 了
    foo = {bar: function(){
    return _ => console.log(this)
    }}
    setTimeout(foo.bar(), 100) // {bar: ƒ}
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5925 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 47ms · UTC 06:09 · PVG 14:09 · LAX 22:09 · JFK 01:09
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.