理解JavaScript面向对象的程序设计

  ECMA-262把对象定义为:“无序属性的集合,其属性可以包含基本值、对象或者函数。”严格来讲,可以说对象是一组没有特定顺序的值。可以把ECMAScript的对象想象成散列表——一组名值对,其中值可以是数据或者函数。每个对象都是基于一个引用类型创建的,这个类型可以是原生类型,也可以是开发人员定义的类型。

  本章主要目标:

  • 理解对象属性
  • 理解并创建对象
  • 理解继承

理解对象

属性类型

  ECMA-262第5版在定义只有内部才用的特性(attribute)时,描述了属性(property)的各种特征,在JavaScript中不能直接访问它们,为了表示为内部值,该规范把它们放到了两对方括号中。

  ECMAScript只有两种属性:数据属性和访问器属性。

  1. 数据属性
    数据属性包含一个数据值的位置,在这个位置可以读写值。
    • [[Configurable]]
      默认为true,表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。
    • [[Enumerable]]
      默认为true,表示能否通过for-in循环返回属性。
    • [[Writable]]
      默认为true,表示能否修改属性的值
    • [[Value]]
      默认为undefined,包含这个属性的数据值。
  2. 访问器属性
    访问器属性不包含数据值, 包含一对getter和setter函数(非必需)。
    • [[Configurable]]
      默认为true,表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。
    • [[Enumerable]]
      默认为true,表示能否通过for-in循环返回属性。
    • [[Get]]
      默认为undefined,在读取属性时调用的函数。
    • [[Set]]
      默认为undefined,在写入属性时调用的函数。

定义多个属性

  修改默认属性:调用Object.defineProperty()方法

  定义多个属性:调用Object.defineProperties()方法

读取属性的特性

  读取属性的特性:调用Object.getOwnPropertyDescription()方法

创建对象

Object构造函数

1
2
3
4
5
6
var dog = new Object();
dog.name = "Tom";
dog.age = 4;
dog.say() = function() {
alert(this.mame);
};

对象字面量

1
2
3
4
5
6
7
var dog = {
name: "Tom",
age: 4,
say: function() {
alert(this.name);
}
};

工厂模式

  若要创建多个对象前两种会产生很多重复代码,所以使用工厂模式,抽象创建具体对象的过程。但是工厂模式无法解决对象识别问题。

1
2
3
4
5
6
7
8
9
10
11
12
function dog(name, age) {
var o = new Object();
o.name = "Tom";
o.age = 4;
o.say() = function() {
alert(this.name);
};
return o;
}

var dog1 = dog("Tom", 4);
var dog2 = dog("John", 2);

构造函数模式

  构造函数模式创建类似Object,Array一样的构造函数,可以使用new操作符创建对象。但是构造函数模式会创建重复的function。

1
2
3
4
5
6
7
8
9
10
function Dog(name, age) {
this.name = name;
this.age = age;
this.say = function() {
alert(this.name);
};
}

var dog1 = new Dog("Tom", 4);
var dog2 = new Dog("John", 2);

原型模式

  每个函数在创建时都有一个prototype属性,指向原型对象,原型对象包含可以由特定类型的所欲实例共享的属性和方法。但是原型模式省略了传入参数初始化,一定程度上不方便,并且,对于包含引用值的属性,共享会造成麻烦。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Dog() {
}

Dog.prototype.name = "Tom";
Dog.prototype.age = 4;
Dog.prototype.say() = function() {
alert(this.name);
};

var dog1 = new Dog();
var dog2 = new Dog();

dog1.say(); // Tom
dog2.say(); // Tom

alert(dog1.say === dog2.say); // true

  简化写法:

1
2
3
4
5
6
7
8
9
10
11
12
function Dog() {
}

Dog.prototype = {
// 使用对象字面量创建Dog.prototype后构造器属性指向Object,需要手动修改为Dog。
constructor: Dog,
name: "Tom",
age: 4,
say: function() {
alert(this.name);
}
};

组合使用构造函数模式和原型模式

  集两个方式的长处,既能通过构造函数传入参数,也能有共享的属性和方法。这种模式在ECMAScript中使用最广泛、认识度最高。可以说是默认的一种方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Dog(name, age) {
this.name = name;
this.age = age;
}

Dog.prototype = {
constructor: Dog,
say: function() {
alert(this.name);
}
};

var dog1 = new Dog("Tom", 4);
var dog2 = new Dog("John", 2);

动态原型模式

  把所有信息都封装在构造函数中,可以通过检查某个方法是否有效来决定是否需要初始化原型。

1
2
3
4
5
6
7
8
9
10
function Dog(name, age) {
this.name = name;
this.age = age;

if(typeof this.say != "function") {
Dog.prototype.say = function() {
alert(this.name);
};
}
}

