构造函数和原型对象

一般地,javascript使用构造函数和原型对象来进行面向对象编程,它们的表现与其他面向对象编程语言中的“类”相似又不同。

上一篇文章 已经做过对 构造函数 和 原型对象 的简单介绍,下面为了对比它们,做一些内容细节上的补充:

构造函数的基本特点

new操作符

构造函数是用new创建对象时调用的函数,与普通函数唯一的区别是构造函数名应该首字母大写。

function Person(){
    this.age = 30;
}
var person1 = new Person();
console.log(person1.age);//30

如果忘记使用new操作符,则this将会指向全局对象window:

function Person(){
    this.age = 30;
}
var person1 = Person();
console.log(window.age);//30
console.log(person1.age);//Uncaught TypeError: Cannot read property 'age' of undefined

构造函数的参数

根据需要,构造函数可以接受参数:

function Person(age){
    this.age = age;
}
var person1 = new Person(30);
console.log(person1.age);//30

如果没有参数,可以省略括号:

function Person(){
    this.age = 30;
}
//等价于var person1 = new Person() :
var person1 = new Person;
console.log(person1.age);//30

鉴别对象的类型的方法:

instanceof

instanceof操作符可以用来鉴别对象的类型:

function Person(){
    //
}
var person1 = new Person;
console.log(person1 instanceof Person);//true

constructor

上一篇文章 已经介绍过了constructor属性的指向。

每个对象在创建时都自动拥有一个构造函数属性constructor,其中包含了一个指向其构造函数的引用。而这个constructor属性实际上继承自原型对象,而constructor也是原型对象唯一的自有属性:

function Person(){
    //
}
var person1 = new Person;
console.log(person1.constructor === Person);//true    
console.log(person1.__proto__.constructor === Person);//true

以下是person1的内部属性,发现constructor是继承属性:

js-constructor

虽然对象实例及其构造函数之间存在这样的关系,但是还是建议使用instanceof来检查对象类型。这是因为构造函数属性可以被覆盖,所以并不一定完全准确:

function Person(){
    //
}
var person1 = new Person;
Person.prototype.constructor = 123;
console.log(person1.constructor);//123
console.log(person1.__proto__.constructor);//123

构造函数的返回值

普通函数中的return语句用来返回函数调用后的返回值,而new构造函数的返回值有点特殊:

1.如果构造函数使用return语句但没有指定返回值,或者返回一个原始值

那么这时将忽略返回值,同时使用这个新对象作为调用结果:

function fn(){
    this.a = 2;
    return;
}
var test = new fn();
console.log(test);//{a:2}

2.如果构造函数显式地使用return语句返回一个对象

那么调用表达式的值就是这个对象:

var obj = {a:1};
function fn(){
    this.a = 2;
    return obj;
}
var test = new fn();
console.log(test);//{a:1}

常见应用:

所以,针对丢失new的构造函数的解决办法是:在构造函数内部使用instanceof判断是否使用new命令,如果发现没有使用,则直接使用return语句返回一个实例对象:

function Person(){
    if(!(this instanceof Person)){
        return new Person();
    }
    this.age = 30;
}
var person1 = Person();
console.log(person1.age);//30
var person2 = new Person();
console.log(person2.age);//30

构造函数的问题

使用构造函数的好处在于所有用同一个构造函数创建的对象都具有同样的属性和方法

function Person(name){
    this.name = name;
    this.sayName = function(){
        console.log(this.name);
    }
}
var person1 = new Person('bai');
var person2 = new Person('hu');
person1.sayName();//'bai'

构造函数允许给对象配置同样的属性,但是构造函数并没有消除代码冗余。

使用构造函数的主要问题是每个方法都要在每个实例上重新创建一遍。 在上面的例子中,每一个对象都有自己的sayName()方法。这意味着如果有100个对象实例,就有100个函数做相同的事情,只是使用的数据不同:

function Person(name){
    this.name = name;
    this.sayName = function(){
        console.log(this.name);
    }
}
var person1 = new Person('bai');
var person2 = new Person('hu');
console.log(person1.sayName === person2.sayName);//false

可以通过把函数定义转换到构造函数外部来解决问题:

function Person(name){
    this.name = name;
    this.sayName = sayName;
}
function sayName(){
    console.log(this.name);
}
var person1 = new Person('bai');
var person2 = new Person('hu');
console.log(person1.sayName === person2.sayName);//true

但是,在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。而且,如果对象需要定义很多方法,就要定义很多全局函数,严重污染全局空间,这个自定义的引用类型没有封装性可言了。

如果所有的对象实例共享同一个方法会更有效率,这就需要用到下面所说的原型对象。

原型对象

介绍prototype、proto和constructor等关系

请参考我的上一篇文章: 理解prototype、proto和constructor等关系

是否为实例对象–原型对象关系

isPrototypeOf()

一般地,可以通过isPrototypeOf()方法来确定对象之间是否是实例对象和原型对象的关系:

function Foo(){};
var f1 = new Foo;
console.log(f1.__proto__ === Foo.prototype);//true
console.log(Foo.prototype.isPrototypeOf(f1));//true

Object.getPrototypeOf()

ES5新增了Object.getPrototypeOf()方法,该方法返回实例对象对应的原型对象:

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

实际上,Object.getPrototypeOf()方法和__proto__属性是一回事,都指向原型对象:

function Foo(){};
var f1 = new Foo;
console.log(Object.getPrototypeOf(f1) === f1.__proto__ );//true

属性的查找

当读取一个对象的属性时,javascript引擎首先在该对象的自有属性中查找属性名字。如果找到则返回。如果自有属性不包含该名字,则javascript会搜索proto中的对象。如果找到则返回。如果找不到,则返回undefined

var o = {};
console.log(o.toString());//'[object Object]'

