JavaScript必须掌握的基础-原型原型链

42次阅读

共计 6854 个字符,预计需要花费 18 分钟才能阅读完成。

原型和原型链的主要作用:

  • 实现属性和方法的公用
  • 继承

所以下面的例子全是以构造函数为例。

原型

函数是也是对象,是一个属性的集合,所以函数下也有属性,也可以自定义属性。当我们创建一个函数时就默认会有一个 prototype 属性,这个属性是一个对象 (属性的集合)。这个东西就是原型 — 通过调用构造函数而创建的那个对象 实例 的原型对象。prototype里也有个属性constructor, 指向的是函数本身。

prototype
function Person() {}
Person.prototype.name='erdong';
var p1=new Person();
var p2=new Person();
console.log(p1.name); // erdong
console.log(p2.name); // erdong

函数 Person 有个 prototype 属性,给这个属性添加一个 name 的属性。p1 和 p2 为这个函数的实例,当访问 p1.name 和 p2.name 时其值都是 prototype下面 name 的值。这个 prototype 对象就是 p1 和 p2 的实例原型,它下面的所以属性和方法 p1 和 p2 都可以获取并使用。

看一下原型对象与构造函数的关系:

那么实例是怎样与原型对象做关联的呢?

__proto__

每个 JavaScript 对象都具有的一个属性 — __proto__ 这个属性指向该对象的原型。不过它是一个隐式属性,并不是所有浏览器都支持它,我们可以把它看做一种实例与实例原型之间的联系桥梁。

function Person() {}
var p1 = new Person();

console.log(p1.__proto__==Person.prototype); // true

上述 p1.__proto__ 与原型对象时相等的,由此可见 p1.__proto__ 指向的是原型对象。

constructor

每个函数都有一个 prototype 属性,而 prototype 下都有一个 constructor 属性,它指向 prototype 所在函数。

function Person() {}
console.log(Person.prototype.constructor==Person) // true

以上就是关于原型几个重要的 ” 属性 ” 已经说完了,下面来讲讲原型连。

实例与原型

JavaScript规定,当读取对象的某个属性或方法时,先从自身查找,如果找不到就去其 __proto__ 指向的原型对象上去找,如果找不到就去原型对象的原型对象上查找,如果再找不到就去原型对象的原型对象上去找 … , 就这样直到找到最上层,至于哪里是最上层,下面会提到。

function Person() {}

var p1 = new Person();

console.log(p1.name);// undefined

p1.show();// Uncaught TypeError: p1.show is not a function

上述例子,p1 为构造函数 Person 的实例,当访问 p1 的 name 属性和 show 方法时,因为 p1 是刚 new 出来的实例,所以并没有找到。看下面的例子:

function Person() {}

Person.prototype.name='erdong';

Person.prototype.show=function () {console.log(this.name);
}

var p1=new Person();

console.log(p1.name);// erdong

p1.show();// erdong

当在 prototype上添加 name 属性和 show 方法后,p1 就可以正确的访问,这就说明 p1 在查找属性 (方法) 时,在自身没有找到 就会去 __proto__ 所指的原型对象上去查找。再来看一个例子:

function Person() {}

Person.prototype.name='erdong';

Person.prototype.show=function () {console.log(this.name);
}

var p1=new Person();

p1.name = 'chen'

console.log(p1.name);// chen

当我们在 p1(对象) 上添加一个属性 name 这个时候再去访问 p1.name 那么输出的就是 “chen” 而不是 “erdong”。这就是一个对象查找属性 (方法) 时的一个规则。

原型的顶层

我们在上述例子查找 p1 的 name 时当查找到 Person.prototype 还未找到时,我们应该还往下查找,下一级是谁呢?因为 Person.prototype是对象,那么他就有一个 __proto__ 属性,指向的是其原型对象 – 也就是其对应构造函数的 prototype。那么 Person.prototype 是谁呢?是Object,因为对象可以通过 new Object() 创建:

