封装

一 封装

1.1 prototype对象

1.1.1 构造函数的缺点

JavaScript通过构造函数生成新对象,因此构造函数可以视为对象的模板。实例对象的属性和方法,可以定义在构造函数内部。所有实例对象都会生成相同的属性。

但是,这样做是对系统资源的浪费,因为同一个构造函数的对象实例之间,无法共享属性和方法。所有创建的对象的方法功能是相同的,完全可以只定义一份。

1.1.2 prototype属性的作用

构造函数也有自己的属性和方法,其中有一个prototype属性指向另一个对象,一般称为prototype对象。该对象非常特别,只要定义在它上面的属性和方法,能被所有实例对象共享。也就是说,构造函数生成实例对象时,自动为实例对象分配了一个prototype属性。

  • 定义在prototype上面的属性和方法,能被所有实例对象共享
1
2
3
4
5
6
7
8
9
10
11
function Animal (name) {
this.name = name;
}
Animal.prototype.color = "white";
var cat1 = new Animal('大毛');
var cat2 = new Animal('二毛');
cat1.color // 'white'
cat2.color // 'white'
  • 修改prototype对象,变动就立刻会体现在实例对象
1
2
3
4
Animal.prototype.color = "yellow";
cat1.color // 'yellow'
cat2.color // 'yellow'
  • 当实例对象本身没有某个属性或方法的时候,它会到构造函数的prototype对象去寻找该属性或方法
  • 如果实例对象自身就有某个属性或方法,它就不会再去prototype对象寻找这个属性或方法
1
2
3
4
cat1.color = 'black';
cat2.color // 'yellow'
Animal.prototype.color // "yellow";

1.1.3 原型链

所有对象都有prototype原型对象

由于JavaScript的所有对象都有构造函数,而所有构造函数都有prototype属性(其实是所有函数都有prototype属性),所以所有对象都有自己的prototype原型对象。

JavaScript的所有对象,都有自己的继承链。每个对象都继承另一个对象,该对象称为“原型”(prototype)对象。只有null除外,它没有自己的原型对象。

一个对象的属性和方法,有可能是定义它自身上面,也有可能定义在它的原型对象上面。由于原型本身也是对象,又有自己的原型,所以形成了一条原型链(prototype chain)。

  • “原型链”的作用

当读取对象的某个属性时,JavaScript引擎先寻找对象本身的属性,如果找不到,就到它的原型去找,如果还是找不到,就到原型的原型去找。以此类推,如果直到最顶层的Object.prototype还是找不到,则返回undefined。

举例

如果让某个函数的prototype属性指向一个数组,就意味着该函数可以用作数组的构造函数,因为它生成的实例对象都可以通过prototype属性调用数组方法。

1
2
3
4
5
6
7
8
9
10
function MyArray (){}
MyArray.prototype = new Array();
MyArray.prototype.constructor = MyArray;
var mine = new MyArray();
mine.push(1, 2, 3);
mine.length // 3
mine instanceof Array // true
  • 如果实例对象自身就有某个属性或方法,就不会再去prototype对象寻找
1
2
3
4
cat1.color = 'black';
cat2.color // 'yellow'
Animal.prototype.color // "yellow";

1.1.4 constructor属性

  • prototype对象有一个constructor属性,默认指向prototype对象所在的构造函数
1
2
3
4
function P() {}
P.prototype.constructor === P
// true
  • 由于constructor属性定义在prototype对象上面,意味着可以被所有实例对象继承
1
2
3
4
5
6
7
8
9
10
11
12
function P() {}
var p = new P();
p.constructor
// function P() {}
p.constructor === P.prototype.constructor
// true
p.hasOwnProperty('constructor')
// false

上面代码表示p是构造函数P的实例对象,但是p自身没有contructor属性,该属性其实是读取原型链上面的P.prototype.constructor属性。

  • constructor属性的作用是分辨prototype对象到底定义在哪个构造函数上面
