JavaScript中的对象
对象的概念
JavaScript对象的描述: An object is a collection of properties and has a single prototype object. The prototype may be either an object or the null value.
从上面一段话中我们可以看出,在JavaScript中,对象是一个属性的集合,并且有一个原型对象。 而这个原型本身或者为null,或者也是一个对象。
JavaScript中内置了11种对象,我们这里关注比较特殊的两个对象Function和Object。
Function: 在JavaScript中函数也是对象,用来构造对象的函数叫做构造函数,如 function Animal (){}, Animal就是一个构造函数,所有的函数都可以用来构造对象,用new就是构造函数,但通常我们采用首字母大写来与普通函数区别。 所有的构造函数都是由Function这个函数对象构造出来的,它构造了系统中所有的函数对象,包括用户自定义的函数对象,系统内置的函数对象,也包括它自己。可以用以下代码证明:
function Animal() {};
console.log(Animal instanceof Function); // 用户自定义函数对象
console.log(Object instanceof Function); //系统内置Object函数对象
console.log(Function instanceof Function); //Function本身
console.log(Date instanceof Function); //系统内置的Date对象
运行结果
Object:Object是JavaScript中另外一个比较重要的内置对象,所有的对象都将继承Object原型。它本身也是一个构造函数,且由Function构造。
理解构造关系在JavaScript中十分重要,它决定了JavaScript中的原型链。对象有一个重要的属性[[prototype]], 这个[[prototype]]是一个引用,其指向构造对象本身的构造函数的prototype。 为了便于表达,我们用Mozilla定义的__proto__来表示[[prototype]](非标准,其它JavaScript引擎不一定叫__proto__)。注意不要把__proto__和prototype弄混, 所有的对象都有一个额外的属性__proto__来指向一个原型,构造函数也不例外,这个__proto__就指向其构造函数的prototype。在本文的描述中__proto__只是指向一个prototype,而prototype本身是个对象,一般用来存放函数。
在ECMAScript中这样描述prototype:
Each constructor is a function that has a property named “prototype” that is used to implement prototype-based inheritance and shared properties.
通过上面描述我们可知,prototype只是构造函数的一个属性,其用于实现基于原型的继承和共享属性。
下面我们通过一段代码的内存布局图来理解对象的构造关系以及其__proto__的指向关系。
function Animal() {};
var dog = new Animal();
这两句简单的代码在内存中布局如下:
__proto__和prototype总结
prototype: prototype是在构造函数生成的时候,会默认由Object函数给其构造一个prototype,所以其prototype的__proto__指向Object的prototype,所以所有的构造函数都有prototype, 但是实例对象没有,就是你手工赋值一个prototype,那prototype也是一个普通属性,和原型没有关系。
- • prototype是一个实例对象而不是构造函数,所以其有__proto__而没有prototype.
- • prototype由Object创建,所以其__proto__ 都指向Object的prototype
__proto__: 所有对象(包括构造函数, 因为函数也是对象)都有__proto__属性,这个属性指向其构造函数的prototype。 每个
- • Function由Function自己构造,其__proto__都指向Function的prototype。
- • Object也由Function构造,其__proto__都指向Function的prototype。
- • 构造函数也由Function构造,其__proto__指向Function的prototype(如Animal指向Function的prototype)。
- • 实例由构造函数构造,所以实例也有__proto__,并且指向其构造函数的prototype(如dog指向Animal的prototype)。
因为实例的__proto__指向其构造函数的prototype, 构造函数的prototype的__proto__又指向Object的prototype,所以实例可以根据这种关联找到所有prototype上的属性和方法,这种指向关系即我们常说的原型链。
对象的创建
由于JavaScript语法的灵活性,其对象的创建有很多方式,在阅读别人源码的时候,经常会看到各种不同的创建方法。下面我们看看几种常见的创建对象方式的优缺点。
- 1.字面量方式创建:
var person = {
name:"Archer",
age:29,
job:"software engineer",
sayName:function(){
console.log(this.name);
}
};
person.sayName();
这种方式创建对象简单易用,但是不方便代码重用。
- 2.以工厂模式创建:
function createPerson(name,age, job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
console.log(this.name);
};
return o;
}
var person1 = createPerson("Archer", 29,"software engineer");
var person2 = createPerson("idda", 24,"software engineer");
这种方式创建也很简单,可以重用代码。
- 3.构造函数创建:
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
console.log(this.name);
};
}
var person1 = new Person("Archer", 29,"software engineer");
var person2 = new Person("idda", 24,"software engineer");
构造函数和工厂模式比:
- ¬ 没有创建对象o。
- ¬ 直接将属性方法赋值给this对象。
- ¬ 没有return语句。
构造函数模式时,创建对象实际为如下4步:
- ¬ 创建一个新对象
- ¬ 将构造函数的作用域赋值给新对象(this就指向这个新对象)
- ¬ 执行构造函数中的代码(为这个新对象添加属性,方法)
- ¬ 返回新对象。
构造函数和其它普通函数的区别:
- ¬ 调用方式不同(构造函数和普通函数没有本质区别,任何函数,用new调用就是构造函数,不通过new调用就是普通函数)
//当构造函数使用
var person = new Person("Archer", 29,"software engineer");
person.sayName(); //Archer
//普通函数使用
Person("idda",20,"Doctor"); //在浏览器中,是添加到window
window.sayName(); //idda
console.log(window.age); //20
- ¬ 按照习俗,将构造函数首字母大写
构造函数的缺点:每个方法都要在每个实例中创建一遍:
console.log(person1.sayName === person2.sayName); //false
内存布局为:
- 4.原型模式:
function Person(){
}
Person.prototype.name = "Archer";
Person.prototype.age = 28;
Person.prototype.job = "software engineer";
Person.prototype.sayName = function(){
console.log(this.name);
};
var person1 = new Person();
var person2 = new Person();
我们创建的每个函数都有一个prototype(原型)属性, 这个属性是一个指针,指向一个prototype对象。 这个prototype对象包含所有此函数的实 例共享的属性和方法。
原型模式的优点对比构造函数来说就是让所有实例共享它包含的属性和方法。
console.log(person1.sayName === person2.sayName); //true
执行:Console.log(person1.name);
跟踪person1的状态可见,person1没有name属性,会根据原型链去起__proto__重查找name,其值为Archer.
执行person1.name = "idda";
后跟踪其状态,可见person1增加了一个同名属性name。
所以更改person1的属性,不会修改person2的属性,代码及结果如下:
执行结果
内存布局如下:
原型的简化语法
function Person(){
}
Person.prototype ={
name:"archer",
age:29,
job:"software engineer",
sayHello:function(){
console.log("hi");
}
};
注意简化写法constructor不再指向Person,而是指向Object,如果需要指向Person,可手动修改:
function Person(){
}
Person.prototype ={
constructor:Person,
name:"archer",
age:29,
job:"software engineer",
sayHello:function(){
console.log("hi");
}
};
另外一个微小的区别,其constructor的[[Enumerable]]的值由false变成了true,可以代码重置
Object.defineProperty(Person.prototype,”constructor”,{
enumerable:false,
value:Person
}
);
对原型的修改,都会反映到其指向它的实例。如果重写了对象原型后,对象和其新原型没有任何联系,得不到新原型的任何属性
function Person(){
}
var friend = new Person();
Person.prototype ={
name:”archer”,
age:29,
job:”software engineer:,
sayHello:function(){
console.log(“hi”);
}
};
firend不能访问name,sayHello等属性,如图
原型模型的问题是所有实例都共享原型,对于原型中的值类型,可以访问原型中的值,但是如果修改原型中的值,则会给对象自身添加一个同名属性,当下次访问此属性的时候,则返回此属性,不再查找作用域链。对于引用类型,如果不修改引用,而修改其堆上的值,就会导致所有实例的值都发生更改。
,如:
function Person(){
}
Person.prototype.name = "Archer";
Person.prototype.age = 28;
Person.prototype.job = "software engineer";
Person.prototype.firends = ["idda","gang"];
Person.prototype.sayName = function(){
console.log(this.name);
};
var person1 = new Person();
var person2 = new Person();
person1.firends.push("maxi");
下图可以看见,我们修改的是person1,但是person2也跟着改了:
所以我们很少直接用原型模式,一般来说我们都会采用组合模式,组合模式是JavaScript中常用的模式
- 5.组合模式: 用构造函数定义私有属性,用原型模式定义函数和共享属性
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ["mike","xiaoyu","maxi"];
}
Person.prototype = {
constructor: Person,
sayHello:function(){
console.log(this.name);
}
}
var person1 = new Person("archer",20,"software engineer");
var person2 = new Person("idea",29,"peasant-worker");
构造函数中的属性name,age,job,friends都是自身属性,将存储在实例本身里(不在原型中),修改不会影响其它实例,而函数sayHello是共享的,存储在原型中。
内存布局如下:
对象的继承
JavaScript中对象的继承也比较灵活,我们列举几种常见的模式,并讨论其优缺点
- 1.原型链继承
function SuperType(){
this.name = "parent";
}
SuperType.prototype.getSuperValue = function(){
return this.name;
}
function SubType(){
this.sub_name = "child";
}
//注意:此处继承了SuperType,实际上是将SubType的原型改为SuperType的实例,从而拥有其原型链
SubType.prototype = new SuperType();
//注意,此方法一定要放在上面赋值语句后面,也不能采用字面量方式声明(字面量方式声明实际上是创建一个Object对象),否则修改了原型的指针,无法继承其原型链。
SubType.prototype.getSubValue = function(){
return this.sub_name;
}
var sub = new SubType();
console.log(sub.getSuperValue());
内存布局入下:
原型继承的缺点:
- • 所有的实例共享了引用类型的值,值类型由于赋值是新增属性,所以不会共享
- • 无法传递参数到构造函数
- 2.借用构造函数方式继承
function SuperType(){
this.colors = new ["blue”,”green”,”red"];
}
function SubType(){
//借用构造函数继承
SuperType.call(this);
this.sayHello = function(){
console.log("hi");
}
}
借用构造的问题是,方法都在构造函数内定义,无法重用代码。
- 3.组合继承:将原型链和借用构造函数组合在一起,思路是通过构造函数继承实例属性,通过原型链继承原型属性. 是javascript中最常用的继承模式。
function SuperType(name){
this.name = name;
this.colors = ["white","red"]
}
SuperType.prototype.sayHello = function(){
console.log(this.name);
}
function SubType(name,age){
SuperType.call(this,name); //第二次调用
this.age = age;
}
SubType.prototype = new SuperType(); //第一次调用
SubType.prototype.constuctor = SubType;
SubType.prototype.sayAge = function(){
console.log(this.age);
}
var person = new SubType("Archer",29);
person.sayAge();
内存布局为:
组合继承的缺点在于调用了两次SuperType的构造函数,而且new SuperType调用了构造函数,造成内存浪费。SuperType的构造函数生产的变量会被SubType调用的call(this,name)给覆盖。可用下列方法改进:
将 SubType.prototype = new SuperType(); 替换为:
var F = function(){
};
F.prototype = SuperType.prototype;
SubType.prototype = new F();
改进前:
改进后可以看见其__proto__指向F,所以没有给colors和name分配内存。