我一直很难理解Javascript语言的继承机制。
它没有"子类"和"父类"的概念,也没有"类"(class)和"实例"(instance)的区分,全靠一种很奇特的"原型链"(prototype chain)模式,来实现继承。
我花了很多时间,学习这个部分,还做了很多笔记。但是都属于强行记忆,无法从根本上理解。
直到昨天,我读到法国程序员Vjeux的解释,才恍然大悟,完全明白了Javascript为什么这样设计。
下面,我尝试用自己的语言,来解释它的设计思想。彻底说明白prototype对象到底是怎么回事。其实根本就没那么复杂,真相非常简单。
一、从古代说起
要理解Javascript的设计思想,必须从它的诞生说起。
1994年,网景公司(Netscape)发布了Navigator浏览器0.9版。这是历史上第一个比较成熟的网络浏览器,轰动一时。但是,这个版本的浏览器只能用来浏览,不具备与访问者互动的能力。比如,如果网页上有一栏"用户名"要求填写,浏览器就无法判断访问者是否真的填写了,只有让服务器端判断。如果没有填写,服务器端就返回错误,要求用户重新填写,这太浪费时间和服务器资源了。
因此,网景公司急需一种网页脚本语言,使得浏览器可以与网页互动。工程师Brendan Eich负责开发这种新语言。他觉得,没必要设计得很复杂,这种语言只要能够完成一些简单操作就够了,比如判断用户有没有填写表单。
1994年正是面向对象编程(object-oriented programming)最兴盛的时期,C++是当时最流行的语言,而Java语言的1.0版即将于第二年推出,Sun公司正在大肆造势。
Brendan Eich无疑受到了影响,Javascript里面所有的数据类型都是对象(object),这一点与Java非常相似。但是,他随即就遇到了一个难题,到底要不要设计"继承"机制呢?
二、Brendan Eich的选择
如果真的是一种简易的脚本语言,其实不需要有"继承"机制。但是,Javascript里面都是对象,必须有一种机制,将所有对象联系起来。所以,Brendan Eich最后还是设计了"继承"。
但是,他不打算引入"类"(class)的概念,因为一旦有了"类",Javascript就是一种完整的面向对象编程语言了,这好像有点太正式了,而且增加了初学者的入门难度。
他考虑到,C++和Java语言都使用new命令,生成实例。
C++的写法是:
ClassName *object = new ClassName(param);
Java的写法是:
Foo foo = new Foo();
因此,他就把new命令引入了Javascript,用来从原型对象生成一个实例对象。但是,Javascript没有"类",怎么来表示原型对象呢?
这时,他想到C++和Java使用new命令时,都会调用"类"的构造函数(constructor)。他就做了一个简化的设计,在Javascript语言中,new命令后面跟的不是类,而是构造函数。
举例来说,现在有一个叫做DOG的构造函数,表示狗对象的原型。
function DOG(name){
this.name = name;
}
对这个构造函数使用new,就会生成一个狗对象的实例。
var dogA = new DOG('大毛');
alert(dogA.name); // 大毛
注意构造函数中的this关键字,它就代表了新创建的实例对象。
三、new运算符的缺点
用构造函数生成实例对象,有一个缺点,那就是无法共享属性和方法。
比如,在DOG对象的构造函数中,设置一个实例对象的共有属性species。
function DOG(name){
this.name = name;
this.species = '犬科';
}
然后,生成两个实例对象:
var dogA = new DOG('大毛');
var dogB = new DOG('二毛');
이 두 개체의 종 속성은 독립적입니다. 둘 중 하나를 수정해도 다른 개체에는 영향을 미치지 않습니다.
dogA.species = '고양이';
alert(dogB.species); // dogA의 영향을 받지 않는 "canine" 표시
각 인스턴스 객체에는 고유한 속성 및 메서드 복사본이 있습니다. 이는 데이터 공유에 실패할 뿐만 아니라 막대한 자원 낭비이기도 합니다.
4. 프로토타입 속성 소개
이를 염두에 두고 Brendan Eich는 생성자의 프로토타입 속성을 설정하기로 결정했습니다.
이 속성에는 객체(이하 "프로토타입 객체"라고 함)가 포함되어 있습니다. 인스턴스 객체에서 공유해야 하는 모든 속성과 메서드는 공유할 필요가 없는 속성과 메서드에 배치됩니다. 생성자에서.
인스턴스 객체가 생성되면 프로토타입 객체의 속성과 메서드를 자동으로 참조합니다. 즉, 인스턴스 객체의 속성과 메서드는 두 가지 유형으로 나누어집니다. 하나는 로컬이고 다른 하나는 참조입니다.
DOG 생성자를 예로 들어 이제 프로토타입 속성으로 다시 작성합니다.
기능 DOG(이름){
this.name = 이름;
}
DOG.prototype = { 종 : '개과' };
var dogA = new DOG('큰 머리');var dogB = new DOG('이毛');
alert(dogA.species) // 개alert(dogB.species); // 개
이제 종 속성은 프로토타입 객체에 배치되고 두 인스턴스 객체에서 공유됩니다. 프로토타입 객체가 수정되는 한 두 인스턴스 객체 모두에 영향을 미칩니다.
DOG.prototype.species = '고양이'
alert(dogA.species) // 고양이
alert(dogB.species); // 고양이
5. 요약
모든 인스턴스 객체는 동일한 프로토타입 객체를 공유하므로 외부에서 보면 프로토타입 객체는 인스턴스 객체의 프로토타입인 것처럼 보이고, 인스턴스 객체는 프로토타입 객체를 "상속"하는 것처럼 보입니다.
이것이 자바스크립트 상속 메커니즘의 설계 아이디어입니다. 제가 명확하게 설명했는지는 모르겠지만, 상속 메커니즘의 구체적인 적용 방법은 제가 쓴 일련의 기사를 참조하세요