1
2
3
4
5
6
function F(){};
var f = new F();
f.constructor === F // true
f.constructor === RegExp // false

1.2 Object.getPrototypeOf方法

Object.getPrototypeOf方法返回一个对象的原型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 空对象的原型是Object.prototype
Object.getPrototypeOf({}) === Object.prototype
// true
// 函数的原型是Function.prototype
function f() {}
Object.getPrototypeOf(f) === Function.prototype
// true
// 假定F为构造函数,f为F的实例对象
// 那么,f的原型是F.prototype
var f = new F();
Object.getPrototypeOf(f) === F.prototype
// true

1.3Object.create

Object.create方法用于生成新的对象,可以替代new命令。它接受一个对象作为参数,返回一个新对象,后者完全继承前者的属性,即前者成为后者的原型。

下面三种方式生成的新对象是等价的

1
2
3
var o1 = Object.create({});
var o2 = Object.create(Object.prototype);
var o3 = new Object();

如果想要生成一个不继承任何属性(比如toString和valueOf方法)的对象,可以将Object.create的参数设为null。

1
2
3
4
var o = Object.create(null);
o.valueOf()
// TypeError: Object [object Object] has no method 'valueOf'

修改对象原型会影响到新生成的对象

1
2
3
4
5
6
var o1 = { p: 1 };
var o2 = Object.create(o1);
o1.p = 2;
o2.p
// 2

1.4 isPrototypeOf方法

isPrototypeOf方法用来判断一个对象是否是另一个对象的原型

1
2
3
4
5
6
var o1 = {};
var o2 = Object.create(o1);
var o3 = Object.create(o2);
o2.isPrototypeOf(o3) // true
o1.isPrototypeOf(o3) // true

上面代码表明,只要某个对象处在原型链上,isProtypeOf都返回true。

二 继承

2.1 构造函数的继承

2.1.1 整体继承

1
2
3
4
5
6
7
8
9
10
function Shape() {
this.x = 0;
this.y = 0;
}
Shape.prototype.move = function (x, y) {
this.x += x;
this.y += y;
console.info('Shape moved.');
};

Rectangle构造函数继承Shape

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Rectangle() {
Shape.call(this); // 调用父类构造函数
}
// 另一种写法
function Rectangle() {
this.base = Shape;
this.base();
}
// 子类继承父类的方法
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;
var rect = new Rectangle();
rect instanceof Rectangle // true
rect instanceof Shape // true
rect.move(1, 1) // 'Shape moved.'

上面代码表示,构造函数的继承分成两部分

  • 子类调用父类的构造方法,
  • 子类的原型指向父类的原型。

2.1.2 部分继承

有时,只需要单个方法的继承,这时可以采用下面的写法。

1
2
3
4
ClassB.prototype.print = function() {
ClassA.prototype.print.call(this);
// some code
}

上面代码中,子类Bprint方法先调用父类Aprint方法,再部署自己的代码。这就等于继承了父类Aprint方法。

2.2 __proto__属性

__proto__属性指向当前对象的原型对象,即构造函数的prototype属性。

1
2
3
4
5
6
var obj = new Object();
obj.__proto__ === Object.prototype
// true
obj.__proto__ === obj.constructor.prototype
// true

因此,获取实例对象obj的原型对象,有三种方法:

  • obj.__proto__
  • obj.constructor.prototype
  • Object.getPrototypeOf(obj)

上面三种方法之中,前两种都不是很可靠:

最新的ES6标准规定,__proto__属性只有浏览器才需要部署,其他环境可以不部署。而obj.constructor.prototype在手动改变原型对象时,可能会失效。

这块的深入部分待补充

非标准的__proto__属性(前后各两个下划线),可以改写某个对象的原型对象。

但是,应该尽量少用这个属性,而是用Object.getPrototypeof()Object.setPrototypeOf(),进行原型对象的读写操作。