o.toString = function(){
    return 'o';
}
console.log(o.toString());//'o'

delete o.toString;
console.log(o.toString());//'[objet Object]'

in

in操作符可以判断属性在不在该对象上,但无法区别自有还是继承属性:

function Test(){};
var obj = new Test;
Test.prototype.a = 1;
obj.b = 2;
console.log('a' in obj);//true
console.log('b' in obj);//true
console.log('b' in Test.prototype);//false

hasOwnProperty()

通过hasOwnProperty()方法可以确定该属性是自有属性还是继承属性:

var o = {a:1};
var obj = Object.create(o);
obj.b = 2;
console.log(obj.hasOwnProperty('a'));//false
console.log(obj.hasOwnProperty('b'));//true

于是可以将hasOwnProperty方法和in运算符结合起来使用,用来鉴别原型属性

function hasPrototypeProperty(object,name){
    return name in object && !object.hasOwnProperty(name);
}

属性的添加

原型对象的共享机制使得它们成为一次性为所有对象定义方法的理想手段。 因为一个方法对所有的对象实例做相同的事,没理由每个实例都要有一份自己的方法:

function Person(name){
    this.name = name;
}
Person.prototype.sayName = function(){
    console.log(this.name);
}
var person1 = new Person('bai');
var person2 = new Person('hu');

person1.sayName();//'bai'

可以在原型对象上存储其他类型的数据,但在存储引用值时需要注意。正所谓有利就有弊,因为这些引用值会被多个实例共享,一个实例能够改变另一个实例的值:

function Person(name){
    this.name = name;
}
Person.prototype.sayName = function(){
    console.log(this.name);
}
Person.prototype.favoraties = [];

var person1 = new Person('bai');
var person2 = new Person('hu');

person1.favoraties.push('pizza');
person2.favoraties.push('quinoa');
console.log(person1.favoraties);//["pizza", "quinoa"]
console.log(person2.favoraties);//["pizza", "quinoa"]

用对象字面形式添加属性

虽然可以在原型对象上一一添加属性,但是直接用一个对象字面形式替换原型对象更简洁:

function Person(name){
    this.name = name;
}
//对象字面形式:
Person.prototype = {
    sayName: function(){
        console.log(this.name);
    },
    toString : function(){
        return '[person ' + this.name + ']'
    }
};

var person1 = new Person('bai');

console.log(person1 instanceof Person);//true
console.log(person1.constructor === Person);//false,解释见下一个demo:
console.log(person1.constructor === Object);//true

当一个函数被创建时,该原型对象的constructor属性自动创建,并指向该函数。当使用对象字面形式改写原型对象Person.prototype时,需要在改写原型对象时手动重置其constructor属性:

function Person(name){
    this.name = name;
}
Person.prototype = {
    constructor: Person, // 只加了这一行代码
    sayName: function(){
        console.log(this.name);
    },
    toString : function(){
        return '[person ' + this.name + ']'
    }
};

var person1 = new Person('bai');

console.log(person1 instanceof Person);//true
console.log(person1.constructor === Person);//true
console.log(person1.constructor === Object);//false

由于默认情况下,原生的constructor属性是不可枚举的,更妥善的解决方法是使用Object.defineProperty()方法,改变其属性描述符中的枚举性enumerable:

function Person(name){
    this.name = name;
}
Person.prototype = {
    sayName: function(){
        console.log(this.name);
    },
    toString : function(){
        return '[person ' + this.name + ']'
    }
};
Object.defineProperty(Person.prototype,'constructor',{
    enumerable: false,
    value: Person
});
var person1 = new Person('bai');
console.log(person1 instanceof Person);//true
console.log(person1.constructor === Person);//true
console.log(person1.constructor === Object);//false

总结

构造函数、原型对象和实例对象之间的关系是实例对象和构造函数之间没有直接联系。

function Foo(){};
var f1 = new Foo;

以上代码的原型对象是Foo.prototype,实例对象是f1,构造函数是Foo

原型对象和实例对象的关系:

console.log(Foo.prototype === f1.__proto__);//true

原型对象和构造函数的关系 :

console.log(Foo.prototype.constructor === Foo);//true

而实例对象和构造函数则没有直接关系,间接关系是实例对象可以继承原型对象的constructor属性:

console.log(f1.constructor === Foo);//true

如果非要扯实例对象和构造函数的关系,那只能是下面这句代码,实例对象是构造函数的new操作的结果:

var f1 = new Foo;

这句代码执行以后,如果重置原型对象,则会打破它们三者间的关系:

function Foo(){};
var f1 = new Foo;
console.log(Foo.prototype === f1.__proto__);//true
console.log(Foo.prototype.constructor === Foo);//true

Foo.prototype = {}; // 此时重置原型对象
console.log(Foo.prototype === f1.__proto__);//false
console.log(Foo.prototype.constructor === Foo);//false

所以,代码顺序很重要。

文章目录
  1. 1. 构造函数的基本特点
    1. 1.1. new操作符
    2. 1.2. 构造函数的参数
    3. 1.3. 鉴别对象的类型的方法:
      1. 1.3.1. instanceof
      2. 1.3.2. constructor
    4. 1.4. 构造函数的返回值
      1. 1.4.1. 常见应用:
    5. 1.5. 构造函数的问题
  2. 2. 原型对象
    1. 2.1. 介绍prototype、proto和constructor等关系
    2. 2.2. 是否为实例对象–原型对象关系
      1. 2.2.1. isPrototypeOf()
      2. 2.2.2. Object.getPrototypeOf()
    3. 2.3. 属性的查找
      1. 2.3.1. in
      2. 2.3.2. hasOwnProperty()
    4. 2.4. 属性的添加
      1. 2.4.1. 用对象字面形式添加属性
  3. 3. 总结
|