之前一直对与原型和原型链这个东西感觉很不好理解,网上的解析都是一张图,然后箭头指向来指向去的,看的实在是有点头晕,所以今天详细了解了一下这个东西,写一篇博客,以供自己查阅。
1、prototype 与 __proto__
首先,任何对象都存在__proto__属性,虽然这个属性是不标准的,真正标准的是[[protopype]]。
var bar = {goo: undefined};
bar.__proto__
{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
函数也是一个特殊对象,所以函数也会存在__proto__,但是,只有函数才会存在 prototype 属性
function foo() {
this.add = function (x, y) {
return x + y;
}
}
foo.__proto__
ƒ () { [native code] }
foo.prototype
{constructor: ƒ}
2、prototype 与 __proto__ 区别
首先,我们存在一个函数,以此来充当构造函数
function foo() {
this.add = function (x, y) {
return x + y;
}
}
这个函数存在的 prototype 属性我们可以详细看一下:
- 一个 constructor 属性:指向当前构造函数 foo()
- 一个 __proto__ 属性:指向的是Object(这是因为原型链上没有其他的原型了,直接就到了Object上)
然后,通过这个构造函数实例一个对象出来看看是什么情况
var f = new foo();
文章开头说过,对象只存在 __proto__ 属性 ,所以直接打印 __proto__ 属性看下
对比一下就会发现,这个构造函数的 prototype 属性 ,和通过这个构造函数实例化的对象的 __proto__ 是一模一样的
foo.prototype === f.__proto__
true
也就是可以看作,我在实例化对象上找东西的时候,就相当于在构造函数的 prototype 属性上找东西。
由此,引出了原型链的概念
3、原型链
MDN上是这么定义的 :当谈到继承时,JavaScript 只有一种结构:对象。每个实例对象( object )都有一个私有属性(称之为 __proto__ )指向它的构造函数的原型对象(prototype )。该原型对象也有一个自己的原型对象( __proto__ ) ,层层向上直到一个对象的原型对象为 null
。根据定义,null
没有原型,并作为这个原型链中的最后一个环节。
几乎所有 JavaScript 中的对象都是位于原型链顶端的 Object
的实例。
这段代码拷贝于汤姆大叔的博客,链接为:https://www.cnblogs.com/TomXu/archive/2012/01/05/2305453.html
function Foo() {
this.value = 42;
}
Foo.prototype = {
method: function() {}
};
function Bar() {}
// 设置Bar的prototype属性为Foo的实例对象
Bar.prototype = new Foo();
Bar.prototype.foo = 'Hello World';
// 修正Bar.prototype.constructor为Bar本身
Bar.prototype.constructor = Bar;
var test = new Bar() // 创建Bar的一个新实例
下面我将按照我的理解,对这段代码进行分解
1、首先,编写一个函数 Foo 作为构造函数,函数内部存在一个属性值 value = 42
2、将函数 Foo的原型 prototype 设置为 一个对象,对象内部存在一个 名叫 method 的方法函数
3、编写一个 函数Bar 作为 第二个构造函数,以此形成原型链
4、将 函数Bar 的 原型prototype 设置为构造函数 Foo 的实例对象,此时,函数 Foo 内部的 value 属性,函数Foo 的 prototype 属性上设置的 对象,都被 Bar.prototype 继承得到。打印 Bar.prototype得到如下值
5、此时,因为,value 属性是直接在函数Foo 内部定义的,所以在实例化的时候,能直接拿到 value 属性,但是 method 方法,是在 函数Foo 的 原型prototype 上的,所以不能直接拿到,这个时候就需要用到 __proto__ 属性了。
6、Bar.prototype 虽然是 函数Bar 的原型属性,但是同时,它也是 构造函数Foo 的实例对象,根据上述第二点概念,构造函数的prototype 属性 === 实例化对象的 __proto__ 。也就是如下情况
Bar.prototype.__proto__ === Foo.prototype
true
7、在 Bar.prototype 上继续新增一个 foo 属性,值为 'Hello World',继续打印 Bar.prototype。可以看到,Bar.prototype 打印之后,直接多了一个 foo 属性
Bar.prototype
{
value: 42
foo: "Hello World"
__proto__: {
method: ƒ ()
__proto__: Object
}
}
8、按照js的原型规则,构造函数原型prototype上的 constructor 属性,应该是指向 构造函数本身的,如果是在 原型prototype 上添加属性的话,是没有问题的。但是 如果是直接给原型 赋值,那么就会导致 该构造函数的原型上失去 constructor 这个属性。所以需要重新赋值。
9、实例化构造函数 Bar,继承 构造函数 Bar 上的属性,以及构造函数Bar 原型prototype 上的属性
所以 实例化的 test对象下的__proto__ === 构造函数Bar的原型prototype ,也就是
test.__proto__ === Bar.prototype
true
10、然后开始了最重要的原型链。
我需要在 test 对象下面找一个 method 函数。但是我们知道,test 对象下是不存在这个函数的,我们代码里面就没有写进去过。
这个时候,js就会开始向上查找了。 test对象没有,那我就去 test.__proto__ 里面去找( 这里就是相当于,在构造函数Bar.prototype 上查找了 )。
test.__proto__ === Bar.prototype
true
因为test.__proto__也是一个对象,那就是也是存在 __proto__ 属性,test.__proto__要是没有,那我就继续往上 在 test.__proto__.__proto__上面查找( 这里的test.__proto__.__proto__ 也就是相当于 Foo.prototype ),只要找不到,那就一直往上找。
test.__proto__.__proto__ === Foo.prototype
true
找到最后,会发现 test.__proto__.....__proto__ 的 constructor 属性 指向了 底层函数 Object () { },所以 Object () {} 也就是最后一个 __proto__ 的 构造函数,也就是 test.__proto__.....__proto__ = Object.prototype。
test.__proto__.__proto__.__proto__ === Object.prototype
true
如果直到找到了 Object.prototype 还是没有找到这个属性的话,会返回一个 undefinde。
Object.prototype 之上也是存在 __proto__ 原型对象的,但是 Object.prototype.__proto__ 会返回 null ,到了这里就是原型链最顶层了。
ps1:如果我的 构造函数Bar 在声明的时候,就已经在内部定义了一个 名叫 method 的方法,但是我在原型上又定义了一个 method 方法。
function Bar() {
this.method = function() {
console.log("111")
}
}
Bar.prototype.method = function () {
console.log("222")
}
var test = new Bar()
这个时候,我再去 实例化对象 test 里面找 method 方法的时候,情况就不一样了。
test.method
ƒ () { console.log("111")}
可以看到,打印的是 构造函数 Bar 内部定义的 method 方法,而不是 构造函数原型 prototype 上新增的 method 方法。
ps2 :在给原型 prototype 赋值的时候,我们可以赋值任何对象,但是不能赋值某个具体的值,例如
function Foo() {}
Foo.prototype = 1; // 无效
4、hasOwnProperty
这是存在于 构造函数原型prototype 上的一个方法,主要用于判断,某个属性是 构造函数内部定义的属性,还是构造函数原型链上的属性 ,因为hasOwnProperty 是 JavaScript 中唯一一个处理属性但是不查找原型链的函数。
// 修改Object.prototype
Object.prototype.bar = 1;
var foo = {goo: undefined};
foo.bar; // 1
'bar' in foo; // true
foo.hasOwnProperty('bar'); // false
foo.hasOwnProperty('goo'); // true
只有 hasOwnProperty 可以给出正确和期望的结果,这在遍历对象的属性时会很有用。 因为 hasOwnProperty 只会在对象本身查找,不会去原型链上查找。
在使用 for in loop 遍历对象时,推荐总是使用 hasOwnProperty 方法,这将会避免原型对象扩展带来的干扰,我们来看一下例子:
// 修改 Object.prototype
Object.prototype.bar = 1;
var foo = {moo: 2};
for(var i in foo) {
console.log(i); // 输出两个属性:bar 和 moo
}
for(var i in foo) {
if (foo.hasOwnProperty(i)) {
console.log(i); // 这次只输出 moo。
}
}
所以,推荐使用 在 for in 遍历对象的时候,使用hasOwnProperty,不要对代码运行的环境做任何假设