JavaScript支持面向对象(Object Oriented Programming, OOP)编程,面向对象代码的封装和实例化对象等操作都非常优雅,它和闭包结合自执行函数实现封装的方式非常不同,向高手进阶的前端同学一定要掌握。
一、对象的定义:
无序属性的集合,其属性可以包含基本值,对象或者函数。这是ECMAScript-262 给出的定义。
对象是由无数个无序的键值对(key-value)组成的集合,它的值(value)可以是基本数据类型、引用数据类型。它的键(key)是对值(value)的索引。
无序的特性,如果对比数组理解起来更方便些,数组虽然也可以保存不同类型的数据,但是数组是通过下标索引元素,元素的具体位置很重要。对象的无序性则不关心键值对在对象中的顺序,它只是通过对象的键访问具体值。
二、对象的创建:
对象字面量:
var obj = {};
对象字面量表达方式最简洁与直观。
new操作符 + Object()创建 :
var obj = new Object();
new 是一种操作符,它后面紧跟的一般都是个构造函数,如果是Object(),则创建了一个新对象。
这些都只是创建个空对象,我们当然要给它添加一些属性和方法。
通过点操作符赋值添加属性和方法:
var obj = {};
obj.value = 'local';
obj.getValue = function() {
return this.value;
}
或者这样,直接key: value 形式书写,注意键值对中间逗号分隔:
var obj = {
value: 'local',
getValue: function() {
return this.value;
}
}
对象属性的访问方式 :
对象的点操作符+属性
obj.value
或用中括号传键的字符串值访问
obj['value']
这种方式有个特性,中括号里的value可以是个变量、数组、对象或函数:
var key1 = 'key1';
var key2 = [ 'key2' ];
var key3 = { value: 'key2' };
function key4(){ return 'key4' };
var obj = {};
obj[key1] = key1;
obj[key2] = key2;
obj[key3] = key3;
obj[key4] = key4;
console.log(obj);
// {key1: "key1", key2: Array(1), [object Object]: {…}, function key4(){ return 'key4' }: ƒ}
这是个很神奇的设定,业务开发中很多场合下会用这种特性。
三、工厂模式:
有时我们想创建两个具有相同属性和方法的对象,我们知道对象是按引用传递的原因不能直接复制,于是像下面这样定义两个相同属性的对象:
var obj1 = {
value: 'value1',
getValue: function() {
return this.value
}
};
var obj2 = {
value: 'value2',
getValue: function() {
return this.value
}
}
如果定义多个这样的对象,里面重复定义过很多次相同的属性和方法,这样显然不合理。
我们可以通过工厂模式(一种设计模式)解决这个问题。
var createObject = function(value) {
var obj = new Object();
obj.value = value;
obj.getValue = function() {
return this.value;
}
return obj;
}
var obj1 = createObject('value1');
var obj2 = createObject('value2');
createObject可以看做一个模板方法,给它传特定的参数,它会通过new Object()创造出一个新的对象(实例),它解决了对象模板复用的问题。
但是这种方式有个缺陷,它不能判断实例的类型(它是由哪个模板创建的)
var createObject1 = function() { // 模板1
return new Object();
}
var createObject2 = function() { // 模板2
return new Object();
}
var obj1 = createObject1();
var obj2 = createObject2();
console.log( obj1 instanceof Object ); // true
console.log( obj2 instanceof Object ); // true
只能通过 instanceof 操作符知道它是由 Object 创建的。我们用构造函数解决这个问题。
四、构造函数:
new 操作符后面如果跟了一个普通函数,这个函数就变得不普通,它称为了构造函数。
function Person(name){
this.name = name;
this.getName = function() {
return this.name;
}
};
var p1 = new Person('sonic');
var p2 = new Person('water');
console.log(p1.getName()); // 'sonic'
console.log(p2.getName()); // 'water'
console.log(p1 instanceof Person);
new 操作符改变了构造函数内部 this 的指向,它是怎么影响内部 this 指向的,后面会有文章详解。
通过 instanceof 可以判断实例是以Person为模板被创建出来的,即知道了这个实例的类型。
现在又出现了新的问题,每创建一个实例对象,这个实例的getName方法都被分配了新的内存空间。
console.log( p1.getName === p2.getName ); // false
如果实例化了100个对象,同一个逻辑的getName方法就被分配了100个内存地址。这种消耗是没必要的,我们只需用到getName方法本身的逻辑。构造函数的原型属性可以解决构造函数中方法的复用问题。顺带一提,工厂模式也不能解决这个问题。
五、原型:
JS给函数 提供了prototype属性,这个属性指向一个对象,这个对象,我们称为原型。
只有当一个函数被当作构造函数时,在这个构造函数的prototype属性上挂载方法才有意义。
function Person(name){
this.name = name;
};
Person.prototype.getName = function() {
return this.name;
}
var p1 = new Person('sonic');
var p2 = new Person('water');
console.log(p1.getName()); // 'sonic'
console.log(p2.getName()); // 'water'
console.log( p1.getName === p2.getName ); // true
如果构造函数 Person 内部没有定义 getName() 方法,实例会去构造函数 Person 的 prototype 属性上找 getName() 方法。
在 prototype 属性上定义的方法会是所有实例共用的方法,它解决的正是我们创建无数个实例时,给方法节省内存分配的问题。
结语:面向对象的内容很多,本文先介绍对象的一些相关内容,后面会相继介绍原型、原型链、构造属性、对象的继承与封装等。