var obj = new Object();
// 我们平时都是通过字面量的形式来写:var obj = {}; 其实就相当于 new Object(); 只不过是 javascript 在内部执行了。obj.name = 'erdong';
console.log(obj.name); // erdong

看图:

为什么当查找到 Object.prototype 找不到就输出 undefined 了呢?
因为当在 Object.prototype 也找不到 name 属性,就会去 Object.prototype 指向的原型对象上查找,我们在上面提到,对象与其原型对象是通过 __proto__做关联的,但是 javascript 中规定,Object.prototype.__proto__是不存在的 也就是null

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

这一点要牢记。

原型链

原型链也是 JavaScript 中很重要的一个概念,之所以说是一个概念,是因为它是不存在的,不像一个对象的属性,或者是一个对象的方法一样实例存在。
我的理解就是 – 一个 (实例) 对象的属性或者方法的查找规则。这个规则可以很简单,也可以很复杂。

我们把上面所有的知识总结一下:
每个函数都有一个原型对象(prototype),原型对象又包含一个属性(constructor),指向的是函数本身,函数的实例都有一个隐式原型(__proto__),指向的是构造函数的原型对象(prototype)。

查找规则:当我们访问实例的一个属性时,先从实例自身查找,如果找不到就去其内部指向的原型对象上去查找,如果再找不到,就去其内部指向的原型对象内部指向的原型对象上去查找,就这样一直找到原型的最顶端。

看图:

蓝色的线就表示一条原型链。

改变 prototype

function Person() {}
Person.prototype={
    name: 'erdong',
    sex: '男',
    doSoming: function() {console.log(this.name);
    }
}

var p1=new Person();
p1.doSoming();
console.log(p1.__proto__==Person.prototype); // true
console.log(Person.prototype.constructor==Person);// false

上面的例子,将构造函数的 prototype 属性重写了。虽然 p1 也能找的到 name, 但是 prototype 下的 constructor 属性不再指向 Person 了。实际上指向了Object

console.log(Person.prototype.constructor==Object); //true

是因为我们重写了 Personprototype,此时 Person.prototype 只是一个普通的对象。即:

Person.prototype.constructor = Person.prototype.__proto__.constructor = Object.prototype.constructor = Object

constructor 属性很重要时,我们可以这样做:

function Person() {}
Person.prototype={
    constructor: Person,  // 主动加上 constructor 属性
    name: 'erdong',
    sex: '男',
    doSoming: function() {console.log(this.name);
    }
}
console.log(Person.prototype.constructor==Person);// true

上述代码实例可以适用于当构造函数拥有很多方法或者属性时的写法。

继承

原型链是实现继承的一种方式。这里只是略提一下,下面的文章会详细理解。

当我们不去完全重写函数的 prototype 属性,而是让它等于另一个构造函数的实例时结果会怎样呢?

function SuperType() {}
SuperType.prototype.name = 'erdong';
SuperType.prototype.getName=function() {return this.name;}

function SubType() {}
SubType.prototype=new SuperType();

var instance=new SubType();

console.log(instance.name); // erdong
console.log(instance.getName()); // erdong

上述例子中有两个构造函数 SuperTypeSubTypeSuperType原型上有个 name 属性和 getName 方法。instance是另一个构造函数 SubType 的实例,原本 instanceSuperType是没有关系的。但是现在 instance 可以获取到 name 属性和 getName 方法。原因就是 SubType 重写了 prototype 属性,让它的值等于 SuperType 的实例。所以存在 SuperType.prototype 中的属性和方法,现在也存在与 SubType.prototype 中。

看下面的图:

蓝色的线为 SubType.prototype 改变后的原型 (__proto__ 和 prototype) 的指向 (也是实例查找属性的路线),红色为原来的原型(__proto__和 prototype) 的指向。

JavaScript 高级程序设计一书中解释上述的示例为原型链的基本概念 – 当我们让原型对象等于另一个构造函数的实例,此时的原型对象将包含一个指向另一个原型的指针,相应的,另一个原型中也包含着一个指向另一个构造函数的指针。加入另一个原型又是另一个构造函数的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓 原型链 的基本概念。

