原生JS - 原型与原型链

JavaScript 常被描述为一种基于原型的语言 (prototype-based language)——每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法。

准确地说,这些属性和方法定义在Object的构造器函数(constructor functions)之上的prototype属性上,而非对象实例本身。

1. 查看构造函数的原型

function Obj(){}
console.log(Obj.prototype);
/*
* {
*   constructor: ƒ Obj(), // 构造函数
*   __proto__: Object // 原型对象
* }
*/ 

2. 向给构造函数的原型添加属性和方法

在前面声明构造函数 Obj 的时候,我们并没有在里面写入任何东西。现在我想给这个构造函数添加一个属性和一个方法。

Obj.prototype.foo = "bar";
Obj.prototype.say = function(){
    console.log(this.foo);
}
console.log(Obj.prototype);

/*
* {
*   foo: "bar",
*   say: ƒ (),
*   constructor: ƒ Obj(), // 构造函数
*   __proto__: Object // 原型对象
* }
*/ 

我们发现,刚才添加的属性和方法通过原型的方式添加到了构造函数 Obj 中。现在我们通过new实例化一个 Obj 对象出来,同时给这个实例化的对象一个新的属性和方法。

var o = new Obj();
o.val = 123;
o.sayHi = function(){console.log("Hi")};
console.log(o);

/*
* {
*   val: 123,
*   sayHi: ƒ (),
*   __proto__: {
*       foo: "bar",
*       say: ƒ (),
*       constructor: ƒ Obj(), // 构造函数
*       __proto__: Object // 原型对象
*   }
* }
*/ 

我们发现, o 有一个 val 属性;而在 o 的原型上,有属性 foo 和方法 say

我们再实例化一个 Obj 对象看看,但是这次,不给他增加 val 属性了。

var o1 = new Obj();
console.log(o1);

/*
* {
*   __proto__: {
*       foo: "bar",
*       say: ƒ (),
*       constructor: ƒ Obj(), // 构造函数
*       __proto__: Object // 原型对象
*   }
* }
*/ 

我们发现,o1 中没有 val 属性;但是在 o1 的原型上,仍然有属性 foo 和方法 say

由此我们可以发现:

  • oo1__proto__ 属性就是 Obj.prototype__proto__ 属性就是我们所说的原型
  • 我们可以再所有实例化的 Obj 对象中访问到原型的属性,但是无法在某一个属性中访问其他对象独有的动态方法和属性(也叫实例方法和属性)。例:我们无法在 o1 中访问到 o 中的 val 属性。

3. prototype__proto__ 的区别

  1. prototype 实际上是一个指针,指向构造函数的原型对象
  2. 对象和函数都有 __proto__ 属性,但是只有函数才有 prototype 属性(.bind()返回的函数没有 prototype 属性)。
  3. **我们可以使用 __proto__ 去访问一个对象的原型对象,即图中的 [[prototype]] **

但是没有官方的方法用于直接访问一个对象的原型对象——原型链中的“连接”被定义在一个内部属性中。在 JavaScript 语言标准中用 [[prototype]] 表示(参见 ECMAScript)。然而,大多数现代浏览器还是提供了一个名为 __proto__ (前后各有2个下划线)的属性。(来源MDN - 对象原型)

  1. 对象中使用 __proto__ 给原型对象添加属性和方法(null除外)

    function NewObj(){}
    var a = new NewObj();
    a.__proto__.aaa = "aaa";
    
    console.log(a);
    /*
    NewObj = {
        __proto__:{
            aaa: "aaa"
            constructor: ƒ NewObj()
            __proto__: Object
        }
    }
    */
    
    console.log(NewObj.prototype); 
    /*
    {
        aaa: "aaa",
        constructor: ƒ NewObj(),
        __proto__: Object
    }
    */ 
    
    console.log(a.__proto__ === NewObj.prototype); // true
    

    因为 a 的原型对象是 NewObj ,因此给 a 的原型添加属性,就是给 NewObj 添加属性,和直接使用 NewObj.prototype 添加属性一样(通过下面代码验证)。

    console.log(a.__proto__ === NewObj.prototype); // true
    
  1. 函数中使用 prototype__proto__ 都可以给函数或者函数的原型对象添加属性。

    我们上面已经通过使用 prototype 给构造函数增加属性和方法,这里只演示使用 __proto__ 的情况

    NewObj.__proto__.bbb = "bbb";
    
    console.dir(NewObj);
    console.dir(Obj);
    

    通过打印的结果,我们发现不仅仅 NewObj 中有了 bbb 这个属性, Obj 中也有了 bbb 这个属性。

    哪怕我们是先创建的 Obj ,再创建的 NewObj ,继而添加的 NewObj.__proto__.bbb 属性,也是如此。

    我们通过下面的验证可以知道,两个构造函数的原型都是 Function ,也就是说它们都是 Function 的实例。那么当我们给其中任意一个函数的 __proto__ 加属性或方法的时候,其他的函数( Function 的实例)都可以在 __proto__ 中找到新增的属性或方法。

    console.log(NewObj.__proto__ === Function.prototype); // true
    console.log(Obj.__proto__ === Function.prototype); // true
    

4. 实例化的对象没有原型上已有的属性,为什么还能访问到?

目前,我们已知 Obj 属性有一个 foo 属性和一个 say 方法,实例化的对象 o 只有一个 val 属性。我们试着从 ObjObj 原型和 o 三个角度去访问一下这两个属性和一个方法。

// Obj
console.log("Obj.val:" + Obj.val); // Obj.val:undefined
console.log("Obj.foo:" + Obj.foo); // Obj.foo:undefined
console.log("Obj.say:" + Obj.say); // Obj.say:undefined
Obj.say(); // TypeError: Obj.say is not a function

