2015-04-17
JS里一切皆对象,理解对象是理解js语言的关键,下面从几个角度解释js里的对象。
在C++/Java等传统面向对象编程中,类(class)是对象(object)的模板,class不占用内存空间,object占用内存,object也称为class instance,同一个class可以被new出很多个object。
Javascript语言不支持"类",但Javascript仍然是面向对象语言,面向对象的3要素在JavaScript里都支持:
实际上,js里一切皆对象。
js里的对象(object)就是一组键值(name-value)的集合, name总是string类型, value可以是各种类型, 可以是基本数据类型, 可以是数组或其他对象, 也可以是函数, object很像是一个hash map. object里的name-value是无序的。
对象是动态的,可以动态地增加属性和方法。
C++/Java等语言里创建object是使用"new YourClassName()"的方式,js里没有类,可以使用下面几种方式创建object:
var person = {firstName:"John", lastName:"Doe", age:50, eyeColor:"blue"};
属性名可以加引号也可以不加引号,如果属性名有空格,则必须加引号。
var person = new Object();
person.firstName = "John";
person.lastName = "Doe";
person.age = 50;
person.eyeColor = "blue";
这种方式也可以创建其他js内建对象: Function, String, Number, Date, RegExp等,但这种方式没有方法1效率高,因此不推荐.
function Person(first, last, age, eyecolor) {
this.firstName = first;
this.lastName = last;
this.age = age;
this.eyeColor = eyecolor;
}
var myFather = new Person("John", "Doe", 50, "blue");
var myMother = new Person("Sally", "Rally", 48, "green");
这一方法其实和方法2是类似的,方法2里new调用的是js内建的构造函数,这里new调用的是自己写的构造函数。
js里构造函数和普通函数没有任何区别,因此为了区分这是一个构造函数,一般在命名上区分,构造函数使用驼峰式命名,普通函数使用半驼峰式命名。
这是ECMAScript 5新定义的方式,假设已经有方法1里的person对象,可以创建新的对象:
var myself = Object.create(myFather);
这种方式是让一个现有的object(myFather)变成新创建对象(myself)的原型。
如果浏览器不支持Object.create()方法,可以用下面的方法模拟:
if (!Object.create) {
Object.create = function (o) {
function F() {}
F.prototype = o;
return new F();
};
}
方法4和方法3是不同的,方法3里创建的对象(myFather)拥有自己的属性(firstName, lastName等),方法4里创建的对象(myself)没有自己的属性,虽然通过myself可以访问到firstName,lastName等属性,但这些属性属于其原型对象myFather.
访问对象包括访问对象属性和调用对象方法。
访问对象非常方便,有如下几种方式:
var array = [];
array.push(123); //方法1: 使用.访问
array["push"](456); //方法2: 使用[]访问
方法1比较直观,但当属性名是一个变量时,方法2会比较有用。
判断对象person1是否有属性name:
if (person1.name) {} //method 1: use if
if ("name" in person1) {} //method 2: use in
if (person1.hasOwnProperty("name")) {} //method 3: use hasOwnProperty()
说明:
使用delete可以删除属性:
console.log("name" in person1); //true
delete person1.name;
console.log("name" in person1); //false
使用for-in loop可以循环对象里的所有属性:
for (var property in person1) {
console.log("Name: " + property);
console.log("Value: " + person1[property]);
}
ECMAScript5里提供了一个新方法 Object.keys():
var properties = Object.keys(person1);
var i, len;
for (i=0, len=properties.length; i < len; i++){
console.log("Name: " + properties[i]);
console.log("Value: " + person1[properties[i]]);
}
注意:for-in loop会返回原型对象里的属性,Object.keys()只会返回own properties
我们平时使用的基本都是data properties,js对象里实际上有3种类型的属性:
我们可以为accessor properties定义getter方法和setter方法,如果只有getter,说明这是只读的,如果只有setter,说明这是只写的。
var person1 = {
_name: "Nicholas",
get name() {
console.log("Reading name");
return this._name;
},
set name(value) {
console.log("Setting name to ", value);
this._name = value;
}
};
console.log(person1.name); // 打印"Reading name" 和 "Nicholas"
person1.name = "Greg"; // 打印"Setting name to Greg"
console.log(person1.name); // 打印"Reading name" 和 "Greg"
由js引擎内部使用的属性,不能通过js代码直接访问到,不过可以通过一些方法间接的读取和设置, 比如:每个对象都有一个内部属性[[Prototype]]
, 你不能直接访问这个属性, 但可以通过Object.getPrototypeOf()
方法间接的读取到它的值.虽然内部属性通常用一个双中括号包围的名称来表示,但实际上这并不是它们的名字,它们是一种抽象操作,是不可见的,根本没有上面两种属性有的那种字符串类型的属性名.
其他的内部属性还有: [[Extensible]]
, [[DefineOwnProperty]]
, [[Put]]
, 以及function特有的内部属性 [[Call]]
每个属性(property)都拥有4个特性(attribute).数据属性和访问器属性一共有6种属性特性:
[[Value]]
: 属性的值.[[Writable]]
: 控制属性的值是否可以改变.[[Get]]
: 存储着getter方法.[[Set]]
: 存储着setter方法.[[Enumerable]]
: 如果一个属性是不可枚举的,则在一些操作下,这个属性是不可见的,比如for...in和Object.keys()[[Configurable]]
: 如果一个属性是不可配置的,则该属性的所有特性(除了%%[[Value]]%%)都不可改变js对象很容易被修改,但有时我们希望对象不被修改,有如下几种方法:
var person1 = {
name: "Nicholas"
};
console.log(Object.isExtensible(person1)); //true
Object.preventExtensions(person1);
console.log(Object.isExtensible(person1)); //false
person1.age = 50;
console.log("age" in person1); //false
在strict模式下,如果试图向non-extensible的对象增加属性,程序会抛出异常。
seal是密封/封口/盖章/封上信封的意思:
var person1 = {
name: "Nicholas"
};
console.log(Object.isExtensible(person1)); //true
console.log(Object.isSealed(person1)); //false
Object.seal(person1);
console.log(Object.isExtensible(person1)); //false
console.log(Object.isSealed(person1)); //true
person1.age = 50;
console.log("age" in person1); //false
var person1 = {
name: "Nicholas"
};
console.log(Object.isExtensible(person1)); //true
console.log(Object.isSealed(person1)); //false
console.log(Object.isFrozen(person1)); //false
Object.freeze(person1);
console.log(Object.isExtensible(person1)); //false
console.log(Object.isSealed(person1)); //true
console.log(Object.isFrozen(person1)); //true
person1.age = 50;
console.log("age" in person1); //false
JS没有类,但有new操作符,new操作符可以用于调用构造函数,并改变了构造函数里的this:
function Person(first, last, age, eyecolor) {
this.firstName = first;
this.lastName = last;
this.age = age;
this.eyeColor = eyecolor;
}
var myFather = new Person("John", "Doe", 50, "blue");
上面的代码实际上类似于:
// create an empty object
var myMother = {};
// call the function as a method of the empty object
Person.call(myMother, "Sally", "Rally", 48, "green");
所以,new操作符的本质就是,new其实就是创建了一个空对象,然后以这个空对象为context(即this)来调用构造函数,然后构造函数里的赋值语句就动态的给空对象增加了属性和方法。
注意:上面的方法虽然和new很类似,但和new还是有区别的,myFather的原型对象是Person.prototype,而myMother的原型对象是Object.prototype
在js中所有对象都有一个隐含的属性[[prototype]]
指向其原型对象,原型对象也有自己的原型,如此下去便形成一个原型链,所有对象的原型链的顶层都是Object.prototype.
请注意,[[prototype]]
并不是一个真实的属性名,因此无法通过这个属性名获得原型对象,但js提供了方法来读取和判断对象的原型:
// 接前面的例子
console.log(Object.getPrototypeOf(myFather) === Person.prototype); //true
console.log(Person.prototype.isPrototypeOf(myFather)); //true
console.log(Person.prototype.isPrototypeOf(myMother)); //false
对象的原型链通常是只读的,用户无法修改某个对象的原型,所以无法修改对象的继承关系。
在js规范里,对象原型通常是不可见的属性,因此无法直接访问。但某些浏览器里支持__proto__
属性,firefox/chrome/safari/nodejs都支持__proto__
属性,在这些浏览器里对象原型是可见的,可以直接访问对象的__proto__
属性得到对象原型,也可以通过修改对象的__proto__
属性来修改对象的原型链。
ECMAScript 6正在讨论把__proto__
属性标准化,但目前属性__proto__
还不是标准。
本文接下来提到的__proto__
, [[prototype]]
都是指一个意思,即自身对象里指向其原型对象的属性。
函数也是对象,所以函数也有属性__proto__
,通过字面量声明的函数其原型对象是Function.prototype,当然Function.prototype的原型对象是Object.prototype.
函数还有一个特有的属性prototype
,每个函数都有一个prototype属性(特别注意,prototype属性是函数对象特有的属性,不要和js中每个对象到其原型的连接相混淆,那个是隐藏的,只是在firefox/chrome等浏览器中你可以使用__proto__
访问到)。
下面的例子解释了__proto__,prototype,constructor三者的关系:
function Person(name) {
this.name = name;
}
Person.prototype.sayName = function() {
console.log(this.name);
};
var person1 = new Person("Nicholas");
var person2 = new Person("Greg");
上述代码对应的内存模型如下:
person1
--------
__proto__ ----------- -------------------------------------
name:"Nicholas" | | |
|--> Person.prorotype |
person2 | ----------------- |
-------- | constructor --------------> Person |
__proto__ ----------- sayName:function() --------- |
name:"Greg" __proto__ --> prototype ---->
| __proto__ --->
V |
Object.prototype v
^ Function.prototype
| ------------------
<-------------------- __proto__
Person是构造函数(当然也是对象),new出来的person1和person2这两个对象都有一个属性__proto__
指向Person.prototype对象,Person.prorotype对象有一个属性cnstructor指向Person对象,Person对象的prorotype属性对应的就是Person.prorotype对象。
参考阅读: https://ruby-china.org/topics/17164 - JavaScript 的对象本质(更适合有 java、c++、c# 等背景的)
js里所有对象的原型链的最顶端对象都是Object.prototype, 也就是说, 所有js对象都具有Object.prototype对象里的属性和方法, 参考 Object.prototype