关于原型的方法

isPrototypeOf、getPrototypeOf、instanceof、in、hasOwnProperty

isPrototypeOf

用于判断传入的对象内部是否有一个原型对象的指针。

function Person(){}
var p = new Person();
console.log(Person.prototype.isPrototypeOf( p) );//  true

我们上面讲到实例与原型对象是通过 __proto__ 做关联的,__proto__并不是 Javascript 规范,所以我们现实中不能使用它来判断实例与原型对象的关系,这个时候就用isPrototypeOf

getPrototypeOf

ES6 Object新增方法,返回的是传入对象的原型。

function Person(){}
var p = new Person();
console.log(Object.getPrototypeOf(p)===Person.prototype );//  true

上述代码输出的是 true 证明 Object.getPrototypeOf(p) 获取到的就是 p 的原型。

instanceof

判断前者是否是后者的一个实例。

function Person(){}
var p = new Person();

console.log(p instanceof Person); // true
console.log(p instanceof Object); // true

由于 p 既是 Person的实例,同时它也是一个对象,所以也是 Object 的实例。

看下面的示例:

console.log(Function instanceof Object); // true
console.log(Object instanceof Function); // true

Function既是 Object 的实例,Object又是 Function 的实例。是有点绕了,下面会说明这一情况。

in

判断前者是否是后者原型链中的一个属性。

function Person() {}

Person.prototype.name='erdong';

var p=new Person();
p.sex='男';

console.log('sex' in p); // true
console.log('name' in p); // true
console.log('address' in p); // false
hasOwnProperty

检测传入的字符串是否是调用者的自身属性,如果是自身的属性,返回 true,如果是原型中的属性或者不存在,返回 false。

function Person() {}

Person.prototype.name='erdong';

var p=new Person();
p.sex='男';

console.log(p.hasOwnProperty('name'));// false
console.log(p.hasOwnProperty('sex')); // true
console.log(p.hasOwnProperty('address')); // false

与 in 不同的是如果该属性存在于实例上包括原型链上,就返回 true,而 hasOwnProperty 只有是自身的属性,才会返回 true。

思考

我们 (构造) 函数也是对象,上面说过对象下面都会有一个 __proto__ 属性,那么函数的 __proto__ 指向谁呢?

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

函数都是通过 new Function()来创建的,虽然我们平时创建函数并不是通过 new

下面这个函数:

function sum (num1, num2) {return num1 + num2;}

其实在 JavaScript 内部应该是这样实现的:

var sum = new Function("num1", "num2", "return num1 + num2");

所以 Person 对应的构造函数应该是Function

那么新的问题又来了?

Function也一个函数,也是一个对象,那么他同样也有 __proto__ 属性,也有 prototype 属性,它们分别指向什么呢?

console.log(typeof Function); // 'function'

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

看到上面是不是会很奇怪?下面解释一下:

Function是一个函数,它也是通过 new Function 创建的,所以它是被自身创建的,它的 __proto__ 指向的自身的prototype– 也就是Function.prototype

那么 Function.prototype.__proto__ 又指向谁呢?

console.log(Function.prototype.__proto__ === Object.prototype)

很显然,Function.prototype.__proto__是一个对象,所以它指向的是Object.prototype

还有一个问题,Object也是一个构造函数,也是一个对象,那么它应该也有 prototype__proto__属性,我们在上面说到 Object.prototype. __proto__ null,那么Object.__proto__ 指向谁呢?

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

上面 Object.__proto__ 又指向了Function.prototype, 是因为Object 是函数,所以它的原型就是Function.prototype

最后总结一下:

看似关系很复杂,其实一条一条捋清楚就有一种恍然大悟的感觉。

写在最后

如果文中有错误,请务必留言指正,万分感谢。

点个赞哦,让我们共同学习,共同进步。

GitHub

正文完
 0