美文网首页
原生JS - 原型与原型链

原生JS - 原型与原型链

作者: docman | 来源:发表于2020-02-06 21:44 被阅读0次

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

相关文章

  • 廖雪峰JS小记

    (function(){})() 原型,原型链 浅谈Js原型的理解JS 原型与原型链终极详解 对象 对象:一种无序...

  • Javascript(三)之原型继承理解

    进阶路线 3 原型继承 3.1 优秀文章 最详尽的 JS 原型与原型链终极详解 一 最详尽的 JS 原型与原型链终...

  • 原生JS - 原型与原型链

    JavaScript 常被描述为一种基于原型的语言 (prototype-based language)——每个对...

  • web前端面试之js继承与原型链(码动未来)

    web前端面试之js继承与原型链(码动未来) 3.2.1、JavaScript原型,原型链 ? 有什么特点? 每个...

  • JS原型链

    1什么是JS原型链? 通过__proto__属性将对象与原型对象进行连接. 1.1 JS原型链的作用? 组成的一个...

  • js_继承及原型链等(四)

    js_继承及原型链等(三) 1. 继承 依赖于原型链来完成的继承 发生在对象与对象之间 原型链,如下: ==原型链...

  • JavaScript 原型、原型链与原型继承

    原型,原型链与原型继承 用自己的方式理解原型,原型链和原型继承 javascript——原型与原型链 JavaSc...

  • JS的__proto__和prototype

    最近在回顾JS的原型和原型链的知识,熟悉JS的同学都知道JS的继承是靠原型链实现的,那跟原型链相关的属性__pro...

  • JS闭包问题(一)

    之前我写过一篇JavaScript原型与原型链的文章,此属于JS中的重难点。 而闭包,是JS中除了原型链之外又一个...

  • javascript中的原型链与继承

    javascript中的原型链与继承javascipt中的原型链和继承机制是这门语言所特有的,但js中的原型机制也...

网友评论

      本文标题:原生JS - 原型与原型链

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