对constructor和prototype的理解

在学习js面向对象过程中,我们总是对constructorprototype充满疑惑,这两个概念是相当重要的,深入理解这两个概念对掌握js的一些核心概念非常的重要。因此,在这里记录下鄙人见解,希望可以给读者带来一些帮助。如若有错,请大佬们不吝指正,十分感谢!

prototype

据高程三:无论什么时候,只要创建一个函数,就会根据特定规则为该函数创建prototype属性,这个属性指向函数的原型对象。

换言之,每个函数都有一个默认的prototype属性。然后这个属性指向函数的原型对象。如果这个函数被用在创建自定义对象的场景中,我们称这个函数为构造函数。构造函数中原型对象中的属性和方法可以被使用该构造函数创建出来的实例对象使用。即以通过构造函数创建出来的所有实例对象,自动拥有和共享该构造函数的原型对象中的所有属性和方法。利用这点,我们看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
//定义构造函数Animal
function Animal(name) {
this.name = name;
}
//设置构造函数Animal原型对象上的方法
Animal.prototype.shout =function (){
console.log('wow wow');
}
//通过构造函数Animal创建实例对象a1,该过程称为实例化
var a1 = new Animal('小白');
a1.shout(); // 'wow wow'

那实例对象为什么可以继承创建该实例的构造函数的原型对象上的属性/方法呢?我们再引入一个概念。

当用构造函数创建实例对象的时候,该实例对象的内部也将包含一个指针(内部属性),指向构造函数的原型对象。ECMA-262第5版中管这个指针叫[[Prototype]]。我们把它称为对象的原型。

虽然在脚本中没有标准的方式访问它。但Firefox Safari Chrome在每个对象上都支持一个属性__proto__;后来ECMAScript 5 增加了一个新方法,叫Object.getPrototypeOf(),在所有支持的实现中,这个方法返回[[Prototype]]的值。其实__proto__和这个方法的作用是一样的(区别一个是部分浏览器厂商实现的非标准,一个是后来推出的标准),都是获取对象的原型(文章后面一律用这个__proto__来表示对象的原型)

一定要区分好原型和原型对象,虽然指向的是同一个对象,但是访问者不一样,叫法也不一样。鄙人总结了一下:

构造函数.prototype(构造函数的原型对象)

prototype属性:只有函数才有的,它跟原型链没有关系。它的作用是构造函数new对象时,告诉实例对象的原型是谁。

实例对象.__proto__(实例对象的原型)

__proto__属性:是所有对象(包括函数)都有的,它才叫做对象的原型,原型链就是靠它形成的。(__proto__不是ECMA标准,仅供开发者调试使用,不要用于正式开发)

构造函数.prototype === 实例对象.__proto__

我们可以画一个图帮助理解

img

说回刚才的问题,为什么实例对象可以继承原型创建该实例的构造函数的原型对象上的属性/方法

以上面的代码来说。构造函数new形式创建实例对象的过程实际上可以分为

  1. var inobj = { } //创建一个内置对象
  2. 如果Animal.prototypeObject类型,则将inobj.__proto__设置为Animal.prototype,否则inobj.__proto__将初始化值(即Object.prototype) //告诉实例对象的__proto__是谁
  3. Animal.call(inobj) //把inobj赋值给函数内部的 this
  4. 如果[[Call]]的返回值是Object类型,则返回这个值,否则返回inobj

那么a1就接收了这个返回值,也就是刚刚实例化的inobj

那么继承又是怎么实现的?这就要讲到js原型链的搜索机制了。

当对象访问某属性或者调用某个方法时:

① 首先在实例对象中搜索该属性/方法

② 如果没有找到则搜索实例对象.__proto__上的属性/方法

③ 如此一层一层沿着原型链继续向上搜索

④ 直到查找到Object.prototype(原型链的顶端),如果有就直接使用,如果没有,返回undefined或者报错

正是因为有这样的搜索机制,inobj设置了__proto__之后,将会继承原型上的属性和方法,然后继承原型的原型上的属性和方法。。。。。。

JS原型链的本质在于__proto__,也就是前文所说的[[Prototype]]

constructor

简单一点说,constructor始终指向创建当前实例对象的构造函数。

根据上面提到高程三中所说的,无论什么时候,只要创建了一个新函数,就会根据特定规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。后面还有一句,在默认情况下,所有原型对象都会自动获得一个constructor(构造器)属性,这个属性指向prototype属性所在的函数,也就是指向构造函数(因为**构造函数.prototype === 实例对象.__proto__**)。如果该实例对象同时是别的构造函数的原型对象,那么它也会有constructor属性,我们通过实例对象.constructor方法访问的也有可能是继承自原型链上的constructor(当然可以通过判断一个属性、方法是存在于实例还是原型链上)

