严格来说,JavaScript并不是一门面向对象的语言,因为它没有原生提供完整的面向对象机制。但它的语言特性又允许我们去模拟大部分这些机制。
new操作符
JavaScript中也用new操作符创建类的实例,例如:
var arr = new Array(10); // 创建长度为10的数组
var date = new Date; // 不需要传参数给构造函数时,括号可以省略
与C#不同的是,JavaScript中new的是函数而不是class,这个函数即为类的构造函数。在构造函数中,可以声明属性和方法。例如:
function Square() {
this.length = 1;
this.perimeter = function() {
return this.length * 4;
};
}
var square = new Square();
square.perimeter(); // 4
square.length = 10;
square.perimeter(); // 40
虽然上面的代码看上去很正常,但实际上存在一个缺陷:
var square1 = new Square();
var square2 = new Square();
square1.perimeter === square2.perimeter; // false
由于每次创建Square对象执行构造函数时都创建了新的函数perimeter,所以多个Square对象的perimeter并非指向同一个函数。这样会造成内存的浪费。要解决这个问题,可以使用原型链。
原型链
JavaScript中的对象都有一个内部属性__proto__(在新浏览器中已经变成可读可写的外部属性了)指向其构造函数的prototype;构造函数的prototype也是一个对象,所以它也有一个__proto__属性指向它的构造函数的prototype……如此逐层查找,直到__proto__属性的值是null为止。这就是原型链。下面以代码证实这一规则:
var arr = new Array();
arr.__proto__ === Array.prototype; // true
那么Array.prototype.__proto__又是什么呢?在控制台打印出来可以发现,是一个Object对象,所以:
Array.prototype.__proto__ === Object.prototype; // true
Object.prototype.__proto__; // null
Object类是所有类型的基类,所以它的prototype的__proto__属性为null。最后得出arr的原型链为:
- arr.__proto__
- Array.prototype
- Array.prototype.__proto__
- an Object object
- object.__proto__
- Object.prototype
- Object.prototype.__proto__
- null
原型链的用处在于:当访问对象的某个属性或方法时,如果对象本身没有这个属性或方法,就会在原型链中由浅到深进行查找(浅处优先)。同样地有代码可以证实这一规则:
var arr = new Array();
// arr本身没有push
arr.hasOwnProperty('push'); // false
// 访问arr.push时实际上访问的是Array.prototype.push。
arr.push === arr.__proto__.push; // true
arr.push === Array.prototype.push; // true
不难发现,即使有多个Array对象,它们也是共享Array.prototype上的方法。所以,借助原型链就可以解决前面提到的多个函数副本浪费内存的问题:
function Square() {
this.length = 1;
}
Square.prototype.perimeter = function() {
return this.length * 4;
};
var square1 = new Square();
var square2 = new Square();
square1.perimeter === square2.perimeter; // true
继承
原型链的层次结构跟面向对象的继承非常相似,所以原型链也可以用于模拟继承。先来看一个简单的例子:
function Shape() {
this.name = '形状';
}
Shape.prototype.perimeter = function() { };
function Square() {
this.length = 1;
}
// JavaScript中没有抽象类的概念,所以Shape是可以实例化的
Square.prototype = new Shape(); // important
Square.prototype.perimeter = function() {
return this.length * 4;
};
var square = new Square();
square.name; // '形状'
square.perimeter(); // 4
其中最为关键的就是把一个Shape对象加入到Square的原型链中去。根据原型链的规则:
- 访问square.name时,实际上访问的是Square.prototype.name,也就是那个Shape对象的name属性。
- 调用square.perimeter()时,虽然Square.prototype和Shape.prototype中都存在perimeter方法,但根据浅处优先原则,调用的是Square.prototype.perimeter。
很明显,这跟继承的效果是一样的。
封装
目前JavaScript没有任何机制可以控制属性和方法的可访问性,也就是说private和protected都无法实现。但是利用闭包的特性可以实现类似private的效果:
function Square() {
var length = 1;
this.setLength = function(len) {
length = len;
};
this.getLength = function() {
return length;
};
this.perimeter = function() {
return length * 4;
};
}
var square = new Square();
square.perimeter(); // 4
square.setLength(10);
square.perimeter(); // 40
由于length变量的作用域是在Square内,所以外部无法访问,也就达到了私有的效果。但这样一来,三个方法就不能写到Square.prototype上,因为一旦它们脱离了Square的作用域,就无法访问length了。
鉴于封装特性难以实现,实际工作中更多是通过编码规范来进行约束。例如可以规定:名字以双下划线开头的属性或方法是private的,名字以单下划线开头的属性或方法是protected的。代码示例:
function Square() {
this.__length = 1;
}
Square.prototype.setLength = function(len) {
this.__length = len;
};
Square.prototype.getLength = function() {
return this.__length;
};
Square.prototype.perimeter = function() {
return this.__length * 4;
};
var square = new Square();
square.perimeter();
square.setLength(10);
square.perimeter();
小结
最后,我们再来了解一下new操作符的大概执行过程:
- 创建一个新的空对象;
- 设置新对象的__proto__属性为构造函数的prototype;
- 以新对象为this执行构造函数。
如果以代码表示,则为:
// 以new Foo()为例
var obj = { };
obj.__proto__ = Foo.prototype;
Foo.call(obj);
大家不妨以此过程回顾前面的代码段。
本篇到此结束,下一篇将会讲述在JavaScript中模拟面向对象的其他细节问题。
评论 (2条)