类(class):类是对拥有一样属性(property)和行为的一系列对象(object)的抽象。 这里说的“行为”,在基于类的面向对象的语言中一般叫作类的方法(method)。而在 JavaScript 里,函数也是“一等公民”,能够被直接赋值给一个变量或一个对象的属性,所以在本文后续的讨论中,把“行为”也纳入“属性”的范畴。javascript
JavaScript 规定每个对象均可以有一个原型([[prototype]] 内部属性)。(在实现 ECMAScript 5.1 规范之前,除了 Object.prototype 之外的对象都必须有一个原型。)每一个对象都“共享”其原型的属性:在访问一个对象的属性时,若是该对象自己没有这个属性,则 JavaScript 会继续试图访问其原型的属性。这样,就能够经过指定一些对象的原型来使这些对象都拥有一样的属性。从而咱们能够这样认为,在 JavaScript 中,以同一个对象为原型的对象就是属于同一个类的对象。java
那么 JavaScript 中的对象与其原型是怎样被关联起来的呢?或者说,JavaScript 中的对象的原型是怎样被指定的呢?数组
JavaScript 有一个 new 操做符(operator),它基于一个函数来建立对象。这个用 new 操做符建立出来的对象的原型就是 new 操做符后面的函数(称为“构造函数”)的 prototype 属性。例如:闭包
var obj = {"key": 1}; function fun() {} fun.prototype = obj; var a = new fun();
此时 fun 对象的原型就是 obj 对象。app
Object.create 方法直接以给定的对象做为原型建立对象。一个代码例子:ide
var a = {"aa": 1}; var b = Object.create(a);
此时 b 对象的原型就是 a 对象。函数
new 操做符和 Object.create 方法都是在建立一个对象的同时就指定其原型。而 Object.setPrototypeOf 方法则是指定一个已被建立的对象的原型。代码例子:this
var a = {"aa": 1}; var b = Object.create(a); // 此时 b 的原型是 a var c = {"cc": 2}; Object.setPrototypeOf(b, c); // 此时 b 的原型变为 c 了
数字、布尔值、字符串、数组和函数在 JavaScript 中也是对象,而它们的原型是被 JavaScript 隐式指定的:prototype
下面给出定义一个类的一段 JavaScript 代码的示例。它定义一个名为 Person 的类,它的构造函数接受一个字符串的名称,还一个方法 introduceSelf 会输出本身的名字。code
// ----==== 类定义开始 ====---- function Person(name) { this.name = name; } Person.prototype.introduceSelf = function () { console.log("My name is " + this.name); }; // ----==== 类定义结束 ====---- // 下面实例化一个 Person 类的对象 var someone = new Person("Tom"); // 此时 someone 的原型为 Person.prototype someone.introduceSelf(); // 输出 My name is Tom
若是转换为 ECMAScript 6 引入的类声明(class declaration)语法,则上述 Person 类的定义等同于:
class Person { constructor(name) { this.name = name; } introduceSelf() { console.log("My name is " + this.name); } }
在上面的例子中,假如咱们不经过 Person.prototype 来定义 introduceSelf 方法,而是在构造函数中给对象指定一个 introduceSelf 属性:
function Person(name) { this.name = name; this.introduceSelf = function () { console.log("My name is " + this.name); }; } var someone = new Person("Tom"); someone.introduceSelf(); // 也会输出 My name is Tom
虽然这种方法中,经过 Person 构造函数 new 出来的对象也都有 introduceSelf 属性,但这里 introduceSelf 变成了 someone 自身的一个属性而不是 Person 类的共有的属性:
function Person1(name) { this.name = name; } Person1.prototype.introduceSelf = function () { console.log("My name is " + this.name); }; var a = new Person1("Tom"); var b = new Person1("Jerry"); console.log(a.introduceSelf === b.introduceSelf); // 输出 true delete a.introduceSelf; a.introduceSelf(); // 仍然会输出 My name is Tom,由于 introduceSelf 不是 a 自身的属性,不会被 delete 删除 b.introduceSelf = function () { console.log("I am a pig"); }; Person1.prototype.introduceSelf.call(b); // 输出 My name is Jerry // 即便 b 的 introduceSelf 属性被覆盖,咱们仍然能够经过 `Person1.prototype` 来让 b 执行 Person1 类规定的行为。
function Person2(name) { this.name = name; this.introduceSelf = function () { console.log("My name is " + this.name); }; } a = new Person2("Tom"); b = new Person2("Jerry"); console.log(a.introduceSelf === b.introduceSelf); // 输出 false // a 的 introduceSelf 属性与 b 的 introduceSelf 属性是不一样的对象,分别占用不一样的内存空间。 // 所以这种方法会形成内存空间的浪费。 delete a.introduceSelf; a.introduceSelf(); // 会抛 TypeError b.introduceSelf = function () { console.log("I am a pig"); }; // 此时 b 的行为已经与 Person2 类规定的脱节,对象 a 和对象 b 看起来已经不像是同一个类的对象了
可是这种方法也不是一无可取。例如咱们须要利用闭包来实现对 name 属性的封装时:
function Person(name) { this.introduceSelf = function () { console.log("My name is " + name); }; } var someone = new Person("Tom"); someone.name = "Jerry"; someone.introduceSelf(); // 输出 My name is Tom // introduceSelf 实际用到的 name 属性已经被封装起来,在 Person 构造函数之外的地方没法访问 // name 至关于 Person 类的一个私有(private)成员属性
类的继承实际上只须要实现:
实现第 2 点的方式比较直观。而怎样实现第 1 点呢?其实咱们只须要让子类的构造函数的 prototype 属性 (子类的实例对象的原型) 的原型是父类的构造函数的 prototype 属性 (父类的实例对象的原型),简而言之就是:把父类实例的原型做为子类实例的原型的原型。这样在访问子类的实例对象的属性时,JavaScript 会沿着原型链找到子类规定的成员属性,再找到父类规定的成员属性。并且子类可在子类构造函数的 prototype 属性中重载(override)父类的成员属性。
下面给出一个代码示例,定义一个 ChinesePerson 类继承上文中定义的 Person 类:
function ChinesePerson(name) { Person.apply(this, name); // 调用父类的构造函数 } ChinesePerson.prototype.greet = function (other) { console.log(other + "你好"); }; Object.setPrototypeOf(ChinesePerson.prototype, Person.prototype); // 将 Person.prototype 设为 ChinesePerson.prototype 的原型 var someone = new ChinesePerson("张三"); someone.introduceSelf(); // 输出“My name is 张三” someone.greet("李四"); // 输出“李四你好”
上述定义 ChinesePerson 类的代码改用 ECMAScript 6 的类声明语法的话,就变成:
class ChinesePerson extends Person { constructor(name) { super(name); } greet(other) { console.log(other + "你好"); } }
你会不会以为上面代码示例中,introduceSelf 输出半英文半中文挺别扭的?那咱们让 ChinesePerson 类重载 introduceSelf 方法就行了:
ChinesePerson.prototype.introduceSelf = function () { console.log("我叫" + this.name); }; var someone = new ChinesePerson("张三"); someone.introduceSelf(); // 输出“我叫张三” var other = new Person("Ba Wang"); other.introduceSelf(); // 输出 My name is Ba Wang // ChinesePerson 的重载并不会影响父类的实例对象