// Obj.prototype
console.log("Obj.prototype.val:" + Obj.prototype.val); // Obj.prototype.val:undefined
console.log("Obj.prototype.foo:" + Obj.prototype.foo); // Obj.prototype.foo:bar
console.log("Obj.prototype.say:" + Obj.prototype.say); // Obj.prototype.say:function(){ console.log(this.foo); }
Obj.prototype.say(); // bar

// o
console.log("o.val:" + o.val); // o.val:123
console.log("o.foo:" + o.foo); // o.foo:bar
console.log("o.say:" + o.say); // o.say:function(){ console.log(this.foo); }
o.say(); // bar

总结:

  1. 因为 valo 的动态属性,所以只有实例化的对象 o 可以访问到这个实例属性。

  2. 由于 Obj 本身只是一个空的构造函数,其本身不具备属性和方法,我们后来增加的 foo 属性和 say 方法都是添加在 Obj.prototype 属性上的。

  3. 通过前面的例子我们可以知道,我们访问到的原型属性都在 __proto__ 上,而 prototype 只是指向该构造函数的原型对象。因此,当我们访问 Obj 上的属性时,如果 Obj 自身没有该属性或方法。则会在其原型对象( Obj.__proto__ )中查找这个属性或方法,如果没有,则继续向上( Obj.__proto__.__proto__ )查找。

    所以:

    • Obj 本身没有 valfoosay(),则在其原型 FunctionObj.__proto__ )中查找,但是也没有找到。于是在其原型 ObjectObj.__proto__.__proto__ )中查找,同样也没有找到三者,因此结果是undefined。
    • Obj.prototype 的指向的是 Obj 的原型对象,而在 Obj.prototype 中找到了 foosay() ,因此可以打印出来。同上的原因没有找到 val ,因此无法打印。
    • 同样的方法也可以知道 o 为什么三个都可以打印出来。

5. 如果实例化的对象和原型有同名属性或方法...

  • 给实例化的对象 o 新增一个 foo 属性,而原型对象上的 foo 仍然存在。

    o.foo = "hello";
    console.log(o.foo); // hello
    console.log(o.__proto__.foo); // bar
    
  • 删除实例化对象上的属性

    删除后, o.foo 打印的是原型链上的 foo 属性。

    delete o.foo;
    console.log(o.foo); // bar
    console.log(o.__proto__.foo); // bar
    
  • 删除对象原型上的属性

    删除后,由于原型链上也没有 foo 这个属性了,所以是 undefined

    delete o.__proto__.foo;
    // 或执行  delete Obj.prototype.foo;
    console.log(o.foo); // undefined
    console.log(o.__proto__.foo); // undefined
    

6. 使用 create() 创建对象

我们知道可以使用 Object.create() 创建对象,传入的参数是一个对象。

例如:

var o2 = Object.create(o);
console.log(o2.__proto__ === o); // true
console.log(o2.__proto__.__proto__ === Obj.prototype); // true

显然, o2 的原型对象是 o (继承)。

7. 使用 constructor 创建对象

除了直接 new 构造函数,我们还可以通过 new 实例化对象的 constructor 来创建一个新的实例化对象。为了区分,我们新建一个构造函数,然后通过实例化传入参数,来看看效果。

function HowOldAreYou(age){
    this.age = age;
}
var person = new HowOldAreYou(18);
console.log(person); // HowOldAreYou {age: 18}

现在使用实例化对象的 constructor 来创建:

var person2 = new person.constructor(25);
console.log(person2); // HowOldAreYou {age: 25}

仔细观察 personperson2 ,二者都是 HowOldAreYou 的实例化对象,且从控制台直接打印的结果也能看出来,二者也不属于继承关系。下面的代码也可以简单验证

console.log(person.__proto__ === person2.__proto__); // true

Tips:

通常,我们在使用 typeof 判断数据类型时, ArrayObject 类型的结果都是 "object" 。那么,我们现在也可以使用 constructor 的方式对二者进行区分。

console.log([].constructor === Array); // true
console.log({}.constructor === Object); // true

8. 关于 null

我在翻阅一些资料的时候,发现有很多地方在讲解原型的时候,都会单独标注 null除外 。这是为什么?

当我们尝试打印 null 的类型

typeof null; // object

结果似乎不是我们想的那样,这也让很多人都将 null 当做一个 JavaScript 对象。而事实是,这应该算是JavaScript 语言本身的一个 bug。 这是因为

编程语言最后的形式都是二进制,所以 JavaScript 中的对象在底层肯定也是以二进制表示的。在JavaScript的底层中,如果前三位都是零的情况,就会被判定为对象。而底层中 null 的二进制表示都是零。所以在对 null 的类型判定时,会把 null 判定为 object。

null 本身没有任何属性和方法,有很多方法(例如 for...in... )在使用时,遇上nullundefined 则会跳过不执行。这也帮助我们理解了,万物皆对象,任何数据类型的原型链的顶端都是 Object 。同时,有下面这张图,应该也可以更好的理解这种中间的关系了。

参考资料:

对象原型 - MDN: https://developer.mozilla.org/zh-CN/docs/Learn/JavaScript/Objects/Object_prototypes

一篇文章看懂proto和prototype的关系及区别: https://www.jianshu.com/p/7d58f8f45557

js的原型和原型链:https://www.jianshu.com/p/be7c95714586

JavaScript 中的 null 是一个对象吗: https://www.jianshu.com/p/f2c5aa0fb5f0

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容