寄生构造函数模式

  在前集中模式都不适用的情况下,可以适用寄生构造函数模式。基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后在返回新创建的对象。从表面上看很想是典型的构造函数。这个模式可以在特殊情况下用来为对象创建构造函数。寄生构造函数模式返回的对象与构造函数或者与构造函数的原型属性之间没有关系,在可以使用其他模式的情况下尽量不要使用此模式。

稳妥构造函数模式

  道格拉斯·克罗克福德发明了JavaScript中的稳妥对象(durable objects)这个概念。稳妥对象指的是没有公共属性,其方法不引用this对象,适合用于一些安全的环境中或者在方式数据被其他应用程序改动时使用。稳妥构造函数遵循与寄生构造函数类似的模式,但是不使用this和new。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Dog(name, age) {
var o = new Object();

///////////////////
//定义私有变量和函数//
///////////////////

o.say = function() {
alert(name);
};
}

var dog1 = Dog("Tom", 4);
alert(dog1.name); // undefined
dog1.say(); // Tom

  除了say()方法,其他方式无法访问到name属性。

继承

  由于函数没有签名,在ECMAScript中不支持接口继承,只支持实现继承。其实现继承的主要方法是依靠原型链实现。

原型链

  原型链是ECMAScript实现继承的主要方法,其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。

  原型链实现的基本方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
"use strict"

function Father() { // 父类
this.name = "father";
}

Father.prototype.getName = function() { // 父类原型的方法
return this.name
};

function Sun() { // 子类
this.age = 4;
}
Sun.prototype = new Father(); // 子类继承父类

Sun.prototype.getAge = function() { // 子类原型的方法
return this.age;
};

var example = new Sun(); // 子类实例
console.log(example.getName()); // 输出father,说明子类继承了父类的方法

  所有的引用类型都继承了Object,所以完整的原型链应该包括Object层。除此外,原型链实现继承还有两个问题:一是包含引用类型的原型,而是不能向超类的构造函数中传递参数。所以很少会单独使用原型链。

  引用类型的问题,可见所有子类实例的colors共享了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
"use strict"

function Father() {
this.colors = ["red", "blue", "green"];
}

function Sun() {
}

Sun.prototype = new Father();

var sun = new Sun();
sun.colors.push("black");
console.log(sun.colors); // ["red", "blue", "green", "black"]

var sun2 = new Sun();
console.log(sun2.colors); // ["red", "blue", "green", "black"]

借用构造函数

  借用构造函数(又叫伪造对象或者经典继承)解决了原型中包含引用类型值所带来的问题。其基本思想为在子类的构造函数内部调用超类的构造函数,使用apply()或call()方法在新创建的对象上执行构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
"use strict"

function Father() {
this.colors = ["red", "blue", "green"];
}

function Sun() {
Father.call(this); // 继承父类
}

var sun = new Sun();
sun.colors.push("black");
console.log(sun.colors); // ["red", "blue", "green", "black"]

var sun2 = new Sun();
console.log(sun2.colors) // ["red", "blue", "green"],可见解决了共享引用类型的问题

  借用构造函数也解决了原型链向父类传递参数的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
"use strict"

function Father(name) {
this.name = name;
}

function Sun() {
Father.call(this, "Tom"); // 调用父类构造函数,向父类传递参数

this.age = 18;
}

Sun.prototype = new Father();

var sun = new Sun();
console.log(sun.name); // Tom
console.log(sun.age); // 18

  借用构造函数也无法避免构造函数模式存在的问题,方法在构造函数中导致无法函数复用,因此借用构造函数也很少单独使用。

组合继承

  组合继承,又叫伪经典继承,指将原型链和借用构造函数结合到一块,从而发挥二者之长。其基本思路是使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承,这样既能在原型上定义方法实现方法复用,又能保证每个实例都有自己的属性。

原型式继承

  道格拉斯·克罗克福德在2006年写的一篇文章中介绍了一种实现继承的方法,这种方法并没有使用严格意义上的构造函数,这种方法的基本思想是借助原型可以基于已有的对象创建新对象,同事还不必因此创建自定义类型。

1
2
3
4
5
function object(o) {
function F() {}
F.prototype = o;
return new F();
}

寄生式继承

  寄生式继承与原型式继承思路类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。

1
2
3
4
5
6
7
function createAnother(original) {
var clone = object(original);
clone.sayHi = function() {
alert("hi");
};
return clone;
}

寄生组合式继承

  所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。

1
2
3
4
5
function inheritPrototype(subType, superType) {
var prototype = object(superType.prototype);
prototype.constructor = subType;
subType.prototype = prototype;
}

总结

  JavaScript主要通过原型链实现继承,理解JavaScript的原型链,就能理解JavaScript的各种继承的实现方式。

打赏点猫粮钱吧~