img

下面的代码为例,a1.constructor-->a1有constructor属性吗?没有,然后通过原型链查找到-->a1.__proto__, 也就是Animal.prototype上的constructor属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//定义构造函数Animal
function Animal(name) {
this.name = name;
}
//设置构造函数Animal原型对象上的方法
Animal.prototype.shout =function (){
console.log('wow wow');
}
//通过构造函数Animal创建实例对象a1,该过程称为实例化
var a1 = new Animal('小白');

console.log(a1.constructor);
//打印的是创建a1的构造函数Animal
// ƒ Animal(name) {
this.name = name;
}

但是当constructor遇到prototype时,有趣的事情就发生了。 我们知道每个函数都有一个默认的属性prototype,而这个prototype指向的原型对象上的constructor属性默认指向prototype属性所在的函数,也就是这个函数(就一个互相指向的关系)。如下例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person(name) {
this.name = name;
};
Person.prototype.sayName = function () {
console.log(this.name);
};
var p = new Person("ZhangSan");

console.log(p.__proto__.constructor === Person); // true
console.log(Person.prototype.constructor === Person); // true
console.log(p.constructor === Person); // true
console.log(p.hasOwnProperty('constructor')); //false //检测实例上有constructor属性吗?答案是false
console.log(p.__proto__.hasOwnProperty('constructor')); //true //在原型对象上检测到有constructor属性存在

因此结合代码输出结果,结合上面的图片就不难理解constructor属性从何而来,指向何处了吧!

结论:构造函数默认有prototype属性,指向原型对象,构造函数的原型对象(实例对象的原型)默认有constructor属性,指向构造函数。

不过,这个constructor 属性易变,不可信赖!

当我们重新定义函数的prototype时(注意:和上例的区别,这里不是修改而是覆盖), constructor的行为就有点奇怪了,如下面例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Person(name) {
this.name = name;
};
Person.prototype = {
sayName: function() {
console.log(this。name);
}
};
var p = new Person("ZhangSan");

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

为什么呢? 原来是因为重新定义Person.prototype时,等价于进行如下代码操作:

1
2
3
4
5
Person.prototype = new Object({
sayName: function() {
console.log(this.name);
}
});

constructor始终指向创建自身的构造函数,所以此时Person.prototype.constructor === Object

怎么修正这种问题呢?方法也很简单,重新覆盖Person.prototype.constructor即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Person(name) {
this.name = name;
};
Person.prototype = {
constructor: Person,
sayName: function() {
console.log(this.name);
}
};
var p = new Person("ZhangSan");

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

所以说constructor属性易变,如果是直接在原型上添加属性和方法倒是不会改变constructor,但是重写原型对象后若没有手动修正constructor属性,那么就不可靠了。

其实 constructor 的出现原本就是用来进行对象类型判断的,既然不可靠,那我们有一种更加安全可靠的判定方法:instanceof 操作符

即使上面没有修正constructor属性,下面的结果依然是true。

1
console。log(p instanceof Person);  //true

下面还有一个示例,根据这个示例我还画了一个较为详细的图,引申到了ObjectFunction函数,其他的如Array等构造函数其实也是同理,有兴趣的可以花点时间研究看看(图片建议从下往上去分析)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//定义Animal构造函数
function Animal() {
}
//定义Dog构造函数
function Dog() {
}

var a1 = new Animal();
Dog.prototype = a1;
Dog.prototype.constructor = Dog; //手动修正constructor
var d1 = new Dog();

console.log(d1.constructor); //Dog构造函数
console.log(Dog.prototype.constructor); //Dog构造函数
console.log(d1 instanceof Dog); //true
console.log(d1 instanceof Animal); //true

img

有个注意点就是Object.prototype是由Object构造函数实例化出来的,同时Object.prototypeObject构造函数的原型对象,那么Object.prototype的原型__proto__不就是它自己吗?这样就无限循环了,所以或许为了纠正这个bug,就把它的原型__proto__设置为null,好有个终点嘛!然后还有一个注意点就是,由于创建对象的过程都是通过new创建一个inobj,因此在js中,万物皆对象,所有的内置或自定义对象都继承自Object对象,几乎所有的对象都可以使用Object.prototype上面的属性和方法。

画的也不是很完整,constructor没画上去,感觉那样线就太多太乱了。不过细心钻研一下还是能懂的,如若有错,请不吝指正哈!

坚持原创技术分享,您的支持将鼓励我继续创作!
0%