简介
基本上 JavaScript 里的任何东西都是对象。
JavaScript 具有面向对象(object-oriented)的特性,在 JavaScript 中,大多数事物都是对象,从基本的字符串和数组,到建立在 JavaScript 之上的浏览器 API。
在 JavaScript 中,有许多标准内置对象(也称作内置对象或全局对象),比如:Number、String、RegExp、Math、Date、Array、Object、Function、Boolean、Symbol、Error等等。
对象基础
对象是一个包含相关数据和方法的集合,通常由一些变量和函数组成,我们称之为对象的属性和方法。
也就是说,对象由属性和方法组成。
对象中属性的值可以是字符串、数值,还可以是数组和对象等。
空对象
var obj = {};
只包含属性的对象
var obj = {
name : 'jack',
age : 20
};
只包含方法的对象
var obj = {
md : function(){
return 'this is a method';
}
};
包含属性和方法的对象
var obj = {
name : 'jack',
age : 20,
md : function(){
return 'this is a method';
}
};
// console.log( obj );
以上都是通过字面量(literal)的方式来创建一个对象,也是最常用的一种创建对象的方式。当然,对象也可以通过关键字 new 来创建。
访问对象的属性
两种方式:点语法 和 数组语法。
obj.name
或
obj['name']
但是,点语法只能接受字面量的属性的名字,不能接受变量作为名字。
调用对象的方法
obj.md()
对象无处不在
在 JavaScript 中,其实你一直在使用对象。
比如,当你直接将一个字符串赋值给一个变量。
var str = 'baidu,google';
然后,你可以使用下面的方法将字符串分割为数组。
str.split(',')
这是因为,当你使用字面量创建一个字符串时,字符串会自动的被创建为内置对象 String 的实例,因此会有一些常见的方法和属性可用。
看到这,你是不是会觉得很神奇,JavaScript 与其他的语言有很大的区别,以后慢慢分析。
构建函数和对象实例
有人认为 JavaScript 不是真正的面向对象的语言,比如它没有用于创建 class 类的声明。
JavaScript 用一种称为构建函数的特殊函数来定义对象和它们的特征。构建函数非常有用,因为很多情况下您不知道实际需要多少个对象实例。
JavaScript 从构建函数创建的新实例的特征并非全盘复制,而是通过一个叫做原形链的参考链链接过去的。
JavaScript 是通过构建函数(也叫构造函数)模拟 Class 来创建对象实例的。
一个简单的例子
通过一个普通的函数定义一个“人”:
function createNewPerson(name) {
var obj = {};
obj.name = name;
obj.greeting = function () {
alert('Hi! I\'m ' + this.name + '.');
}
return obj;
}
调用这个函数创建一个叫 jack 的人,可在浏览器的控制台进行测试。
var jack = createNewPerson('jack');
jack.greeting();
console.log(jack.name);
上述代码运行良好,但是有点冗长;如果我们知道如何创建一个对象,就没有必要创建一个新的空对象并且返回它。幸好 JavaScript 通过构建函数提供了一个便捷的方法,方法如下:
function Person(name) {
this.name = name;
this.greeting = function() {
alert('Hi! I\'m ' + this.name + '.');
};
}
【 构建函数通常是大写字母开头,这样便于区分构建函数和普通函数。 】
那如何调用构建函数创建新的实例呢?
var person1 = new Person('Bob');
var person2 = new Person('Sarah');
现在,我们已经知道了两种创建对象实例的方式:
- 字面量的方式直接声明一个对象。
- 构造函数
下面介绍其他的几种方式。
Object() 构造函数
可以使用 Object() 构造函数来创建一个对象实例。
var person1 = new Object();
这样就在 person1 变量中存储了一个空对象。然后, 可以根据需要,使用点或括号表示法向此对象添加属性和方法。
person1.name = 'Chris';
person1['age'] = 38;
person1.greeting = function() {
alert('Hi! I\'m ' + this.name + '.');
}
还可以将对象文本传递给Object() 构造函数作为参数, 以便用属性/方法填充它。
var person1 = new Object({
name : 'Chris',
age : 38,
greeting : function() {
alert('Hi! I\'m ' + this.name + '.');
}
});
create() 方法
JavaScript 有个内嵌的方法 create(),它允许您基于现有对象创建新的对象实例。
var person2 = Object.create(person1);
person2 是基于 person1 创建的, 它们具有相同的属性和方法。这非常有用, 因为它允许您创建新的对象实例而无需定义构造函数。
对象原型
通过原型这种机制,JavaScript 中的对象从其他对象继承功能特性;这种继承机制与经典的面向对象编程语言的继承机制不同。
JavaScript 常被描述为一种基于原型的语言 (prototype-based language)。
每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototype chain)。
原型链可以解释为何一个对象会拥有定义在其他对象中的属性和方法。
准确地说,这些属性和方法定义在 Object 的构造函数的 prototype 属性上,而非对象实例本身。
在传统的 OOP 中,首先定义“类”,此后创建对象实例时,类中定义的所有属性和方法都被复制到实例中。
在 JavaScript 中并不如此复制,而是在对象实例和它的构造器之间建立一个链接(它是__proto__属性,是从构造函数的prototype属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法。
注意:理解对象的原型(可以通过Object.getPrototypeOf(obj)或者已被弃用的__proto__属性获得)与构造函数的prototype属性之间的区别是很重要的。前者是每个实例上都有的属性,后者是构造函数的属性。也就是说,Object.getPrototypeOf(new Foobar())和Foobar.prototype指向着同一个对象。
以上描述很抽象,我们先看一个例子。
理解原型对象
先定义一个构造器函数:
function Person(first, last, age, gender, interests) {
this.name = {
first,
last
};
this.age = age;
this.gender = gender;
this.interests = interests;
this.bio = function() {
alert(this.name.first + ' ' + this.name.last + ' is ' + this.age + ' years old. He likes ' + this.interests[0] + ' and ' + this.interests[1] + '.');
};
this.greeting = function() {
alert('Hi! I\'m ' + this.name.first + '.');
};
};
然后创建一个实例:
var person1 = new Person('Bob', 'Smith', 32, 'male', ['music', 'skiing']);
在控制台输入 "person1.",你会看到,浏览器将根据这个对象的可用的成员名称进行自动补全。
你可以看到定义在 person1 的原型对象(即Person() 构造器)中的成员 name、age、gender、interests、bio、greeting,还可以看到一些其他成员,如 watch、valueOf 等,这些成员定义在 Person() 构造器的原型对象(即 Object 构造函数)之上。
也就是说,对象实例 person1 继承了 Person() 构造器,而 Person() 构造器又继承了 Object 构造器。这就是原型链的运作机制。
那么,调用 person1 的“实际定义在 Object 上”的方法时,会发生什么?比如:
person1.valueOf()
这个方法仅仅返回了被调用对象的值。在这个例子中发生了如下过程:
- 浏览器首先检查,person1 对象是否具有可用的 valueOf() 方法。
- 如果没有,则浏览器检查 person1 对象的原型对象(即 Person)是否具有可用的 valueof() 方法。
- 如果也没有,则浏览器检查 Person() 构造器的原型对象(即 Object)是否具有可用的 valueOf() 方法。Object 具有这个方法,于是该方法被调用。
注意: 原型链中的方法和属性没有被复制到其他对象。
没有官方的方法用于直接访问一个对象的原型对象——原型链中的“连接”被定义在一个内部属性中,在 JavaScript 语言标准中用 [[prototype]] 表示(参见 ECMAScript)。
然而,大多数浏览器还是提供了一个名为 __proto__
(前后各有2个下划线)的属性,其包含了对象的原型。你可以尝试输入 person1.__proto__
和person1.__proto__.__proto__
,看看代码中的原型链是什么样的。
prototype 属性
那么,允许被继承的属性和方法在哪儿定义呢? —— 答案就是 prototype 属性。
如果你查看 Object 参考页,会发现左侧列出许多属性和方法,大大超过了我们在 person1 对象中看到的继承成员的数量。某些属性或方法被继承了,而另一些没有,为什么呢?
原因在于,继承的属性和方法是定义在 prototype 属性中(你可以称之为子命名空间 sub namespace ),那些以 Object.prototype. 开头的属性。
prototype 属性的值是一个对象,希望被原型链下游的对象继承的属性和方法,都被储存在其中。
因此, Object.prototype.watch()、Object.prototype.valueOf() 等等成员,适用于任何继承自 Object() 的对象类型,包括使用构造器创建的新的对象实例。
而 Object.is()、Object.keys(),以及其他不在 prototype 对象内的成员,不会被“对象实例”或“继承自 Object() 的对象类型”所继承。这些方法/属性仅能被 Object() 构造器自身使用。
你可以检查已有的 prototype 属性。回到先前的例子,在 JavaScript 控制台输入:
Person.prototype
输出并不多,毕竟我们没有为自定义构造器的 prototype 定义任何成员。缺省状态下,构造器的 prototype 属性初始为空白。现在尝试:
Object.prototype
你会看到 Object 的 prototype 属性上定义了大量的方法,继承自 Object 的对象都可以使用这些方法。
JavaScript 中到处都是通过原型链继承的例子。
比如,你可以尝试从 String、Date、Number 或 Array 全局对象的 prototype 中寻找方法和属性。它们都在自己的 prototype 上定义了一些方法,因此当你创建一个字符串时:
var myString = 'This is my string.';
myString 立即具有了一些可用的方法,如 split()、indexOf()、replace() 等。
重点说明:
- prototype 属性是 JavaScript 中最容易混淆的名称之一。
- prototype 属性并不是指向当前对象的原型对象。
- 原型对象是一个内部对象,可以通过对象实例的
__proto__
属性来访问。 - prototype 属性用来定义需要被其他对象继承的成员。
以上面的例子进行说明:
- person1 是 Person 的对象实例,person1 的原型对象是 Person。
- Person 的原型对象是 Object,Person 继承了 Object。
- Object 的 prototype 属性中定义了可以被其他对象继承的成员。
create()
我们曾经讲过如何用 Object.create() 方法创建新的对象实例。
var person2 = Object.create(person1);
create() 实际做的是从指定原型对象创建一个新的对象。这里以 person1 为原型对象创建了 person2 对象。
在控制台输入:
person2.__proto__
结果返回 person1 对象。
constructor 属性
每个对象实例都具有 constructor 属性,它指向创建该实例的构造器函数。
在控制台中尝试下面的指令:
person1.constructor
person2.constructor
都将返回 Person() 构造器,因为该构造器包含这些实例的原始定义。
一个小技巧是,你可以在 constructor 属性的末尾添加一对圆括号(括号中包含所需的参数),从而用这个构造器创建另一个对象实例。毕竟构造器是一个函数,故可以通过圆括号调用;只需在前面添加 new 关键字,便能将此函数作为构造器使用。
在控制台中输入:
var person3 = new person1.constructor('Karen', 'Stephenson', 26, 'female', ['playing drums', 'mountain climbing']);
通常你不会去用这种方法创建新的实例;但如果你刚好因为某些原因没有原始构造器的引用,那么这种方法就很有用了。
此外,constructor 属性还有其他用途。比如,想要获得某个对象实例的构造器的名称,可以这么用:
person1.constructor.name
修改原型
我们可以修改构造器的 prototype 属性。
比如,给 Person 构造器的 prototype 属性添加一个新的方法:
Person.prototype.farewell = function() {
alert(this.name.first + ' has left the building. Bye for now!');
}
保存后,刷新页面,在控制台输入:
person1.farewell();
整条继承链都动态地更新了,任何由此构造器创建的对象实例都自动获得了这个方法。这很有用。
虽然我们是在实例化了 person1 之后,才给 Person 构造器的 prototype 添加了一个新方法,但新方法仍然可用于 person1 对象实例。
这证明了先前描述的原型链模型。这种继承模型下,上游对象的方法不会复制到下游的对象实例中;下游对象本身虽然没有定义这些方法,但浏览器会通过上溯原型链、从上游对象中找到它们。这种继承模型提供了一个强大而可扩展的功能系统。
一种极其常见的对象定义模式是:在构造器(函数体)中定义属性,在构造器外通过 prototype 属性定义方法。
如此,构造器只包含属性定义,而方法则分装在不同的代码块,代码更具可读性。例如:
// 构造器及其属性定义
function Test(a,b,c,d) {
// 属性定义
};
// 定义第一个方法
Test.prototype.x = function () { ... }
// 定义第二个方法
Test.prototype.y = function () { ... }
// 等等……