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
。
由此我们可以发现:
o
和o1
的__proto__
属性就是Obj.prototype
,__proto__
属性就是我们所说的原型- 我们可以再所有实例化的
Obj
对象中访问到原型的属性,但是无法在某一个属性中访问其他对象独有的动态方法和属性(也叫实例方法和属性)。例:我们无法在o1
中访问到o
中的val
属性。
3. prototype
和 __proto__
的区别
-
prototype
实际上是一个指针,指向构造函数的原型对象 - 对象和函数都有
__proto__
属性,但是只有函数才有prototype
属性(.bind()返回的函数没有prototype
属性)。 - **我们可以使用
__proto__
去访问一个对象的原型对象,即图中的[[prototype]]
**
但是没有官方的方法用于直接访问一个对象的原型对象——原型链中的“连接”被定义在一个内部属性中。在 JavaScript 语言标准中用
[[prototype]]
表示(参见 ECMAScript)。然而,大多数现代浏览器还是提供了一个名为__proto__
(前后各有2个下划线)的属性。(来源MDN - 对象原型)
-
对象中使用
__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
-
函数中使用
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
属性。我们试着从 Obj
、 Obj
原型和 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
总结:
因为
val
是o
的动态属性,所以只有实例化的对象o
可以访问到这个实例属性。由于
Obj
本身只是一个空的构造函数,其本身不具备属性和方法,我们后来增加的foo
属性和say
方法都是添加在Obj.prototype
属性上的。-
通过前面的例子我们可以知道,我们访问到的原型属性都在
__proto__
上,而prototype
只是指向该构造函数的原型对象。因此,当我们访问Obj
上的属性时,如果Obj
自身没有该属性或方法。则会在其原型对象(Obj.__proto__
)中查找这个属性或方法,如果没有,则继续向上(Obj.__proto__.__proto__
)查找。所以:
-
Obj
本身没有val
、foo
、say()
,则在其原型Function
(Obj.__proto__
)中查找,但是也没有找到。于是在其原型Object
(Obj.__proto__.__proto__
)中查找,同样也没有找到三者,因此结果是undefined。 -
Obj.prototype
的指向的是Obj
的原型对象,而在Obj.prototype
中找到了foo
、say()
,因此可以打印出来。同上的原因没有找到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}
仔细观察 person
和 person2
,二者都是 HowOldAreYou
的实例化对象,且从控制台直接打印的结果也能看出来,二者也不属于继承关系。下面的代码也可以简单验证
console.log(person.__proto__ === person2.__proto__); // true
Tips:
通常,我们在使用
typeof
判断数据类型时,Array
和Object
类型的结果都是"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...
)在使用时,遇上null
和 undefined
则会跳过不执行。这也帮助我们理解了,万物皆对象,任何数据类型的原型链的顶端都是 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