jacascript 构造函数、原型对象和原型链
前言:这是笔者学习之后自己的理解与整理。如果有错误或者疑问的地方,请大家指正,我会持续更新!
先梳理一下定义:
- 我们通常认为 object 是普通对象,function 是函数对象;
- Function 跟 Object 本身是 javascript 自带的函数对象;
- 每一个函数对象都有一个显示的 prototype 属性,它代表了对象的原型(Function.prototype 函数对象是个例外,没有 prototype 属性);
- 所有对象(普通对象 和 函数对象)都有一个名为 __proto__ (注意是两个下划线连在一起) 的内部隐藏属性,指向于它所对应的原型对象。原型链正是基于 __proto__ 才得以形成(原型链不是基于函数对象的属性 prototype)。
- Function.prototype 是一个函数对象,前面说函数对象都有一个显示的 prototype 属性,但是 Function.prototype 却没有 prototype 属性,即Function.prototype.prototype === undefined,所以 Function.prototype 函数对象是一个特例,没有 prototype 属性。
- 在 JavaScript 中,每个函数对象都有名为 prototype 的属性(上面提到过 Function.prototype 函数对象是个例外,没有 prototype 属性 ),用于引用原型对象。此原型对象又有名为 constructor 的属性,它反过来引用函数本身。
- 如何查找一个对象的 constructor,就是在该对象的原型链上寻找碰到的第一个 constructor 属性所指向的对象。
- 注意 Object.constructor === Function;Object 本身就是 Function 函数构造出来的。
- Object 虽是 Function 构造的一个函数对象,但是 Object.prototype 没有指向 Function.prototype,即 Object.prototype !== Function.prototype;
- 所有对象(包括函数对象)的原型链最终都指向了 Object.prototype,Object.prototype 是一个普通对象,而 Object.prototype.__proto__ === null,原型链至此结束。
<script type="text/javascript"> var o1 = {}; var o2 =new Object(); function f1(){} var f2 = function(){} var f3 = new Function(); var f4 = new f1; var f5 = new f2; var f6 = new f3; console.log(typeof Object); //function console.log(typeof Function); //function //Object 和 Function ,是 javascript 自带的对象,是函数对象 console.log(typeof o1); //object console.log(typeof o2); //object console.log(typeof f1); //function console.log(typeof f2); //function console.log(typeof f3); //function console.log(typeof f4); //object console.log(typeof f5); //object console.log(typeof f6); //object console.log(typeof Function.prototype); //function console.log(typeof f1.prototype); //object console.log(Function.prototype.prototype ); //undefined console.log(f1.prototype.prototype ); //undefined //Function.prototype 函数对象是个例外,没有 prototype 属性; //普通对象也没有 prototype 属性 console.log(f4 instanceof f1);//true console.log(f4.constructor === f1);//true console.log(f4._proto_ === f1.prototype); //false // __proto__ (注意是两个下划线连在一起) console.log(f4.__proto__ === f1.prototype); //true //所有对象(普通对象 和 函数对象)都有一个名为 __proto__ 的属性,指向于它所对应的原型对象 //f4函数是由f1函数构造的,f4也就是f1的实例,所以f4的 __proto__指向f1.prototype; //原型链: //在访问属性时,先在f4中查找,没找到就顺着 __proto__指向的原型对象查找, //原型对象也有一个__proto__指针,又指向了另一个原型,一个接一个,就形成了原型链,原型链的最顶层是Object.prototype,它的__proto__指向null。 console.log(f1 instanceof Function);//true console.log(f1.constructor === Function);//true console.log(f1.__proto__ === Function.prototype); //true // f1 函数是由 Function 构造的,f1也就是Function的实例,所以 f1 的__proto__指向Function.prototype; console.log(typeof Function.prototype); //function console.log(typeof Object.prototype); //object console.log(Function.prototype.constructor === Function);//true console.log(Function.prototype instanceof Object);//true console.log(Object.prototype.constructor === Object);//true console.log(Object.prototype instanceof Object);//false console.log(Function.prototype.__proto__ === Object.prototype); //true //Function.prototype 也是函数对象,它是由Object 构造的,所以 Function.prototype 函数对象的__proto__指向 Object.prototype; console.log(typeof Object.prototype); //object console.log(Object.prototype.__proto__); //null //Object.prototype 是一个普通对象,原型链至此结束 console.log(f1.__proto__.__proto__ === Object.prototype); //true console.log(Function.prototype === Object.__proto__);//true console.log(Function.__proto__ === Object.__proto__);//true console.log(Function.__proto__ === Function.prototype);//true </script>
构造函数、原型对象、实例对象之间的关系,比较绕:
- 构造函数有一个 prototype 属性,指向其对应的原型对象;
- 原型对象有一个 constructor 属性,指向其对应的构造函数;
- 实例对象有一个 _proto_ 属性,指向其对应的原型对象;
- 实例对象可以从原型对象继承属性,所以实例对象也有一个 constructor 属性,指向其对应的构造函数;
所以,实例对象和构造函数之间没有直接关系,间接关系是实例对象可以继承原型对象的 constructor 属性。
构造函数
如果一个函数用 new 关键字调用,这个函数就是构造函数,并且背地里会创建一个连接到该函数的 prototype 的新对象,this 指向这个新对象;
如果构造函数没有形参,实例化的时候是可以不带()的;如 var a = Func; 或者 var a = Func(); 两种都可以;
同时我们在构造函数的时候有个约定(不是规范),首字母大写,以避免忘了写 new 关键字或者在普通函数前面加 new;
new 关键字的作用就是执行一个构造函数,并返回一个对象实例。使用 new 命令,它后面的函数的函数调用和普通函数调用就不一样了,步骤如下:
- 创建一个空对象,作为将要返回的对象实例;
- 将空对象的 __proto__ 属性指向构造函数的 prototype 属性;
- 将构造函数内部的 this 关键字指向空对象;
- 执行构造函数内部的代码;
就是说 this 指向这个新对象,构造函数内所有针对 this 的操作,都会发生在这个新对象上;
<script type="text/javascript"> // 创建一个名为Person 的构造函数,它构造一个带有user 和age 的对象 var Person = function (user,age) { this.user = user; this.age = age; }; // 构造一个Person 实例 ,并测试 var shane = new Person ('shane',25); console.log(shane.user);//shane console.log(shane.age);//25 </script>
每个对象在创建时,都自动拥有一个属性 constructor,指向其构造函数;这个 constuctor 属性实际上继承自原型对象;
虽然对象实例和构造函数之间有这样的关系,但还是建议使用 instanceof 来检测实例对象和构造函数的关系。这是因为构造函数的属性可以被修改,并不一定准确;
<script type="text/javascript"> var Person = function (user) { this.user = user; }; // 构造一个Person 实例 ,并测试 var shane = new Person ('shane'); console.log(shane.user);//shane console.log(shane.constructor === Person);//true //每个对象在创建时,都自动拥有一个属性constructor,指向其构造函数; //这个constuctor属性实际上继承自原型对象; Person.prototype.constructor = 123; //constructor属性可以被修改 console.log(shane.constructor === Person);//false 构造函数的属性可以被修改,所以不推荐用来检测实例对象和构造函数的关系 console.log(shane instanceof Person);//true 建议使用instanceof来检测实例对象和构造函数的关系。 </script>
javascript 中构造函数是不需要有返回值的,可以认为构造函数和普通函数之间的区别就是:构造函数没有 return 语句,普通函数可以有 return 语句;
构造函数使用 this 关键字定义变量和方法,当 this 遇到 return 的时候,会发生指向不明(调用结果不明)的问题:
- return 返回的不是一个对象,this 还是指向实例(新对象),调用结果也还是新对象;
- return 返回的是一个对象,this 就指向这个返回的对象,调用结果就是这个返回的对象;
- return 返回的是 null,this 还是指向实例,调用结果也还是新对象;
<script type="text/javascript">
var Person = function(){
this.user = 'shane';
return
}
var shane = new Person;
console.log(shane.user);//shane return没有返回值,this还是指向实例(新对象),调用结果也还是新对象;
var Color = function(){
this.red = 'red';
return 'hello world';
}
var redColor = new Color;
console.log(redColor.red);//red return返回的是一个基本类型的字符串(原始值),this还是指向实例(新对象),调用结果也还是新对象;
var Size = function(){
this.size = 'big';
return {};
}
var sizeBig = new Size;
console.log(sizeBig.size);//undefined return返回的是一个对象,this就指向这个返回的对象,调用结果就是这个返回的对象;
</script>
使用构造函数的好处是所有由同一个构造函数实例化出来的对象,都有同样的属性和方法;
构造函数的不足是所有方法都要在每个实例上重新创建一次,浪费内存空间;
可以把方法定义到构造函数外部来解决,方法定义在构造函数外部,构造函数实例化执行的时候,只是一个指针指向一个函数,而不会再次创建,但这又带来新的问题:
- 如果需要定义很多方法,那就要声明很多变量了,严重污染全局空间;
- 同时这些变量只是为了某个函数创建的,虽然也可以直接在外部使用,但没有意义,这有点浪费;
<script type="text/javascript"> var Person = function (user) { this.user = user; this.who = who; //注意这里是传函数,不是传返回值,不要带() }; function who(){ return this.user; } var shane = new Person ('shane'); var lucy = new Person ('lucy'); console.log(shane.who);//function who(){return this.user;} console.log(shane.who());//shane 带上()执行,不带()就只是函数了 console.log(shane.who === lucy.who);//true //方法定义在构造函数外部,构造函数实例化执行的时候,只是一个指针指向一个函数,而不会再次创建 //但这又带来另一问题,如果需要定义很多方法,那就要声明很多变量了,严重污染全局空间 //同时这些变量只是为了某个函数创建的,虽然也可以直接在外部使用,但没有意义,这有点浪费 </script>
如果所有的对象实例共享同一个方法,并且不用声明变量,会更有效率,这就需要用到下面所说的原型对象;
原型对象
构造函数、原型对象、实例对象之间的关系:
- 用来 new 初始化新创建对象的函数是构造函数;
- 通过构造函数的 new 操作创建的对象是实例对象。可以用一个构造函数,构造多个实例对象;
- 构造函数有一个 prototype 属性,指向其对应的原型对象;通过同一个构造函数实例化出来的多个对象具有相同的原型对象。经常使用原型对象来实现继承。
- 原型对象有一个 constructor 属性,指向其对应的构造函数;
- 实例对象有一个 _proto_ 属性,指向其对应的原型对象;
- 由于实例对象可以继承原型对象的属性,所以实例对象也拥有constructor属性,同样指向原型对象对应的构造函数;
<script type="text/javascript"> var Person = function (user) { //Person 就是构造函数 this.user = user; }; Person.prototype.who = function(){ // Person.prototype就是原型对象 return this.user; } var shane = new Person ('shane'); //shane就是实例对象 console.log(shane.user);//shane console.log(shane.who());//shane console.log(Person.prototype.constructor === Person);//true //原型对象有一个 constructor 属性,指向其对应的构造函数; console.log(shane.constructor === Person);//true //每个实例对象在创建时,都自动拥有一个属性constructor,指向其构造函数; //这个constuctor属性实际上继承自原型对象; console.log(shane.__proto__ === Person.prototype);//true //实例对象有一个 __proto__ 属性,指向其对应的原型对象; Person.prototype.constructor = 123; //constructor属性可以被修改 console.log(shane.constructor === Person);//false 构造函数的属性可以被修改,所以不推荐用来检测实例和构造函数之间的关系 console.log(shane instanceof Person);//true 建议使用instanceof来检测实例和构造函数之间的关系。 </script>
一般地,可以通过 isPrototypeOf() 方法来判断实例对象和原型对象的关系;
ES5 新增了 Object.getPrototypeOf() 方法,该方法返回实例对象对应的原型对象;
<script type="text/javascript"> var Person = function () {}; //Person 就是构造函数 var shane = new Person; //shane就是实例对象 console.log(shane.__proto__ === Person);//false console.log(shane.__proto__ === Person.constructor);//false console.log(shane.__proto__ === Person.prototype);//true Person.prototype就是原型对象 //实例对象有一个 _proto_ 属性,指向该实例对象对应的原型对象; console.log(Person.isPrototypeOf(shane));//false console.log(Person.constructor.isPrototypeOf(shane));//false console.log(Person.prototype.isPrototypeOf(shane));//true //我们可以用 Obj1.isPrototypeOf(obj2);来判断 Obj1 是否为 obj2 的原型对象 console.log(Object.getPrototypeOf(shane) === Person.prototype); //true //ES5新增了Object.getPrototypeOf()方法,该方法返回实例对象对应的原型对象 console.log(Object.getPrototypeOf(shane) === shane.__proto__); //true //实际上,Object.getPrototypeOf()方法和__proto__属性是一回事,都指向原型对象 </script>
属性查找
当读取一个对象的属性时,引擎首先在该对象的自有属性中查找属性名字。如果找到则返回;如果自有属性不包含该名字,则会顺着__proto__搜索原型链中的对象。如果找到则返回。如果找不到,则返回 undefined;
in操作符可以判断属性在不在该对象上,但无法区别自有属性还是原型属性;
通过 obj.hasOwnProperty(attr) 方法可以确定该属性是否为自有属性;
于是可以将 obj.hasOwnProperty(attr) 方法和 in 运算符结合起来使用,用来鉴别原型属性;
<script type="text/javascript"> var Person = function(){ this.name = 'shane'; }; Person.prototype.height = 173; var man = new Person; man.age = 25; console.log('name' in Person);//true 属性要用引号包起来 console.log('age' in Person);//false console.log('height' in Person);//false console.log('height' in Person.prototype);//true console.log(Person.hasOwnProperty('name'));//true console.log(Person.hasOwnProperty('age'));//false console.log(Person.hasOwnProperty('height'));//false console.log('name' in man);//true console.log('age' in man);//true console.log('height' in man);//true console.log(man.hasOwnProperty('name'));//true console.log(man.hasOwnProperty('age'));//true console.log(man.hasOwnProperty('height'));//false // obj.hasOwnProperty(attr) 和 in 操作符结合起来可以用来鉴别原型属性 // (attr in obj && !obj.hasOwnProperty(attr)) 为真则为原型属性; console.log('name' in man && !man.hasOwnProperty('name')); //false console.log('age' in man && !man.hasOwnProperty('age')); //false console.log('height' in man && !man.hasOwnProperty('height')); //true </script>
原型对象可以存放公用属性或方法,但存放引用值时需要注意,实例对象可以修改原型上的这些公用的引用值,所以我们一般把引用值属性写在构造函数里,公用方法写在原型对象上;
<script type="text/javascript"> var Person = function(name){ this.name = name; }; Person.prototype.likes = ['apple','banana']; var shane = new Person('shane'); var lucy = new Person('lucy'); console.log(shane.likes); //['apple','banana'] shane.likes.splice(0,0,'car'); console.log(lucy.likes); //['car','apple','banana'] </script>
我们可以用对象字面量的方式写原型对象上的属性和方法,使代码更加整洁;
原型对象有一个 constructor 属性是指向其对应的构造函数的,所以当我们把一个对象字面量赋给原型对象时,会产生问题;
解决方法就是在对象字面量里手动添加一个 constructor 属性,指向其对应的构造函数;
<script type="text/javascript"> var Person = function(name){ this.name = name; }; Person.prototype = { constructor: Person, //原型对象有一个 constructor 属性是指向其对应的构造函数的,所以当我们把一个对象字面量赋给原型对象时,会产生问题; //在对象字面量里手动添加一个 constructor 属性,指向其对应的构造函数; sayName: function(){ return this.name; } }; var shane = new Person('shane'); console.log(shane.sayName());//shane console.log(shane.constructor === Person);//true </script>
组合模式
构造函数模式和原型模式结合起来使用是创建自定义类型的最常见方式。构造函数模式用于定义实例属性,而原型模式用于定义共享的方法,这种组合模式还支持向构造函数传递参数。实例对象都有自己的一份实例属性的副本,同时又共享对方法的引用,最大限度地节省了内存。
<script type="text/javascript"> var Person = function(name){ this.name = name; this.likes = ['apple','banana']; }; Person.prototype = { constructor: Person, sayName: function(){ return this.name; } }; var shane = new Person('shane'); var lucy = new Person('lucy'); console.log(shane.likes); //['apple','banana'] shane.likes.splice(0,0,'car'); console.log(shane.likes); //['car','apple','banana'] console.log(lucy.likes); //['apple','banana'] </script>
扩展原型
在不能直接访问原型的时候,如果能访问实例,就可以用 instance.constructor.prototype 去修改或扩展原型对象;
<script type="text/javascript">
var shane;
(function(){
function Person (a,b) {
this.a = a;
this.b = b;
}
Person.prototype.sayName = function () {
return this.a;
}
shane = new Person('shane',25);
})();
console.log(shane.sayName());//shane
//在不能直接访问原型的时候,如果能访问实例,就可以用 instance.constructor.prototype 去修改或扩展原型对象;
shane.constructor.prototype.sayAge = function(){
return this.b;
};
console.log(shane.sayAge());//25
</script>