1
2
3
4
5
var obj = {};
var p = {};
obj.__proto__ = p;
Object.getPrototypeOf(obj) === p // true

上面代码通过__proto__属性,将p对象设为obj对象的原型

1
2
3
var a = {x: 1};
var b = {__proto__: a};
b.x // 1

上面代码中,b对象通过__proto__属性,将自己的原型对象设为a对象,因此b对象可以拿到a对象的所有属性和方法。

2.3 属性的继承

2.3.1 对象的原生属性

对象本身的所有属性,可以用Object.getOwnPropertyNames方法获得。

1
2
Object.getOwnPropertyNames(Date)
// ["parse", "arguments", "UTC", "caller", "name", "prototype", "now", "length"]

对象本身的属性之中,有的是可以枚举的(enumerable),有的是不可以枚举的。只获取那些可以枚举的属性,使用Object.keys方法。

1
Object.keys(Date) // []

2.3.2 hasOwnProperty()

hasOwnProperty方法返回一个布尔值,用于判断某个属性定义在对象自身,还是定义在原型链上。

1
2
3
4
5
Date.hasOwnProperty('length')
// true
Date.hasOwnProperty('toString')
// false

hasOwnProperty方法是JavaScript之中唯一一个处理对象属性时,不会遍历原型链的方法。

2.3.3 对象的继承属性

用Object.create方法创造的对象,会继承所有原型对象的属性。

1
2
3
4
5
var proto = { p1: 123 };
var o = Object.create(proto);
o.p1 // 123
o.hasOwnProperty("p1") // false

2.3.4 获取所有属性

  • 使用 in 运算符判断一个对象是否具有某个属性(不管是自身的还是继承的)
1
2
"length" in Date // true
"toString" in Date // true
  • 获得对象的所有可枚举属性(不管是自身的还是继承的),可以使用for-in循环
1
2
3
4
5
6
7
8
9
var o1 = {p1: 123};
var o2 = Object.create(o1,{
p2: { value: "abc", enumerable: true }
});
for (p in o2) {console.info(p);}
// p2
// p1
  • 采用hasOwnProperty方法中针对对象自身的属性进行判断
1
2
3
4
5
for ( var name in object ) {
if ( object.hasOwnProperty(name) ) {
/* loop code */
}
}
  • 获得对象的所有属性(不管是自身的还是继承的,以及是否可枚举),可以使用下面的函数
1
2
3
4
5
6
7
8
9
10
function inheritedPropertyNames(obj) {
var props = {};
while(obj) {
Object.getOwnPropertyNames(obj).forEach(function(p) {
props[p] = true;
});
obj = Object.getPrototypeOf(obj);
}
return Object.getOwnPropertyNames(props);
}

用法如下:

1
2
inheritedPropertyNames(Date)
// ["caller", "constructor", "toString", "UTC", "call", "parse", "prototype", "__defineSetter__", "__lookupSetter__", "length", "arguments", "bind", "__lookupGetter__", "isPrototypeOf", "toLocaleString", "propertyIsEnumerable", "valueOf", "apply", "__defineGetter__", "name", "now", "hasOwnProperty"]

2.4 对象的拷贝

如果要拷贝一个对象,需要做到下面两件事情。

  • 确保拷贝后的对象,与原对象具有同样的prototype原型对象。
  • 确保拷贝后的对象,与原对象具有同样的属性。

下面就是根据上面两点,编写的对象拷贝的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function copyObject(orig) {
var copy = Object.create(Object.getPrototypeOf(orig));
copyOwnPropertiesFrom(copy, orig);
return copy;
}
function copyOwnPropertiesFrom(target, source) {
Object
.getOwnPropertyNames(source)
.forEach(function(propKey) {
var desc = Object.getOwnPropertyDescriptor(source, propKey);
Object.defineProperty(target, propKey, desc);
});
return target;
}