JavaScript学习(十) —— OOP之对象

266 阅读4分钟

JavaScript支持面向对象(Object Oriented Programming, OOP)编程,面向对象代码的封装和实例化对象等操作都非常优雅,它和闭包结合自执行函数实现封装的方式非常不同,向高手进阶的前端同学一定要掌握。

image.png

一、对象的定义:

无序属性的集合,其属性可以包含基本值,对象或者函数。这是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 属性上定义的方法会是所有实例共用的方法,它解决的正是我们创建无数个实例时,给方法节省内存分配的问题。

结语:面向对象的内容很多,本文先介绍对象的一些相关内容,后面会相继介绍原型、原型链、构造属性、对象的继承与封装等。