深入理解JavaScript中的面向对象(二)

Heero.Luo发表于9年前,已被查看2385次

严格来说,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操作符的大概执行过程:

  1. 创建一个新的空对象;
  2. 设置新对象的__proto__属性为构造函数的prototype;
  3. 以新对象为this执行构造函数。

如果以代码表示,则为:

// 以new Foo()为例
var obj = { };
obj.__proto__ = Foo.prototype;
Foo.call(obj);

大家不妨以此过程回顾前面的代码段。

 

本篇到此结束,下一篇将会讲述在JavaScript中模拟面向对象的其他细节问题。

评论 (2条)

发表评论

(必填)

(选填,不公开)

(选填,不公开)

(必填)