JAVASCRIPT OBJECTS

ECMAscript 说明文档对这门语言的定义是“一门适于在宿主环境中执行计算及操做计算对象的面向对象的编程语言”。简单的说,JavaScript是一门面向对象(OO)的语言。前端

面向对象讲究的是专一于对象自己——它们的结构,它们互相间是如何影响的。本文是@堂主 对《Pro JavaScript with Mootools》一书的第三章 Object 部分的翻译,最先译于 2012 年。由于面向对象编程自己已经超出了本书的叙述范围,因此咱们在本章所谈的只是 JavsScript 自身在面向对象方面的那些特色。web

本篇译文字数约 3 万字,各位看官如发现翻译错误或有优化建议,欢迎留言指教,共同成长。另外,一样的建议——非本土产技术类书籍,建议仍是优先阅读英文原版。编程

JavaScript是基于原型的 JavaScript is Prototypal(-ish)

全部面向对象的语言在其核心都会对对象进行处理,对象的建立及构造的过程将大部分的面向对象语言分为2个阵营:数组

  1. 基于类 (Classical or class-based) 的面向对象语言采用类来建立对象。类是一个为建立对象提供蓝本的特殊数据类型。在一个基于类的面向对象的语言中,咱们经过建立类来定义一个对象的结构,并经过建立该类的实例来创造这个对象自己。这一过程被称为实例化 (instantiation)。
  2. 基于原型 (Prototypal or prototype-based) 的面向对象语言没有类的概念,它以其余的对象对蓝本。在一个基于原型的语言中,prototype 是一个由你建立、体现着你指望的结构的对象,这个对象以后会成为其余对象建立所参照的蓝本。经过拷贝其自己 prototype 属性来建立实例的方式被称为克隆(cloning)。对一个纯粹的原型语言而言,任何一个对象都能被做为建立其余对象的原型。

JavaScript 是一本基于原型的语言:这里没有类的概念,全部对象都是由其余对象建立而来。不过,JavaScript 不是一门纯粹的原型语言,在本章的后面咱们会看到 JavaScript 还保留着一些基于类的残存特征。若是你已经对面向对象的语言很熟悉了,你极可能会以为 JavaScript 是奇异的,由于相对你以前的那些面向对象的经验,这门语言的怪异特质是如此明显。app

哈哈,先别打退堂鼓:JavaScript,一门面向对象的语言,由于兼备了基于类和原型的特征,使得它具有了处理复杂、庞大应用的实力。编程语言

一门关于对象的语言 (A Language of Objects)

从本质上讲,一个 JavaScript 的对象就是一些名值对(key-value pairs)的聚合体。相比于简单的如字符串、数字等基本数据类型而言,JavaScript 对象是一种混合的复合数据类型。对象内的每个名值对被称为一个属性(property),key 被称为属性名(property name),value 被称为属性值(property value)函数

属性名一贯是字符串,而属性值则多是任何数据类型:字符串、数字、布尔值或者是复合型的数据类型如数组、函数或对象。尽管 JavaScript 并未将对象属性值可承载的数据类型作任何区分,但咱们仍是习惯的将用函数类型做为值的属性称为方法(methods)以与其余值为非函数类型的属性做区分。为了不困惑,在后面的探讨中咱们采用以下的惯例:以函数为值的属性称之为“方法”,其余的统称为“属性”。若是咱们所指的同时可能为一个对象的方法或属性,那咱们会称它们为这个对象的成员(members)学习

注意:在面对 JavaScript 是一门一等对象语言这个现实时,属性和方法间的区分会显得不那么清晰。本章的观点是:不论值是什么,一个对象内的成员都是一个属性,甚至是函数自己也能够被做为值来传递。

一个对象能够拥有多少属性是没有数量上的限制的,甚至一个对象能够拥有0个属性(此时表示这是一个空对象)。依照其用途,一个对象能够在某些状况下被称为是一个哈希(hash)、字典(dictionary) 或表(table),折射出其结构是一组名值对。不过咱们仍是坚持在讨论时采用“对象”这一称呼。优化

建立一个对象最简单的办法是使用对象字面量(object literal)ui

// 一个对象字面量
var person = {
    name : 'Mark',
    age : 23
};

这里咱们建立了一个具备2个属性的新对象,一个键名是 name,另外一个键名是 age,这个对象被存储在 person 变量里——这为咱们提供了一个有2个成员的 person 对象。注意虽然 key 是字符串但咱们并将其包含在引号里,只要是非保留字的有效标识符,在 JavaScript 中这就是允许的。对于下面的状况,咱们须要用引号将 key 围起来:

// 一个对象字面量
var person = {
    'name of the person' : 'Mark',
    'age of the person' : 23
};

为了引用一个对象中的成员,咱们可使用点记法(dot notation),这可使咱们经过在属性名标识符以前置入一个句点来引用其对应的属性值;咱们还可使用括号记法(bracket notation),这个方法经过为字符串的属性名标识符围上一个中括号 [ ] 来达到一样的引用属性值的目的。

// 一个对象字面量
var person = {
    name : 'Mark',
    age : 23
};

// 点记法
console.log(person.name); // 'Mark'

// 括号记法
console.log(person['age']); // 23

实际上点记法是括号记法的快捷方式、语法糖(syntactic sugar),实际中大多数状况下咱们都使用点记法。固然,点记法被限制在标识符是适当的情形下。在其余状况中,你须要使用括号记法。

var person = {
    'name of the person' : 'Mark',
    'age of the person' : 23
};

console.log(person['name of the person']); // 'Mark'

当你不是采用一个字符串 key 而是采用一个对象来引用的时候,也须要使用括号记法

var person = {
    name : 'Mark',
    age : 23
};

var key = 'name';

console.log(person[key]); // 'Mark'

访问一个不存在的对象成员会返回 undefined。

var person = {};

console.log(person.name); // undefined

同时咱们还能够在一个对象建立以后动态的为其新增成员或改变某个成员的属性值。

var person = {name : 'Mark'};

person.name = 'Joseph';
console.log(person.name); // 'Joseph'

console.log(person.age); // undefined
person.age = 23;
console.log(person.age); // 23

你能够经过为对象成员赋值为函数来建立方法。

var person = {
    name : 'Mark',
    age : 23,
    sayName : function() {
        console.log(this.name);
    }
};

console.log(typeof person.sayName); // 'function'
person.sayName(); // 'Mark'

person.sayAge = function() {
    console.log(this.age); // 23
};

console.log(typeof person.sayAge); // 'function'
person.sayAge(); // 23

你应该会注意到咱们在方法中引用 person 对象的 name、age 属性使用的是 this.name 和 this.age 的方式。回顾一下咱们前一章讨论过的部分,你会知道 this 关键字指的是包含方法等属性的对象的自己,因此在本例中 this 指代的就是 person 对象。

对象的构建模块(The Buliding Blocks of Objects)

虽然对象字面量是一种建立对象的快捷方式,但它并不能完整的展现 JavaScript 面向对象的优点。好比,若是你须要建立 30 个 person 对象,那么对象字面量会是一种很是耗时的方式——为每个对象都写一个对象字面量是不切实际的。为了更有效率,咱们须要为咱们须要的对象建立一个蓝本结构,并使用这个蓝原本创造对象的实例。

在基于类的面向对象语言中,咱们能够为建立一个类来明确对象须要的结构;在基于原型的面向对象语言中,咱们能够简化的建立一个 Person 对象来提供这个结构,以后克隆这个对象来得到咱们须要的新对象。

构造函数(Constructor Functions)

第一种途径是使用 JavaScript 的构造函数(constructor functions,or constructors)方式。对象字面量是对这种方式的一种简化版。下面2个对象是等价的。

// 使用对象字面量
var personA = {
    name : 'Mark',
    age : 23
};

// 使用构造器
var personB = new Object();
personB.name = 'Mark';
personB.age = 23;

Object 函数是咱们的构造器,采用 “var personB = new Object()” 方式和采用 “var personA = {}” 是等价的。采用 new Object(),咱们建立了一个空对象,这个空对象被成为是 Object 的一个实例。

Object constructor 因其表明着JavaScript 的基础对象而显得不同凡响:全部的对象,不论这些对象是由哪一个 constructor 建立出来的,本质上都是 Object 的实例。使用 instanceof 操做符能够判断一个对象是不是一个 constructor 的实例。

// 使用对象字面量
var personA = {};

// 使用构造器
var personB = new Object();

// 检测上面2个对象是不是Object的实例
conlose.log(personA instanceof Object) // true
conlose.log(personB instanceof Object) // true

每个对象都有一个名字为 constructor 的特殊属性,其是对建立该对象自己的 constructor 函数的引用。在咱们上面的简单例子中,constructor的属性值是 Object constructor:

// 使用对象字面量
var personA = {};

// 使用构造器
var personB = new Object();

// 检测是否使用了Object的constructor
conlose.log(personA.constructor == Object) // true
conlose.log(personB.constructor == Object) // true

就像它的名字所示,constructor 函数,显然的,是一个函数。事实上,任何一个 JavaScript 函数都能被用做构造函数。这是JavaScript 对象处理方面的一个独特的地方。不一样于在对象实例化时建立一个新的构造,Javascript 是依赖于现有的构造。

固然,你没必要将你创造的全部函数都用做构造函数。大部分状况下,你会为你的类建立一个专用于构造目的的函数。一个构造函数和其余函数同样——除了自身细节上有些许区别——惯常的作法是将函数名首字母大写以表示其存在目的是做为一个构造函数。

// 一个person构造函数
var Person = {};

// 以正规函数方式使用Person
var result = Person();
console.log(result); // undefined

// 以构造器函数调用Person
var person = new Person();

console.log(typeof person); // 'object'
console.log(person instanceof Person); // true
console.log(person.constructor == Person); // true

咱们经过一个简单的空函数来建立一个构造器。当Person函数被采用常规方式调用时,它返回 undefined。当咱们在调用以前加上一个 new 关键字的时候,状况就变了:它返回了一个新对象。配合使用 new 关键字可使一个函数被做为构造器使用进而产生一个对象的实例化。

在咱们的例子中,new Person() 返回了一个空对象,这和使用 new Object() 的返回是同样的。这里的区别是,返回的对象不仅仅是 Object 的实例,同时也是 Person 的实例,而且该对象的 constructor 属性如今指向的是新的 Person 对象而非 Object 对象。不过返回的总归仍是一个空对象。

回顾一下上一章讲到的,函数内的 this 关键字指向的是一个对象。在这个关于咱们的 Person 函数的例子中,当它被做为平台函数调用时,引发被定义在全局做用域中,因此 this 关键字指向的对象是 global 对象。但当 Person 被做为一个构造函数时,状况就变了。this 关键字再也不指向 global 对象,而是指向新建立出来的那个对象:

// 一个全局变量
var fruit = 'banana';

// 咱们的constructor
var Person = function() {
    console.log(this.fruit);
};

// 被做为普通函数使用时
fruit(); // 'banana'

// 被做为constructor使用时
new Person(); // undefinded

最后一行的代码输出的是 undefined,这是由于 this.fruit 再也不指向一个已存在的变量标识符。new 关键字的做用就是建立一个新对象,并将构造函数内的 this 指向这个新建立的对象。

在本章的开始部分,咱们遇到了一个使用对象字面量建立多个对象的问题——咱们须要一个方法来批量的建立对象的拷贝而非一个个的去敲代码把它们全写一遍。如今咱们知道构造函数能够作到这一点,而且其内的 this 关键字指向的就是新建立的对象。

var Person = function(name, age) {
    this.name = name;
    this.age = age;
};

var mark = new Person('Mark', 23);
var joseph = new Person('Joseph', 22);
var andrew = new Person('Andrew', 21);

console.log(mark.name); // 'Mark'
console.log(joseph.age); // 22
console.log(andrew.name + ', ' + andrew.age); // 'Andrew, 21'

你会注意到这里咱们对构造函数进行了一些修改使其能够接受参数。这是由于构造函数和普通函数同样,只不过其内部的 this 关键字指向的是新建立的对象。当 new Person 被执行的时候,一个新的对象被建立出来,而且 Person 函数被调用。在构造函数内部,参数 name、age 被设置为同名对象属性的值,以后这个对象被返回。

使用构造函数能够很轻松的建立出和构造函数具备相似结构的新对象,而且不用费事的每次都为新对象用字面量的方式书写一遍结构。你能够在编码的开始阶段就建立一个定义了基本结构的构造函数,这对你之后为实例化的对象们增长新的属性或方法早晚会有帮助。

var Person = function(name, age) {
    this.name = name;
    this.age = age;
    this.log = function() {
        console.log(this.name + ', ' + this.age);
    }
};

var mark = new Person('Mark', 23);
var joseph = new Person('Joseph', 22);
var andrew = new Person('Andrew', 21);

mark.log(); // 'Mark, 23'
joseph.log(); // 'Joseph, 22'
andrew.log(); // 'Andrew, 21'

这里你会看到咱们在构造函数里新增了一个 log 方法,该方法会将对象的 name 和 age 信息打印出来。这样就避免了在对象实例化以后还要手工的为每个对象增长 log 方法。

原型(Prototypes)

看起来彷佛构造函数已是关于 JavaScript 对象建立的终极知识点了,但请注意,还没结束呢!咱们如今还只说了二分之一而已。若是咱们把本身局限在仅仅使用构造函数的范围,那么很快就会遇到新问题。

问题之一就是代码组织。在上一节的开头,咱们想有一种简单的方法能够批量建立具备 name 和 age 属性的 person 对象,而且指望同时具有 setName、getName、setAge、getAge 等方法。若是按照咱们如今的需求,沿用上一节的方式,最终咱们的代码会变成下面这个样子:

var Person = function(name, age) {

    // 属性
    this.name = name;
    this.age = age;

    // 方法
    this.setName = function(name) {
        this.name = name;
    }

    this.getName = function() {
        return this.name;
    }

    this.setAge = function(age) {
        this.age = age;
    }

    this.getAge = function() {
        return this.age;
    }

};

如今咱们的 Person 构造器开始变得肿胀了——这还仅是包含了2个属性和4个方法的时候!想一想若是你要建立一个很复杂的应用,那构造函数得变得多么庞大!

另外一个问题是可扩展性。假设咱们有以下代码:

// constructor.js
var Person = function(name, age) {
    this.name = name;
    this.age = age;
    this.log = function() {
        console.log(this.name + ', ' + this.age);
    }
};

// program.js
var mark = new Person('Mark', 23);
mark.log(); // 'Mark, 23'

如今Person是在外部引入的一个JS文件中定义的,咱们在这个页面里引入定义了 Person 构造函数的 constructor.js 文件,并实例化了一个 mark 对象。如今问题来了,由于咱们如今没法修改构造函数自己,那该如何为实例增长 setName、getName、setAge、getAge 等方法呢?

解决方案彷佛很简单,既然不能经过修改构造函数来增长方法,那就直接给实例增长方法不就好了么~很快随着键盘的敲打,代码变成了下面这个样子。

// constructor.js
var Person = function(name, age) {
    this.name = name;
    this.age = age;
    this.log = function() {
        console.log(this.name + ', ' + this.age);
    }
};

// program.js
var mark = new Person('Mark', 23);
mark.log(); // 'Mark, 23'

mark.getName = function() {return this.name;}
mark.getAge = function() {return this.age;}

mark.getName(); // 'Mark'
mark.getAge(); // 23

var joseph = new Person('Joseph', 22);
mark.log(); // 'Joseph, 22'

// 下面的代码会引发报错
joseph.getName();
joseph.getAge();

虽然咱们成功的为 mark 实例添加了须要的方法,但 joseph 实例并不能一样得到这些方法。此时咱们遇到了和使用对象字面量同样的问题:咱们必须为每个对象的实例作一样的设置才行,这显然是不实用的。咱们须要一个更有“疗效”的方法。

在本章的开头咱们说过,Javascript 是一门基于原型的语言,基于原型的语言最重要的特征就是建立对象是经过对一个目标对象的拷贝来实现,而非经过类。但咱们目前还未说起过拷贝,或者做为原型的目标对象,咱们目前为止看到的都是构造函数配合着new关键字。

咱们的线索就是new关键字。记住当咱们使用 new Object 时,new 关键字建立了一个新的对象,并将该对象做为构造函数内this 关键字指向的对象。实际上,new 关键字并未建立一个新的对象:它只是拷贝了一个对象。这个被拷贝的对象不是别的,正是原型(prototype)

全部能被做为构造函数使用的函数都有一个 prototype 属性,这个属性对象定义了你实例化对象的结构。当使用 new Object 时,一个对 Object.prototype 的拷贝被创造出来,这个拷贝就是新建立的那个实例对象。这是 Javascript 的另外一个有趣的特色:不一样于其它的原型语言——对它们来讲,任何对象都能做为原型使用;但在Javascript中,却有一个专为做为原型使用 prototype 对象存在。

注意:对 Javascript 而言,这是一种对其余原型性语言的模仿:对其余原型性语言而言,你能够直接克隆一个对象来获得新的对象,在 Javascript 中则是依赖克隆目标对象的 prototype 属性。在本章的最后一节你会学到实现这一作法。

prototype 对象,和其余对象同样,对其内部可容纳的成员没有数量上的限制,对其增长一个成员基本上就是简单的附加一个值而已。下面咱们对以前的 Person 函数进行一番改写:

var Person = function(name, age) {
    this.name = name;
    this.age = age;
};

Person.prototype.log = function() {
    console.log(this.name + ', ' this.age);
}

var mark = new Person('Mark', 23);
mark.log(); // 'Mark, 23'

能够看到,咱们将 log 方法的定义移出构造函数,经过 Person.prototype.log 的方式去定义,这样咱们就能告诉解析器全部从 Person 构造函数实例化出来的对象都将具备 log 方法,因此最后一行的 mark.log() 会执行。剩余的构造函数仍是保持原样,咱们并未把 this.name 和 this.age 也放在 prototype 中去,由于咱们仍是但愿在对象实例化之时就能初始化这些值。

有了 prototype 这个利器,咱们就能够对开头的代码进行重构,并使其变得更具可维护性:

var Person = function(name, age) {
    this.name = name;
    this.age = age;
};

Person.prototype.setName = function(name) {
    this.name = name;
};

Person.prototype.getName = function() {
    return this.name;
};

Person.prototype.setAge = function(age) {
    this.age = age;
};

Person.prototype.getAge = function() {
    return this.age;
};

上面这段代码还能够像下面这样合并着来写:

var Person = function(name, age) {
    this.name = name;
    this.age = age;
};

Person.prototype = {

    setName : function(name) {
        this.name = name;
    },

    getName : function() {
        return this.name;
    },

    setAge : function(age) {
        this.age = age;
    },

    getAge : function() {
        return this.age;
    }

}

如今好多了,再也没有那么多的东西拥挤在构造函数内了。并且之后一旦须要增长新的方法,只须要按照给 prototype 增长便可,而不用去从新整理构造函数。

咱们曾经有的另外一个问题(第一个是快捷建立多个实例对象,见上面)是在没法修改构造函数的状况下给实例成员添加新的方法,如今随着咱们打通了一个通往构造函数的大门(prototype属性),咱们能够轻松的在不经过构造函数的状况下为实例对象添加方法。

// person.js
var Person = function(name, age) {
    this.name = name;
    this.age = age;
};

// program.js
Person.prototype.log = function() {
    console.log(this.name + ', ' + this.age);
};

var mark = new Person('Mark', 23);
mark.log(); // 'Mark, 23'

var joseph = new Person('Joseph', 22);
joseph.log(); // 'Joseph, 22'

在前面咱们已经看到了一些简单的动态丰富 prototype 的例子。一个函数对象,以构造函数来肯定其形式,并可经过Mootools 的 Function.implement 函数为其增长新的方法。全部 Javascript 函数其实都是 Function 对象的实例,Function.implement 实际上就是经过修改 Function.prototype 对象来实现的。虽然咱们并不能直接操做 Function 的构造函数——一个由解析器提供的内置构造——但咱们依然能够经过 Function.prototype 来为 Function 对象增长新的方法。对原生方法类型的增益咱们将会在后面“衍生与原生”(Types and Natives)一节中进行讨论。

继承(Inheritance)

为了更高的理解 Javascript 是一门基于原型的语言,咱们须要区分原型与实例之间的区别。原型(prototype)是一个对象,它就像一个蓝本,用来定义咱们须要的对象结构。经过对原型的拷贝,咱们能够创造出一个该原型的实例(instance)

// 动物的构造器
var Animal = function(name) {
    this.name = name;
};

// 动物的原型
Animal.prototype.walk = function() {
    console.log(this.name + ' is walking.');
};

// 动物的实例
var cat = new Animal('Cat');
cat.walk(); // 'Cat is walking'

上面的代码中,构造函数 Animal 和它的 prototype 一块儿定义了 Animal 对象的结构,cat 对象是 Animal 的一个实例。当咱们执行 new Animal() 语句,一个 Animal.prototype 的拷贝就被建立,咱们称这个拷贝为一个实例(instance)。Animal.prototype 是一个只有一个成员的对象,这个惟一的成员是 walk 方法。天然,全部 Animal 的实例都会自动拥有 walk 这个方法。

那么,当咱们在一个实例已经被建立以后再去修改 Animal.prototype ,会发生什么呢?

// 动物的构造器
var Animal = function(name) {
    this.name = name;
};

// 动物的原型
Animal.prototype.walk = function() {
    console.log(this.name + ' is walking.');
};

// 动物的实例
var cat = new Animal('Cat');
cat.walk(); // 'Cat is walking'

// 难道动物不该该拥有吃(eat)这个方法吗?
console.log(typeof cat.eat); // undefined --> 没有 TT

// 给动物增长一个“吃”的方法
Animal.prototype.eat = function() {
    console.log(this.name + ' is eating.');
};

console.log(typeof cat.eat); // 'function'
cat.eat(); // 'Cat is eating'

嘿,如今这发生的事有点意思哈?在咱们建立好 cat 实例时候,检测 eat 方法显示的是 undefined。在咱们给 Animal.prototype 对象新增了一个 eat 方法以后,cat 实例就拥有了吃的能力!实际上,cat 的“吃”的能力就是咱们给 Animal.prototype 增长的那个函数。

看起来,彷佛是不论咱们何时给原型增长新的方法,这都会自动触发所有的实例进行一次更新。但记住当咱们新建立一个对象,那么这个新的操做就会建立一个新的原型拷贝。当咱们建立 cat 时,原型还仅拥有一个方法。若是这是一个纯粹的拷贝,那就不该该拥有咱们以后才设置的 eat 方法。毕竟,当你复印了一份文档,以后在源文档上又写上一句 “天朝人民最幸福”,你不能期望那份复印的文档上也当即出现一样的字句,不是吗?

或者是解析器知道何时 prototype 新增了成员并自动给所有的实例都增长上这个方法?也许是当咱们给原型增长了 eat 这个方法后,解析器便马上给所有的 Animal 实例增长上了这个方法?对于这一点的验证是很简单的:咱们能够先给实例设置一个 eat 的方法,以后再给原型增长 eat 方法。若是上面的猜想是对的,那么后增长的原型的 eat 方法会覆盖掉较早给 Animal 实例单独设置的那个 eat 方法。

// 动物的构造器
var Animal = function(name) {
    this.name = name;
};

// 动物的原型
Animal.prototype.walk = function() {
    console.log(this.name + ' is walking.');
};

// 动物的实例
var cat = new Animal('Cat');
cat.walk(); // 'Cat is walking'

// 给cat增长一个eat的方法
cat.eat = function() {
    console.log('Meow. Cat is eating.');
};

// 给动物增长一个“吃”的方法
Animal.prototype.eat = function() {
    console.log(this.name + ' is eating.');
};

cat.eat(); // 'Meow. Cat is eating.'

很明显,前面的猜想是错误的。Javascript 解析器不会更新实例。那真实的状况究竟是什么呢?

全部的对象都有一个叫作 proto 的内置属性,该属性指向该对象的原型。解析器利用该属性将对象“连接”到它对应的原型上。虽然在使用 new 关键字的时候确实是建立了一个原型的拷贝,且这个拷贝看起来确实很像原型自己,但它实际上倒是一个“浅拷贝”。真相是,当这个实例被建立时,它实际上只是一个空对象,这个空对象的 proto 属性指向了其构造函数的 prototype 对象。

你可能会问:“等等,既然这个新的实例是一个空对象,那为何它还会像其来源的原型那样具备属性和方法呢”?其实这就是 proto 属性的做用。实例对象经过 proto 属性连接到它的原型,这样它原型上的属性和方法也能被其实例对象访问到。在咱们的例子中,cat 对象自己被没有 walk 的方法。当解析器读取到 cat.walk() 语句时,它首先检测 cat 对象自身的prototype 对象中有无 walk 这个方法成员,若是没有,就经过 cat 的 proto 属性上溯到其原型的 prototype 中去寻找 walk 方法。而正好在这里解析器找到了它须要的方法,因而咱们的 cat 就能执行“走”的动做了。

这也能解释为何上面的代码中最后 log 出的信息是“Meow. Cat is eating.”,由于咱们给实例对象 cat 的 prototype 属性对象增长了 eat 这个方法成员,因而解析器先在这里找到了它须要的 “eat 方法,进而 cat 的原型 prototype 中的 eat 方法就不会起做用了。

一个实例对象的成员(属性啊方法啊神马的)来自于它的原型(而非是针对这个实例对象单独设置),被称为继承(inheritance)。对全部对象,你都能使用 hasOwnProperty 方法来检测某个成员是否是隶属于它。

var Animal = function() {};
Animal.prototype.walk = function() {};

var dog = new Animal();

var cat = new Animal();
cat.walk = function() {};

console.log(cat.hasOwnProperty('walk')); // true
console.log(dog.hasOwnProperty('walk')); // false

这里,咱们对 cat 使用 .hasOwnProperty(walk) 检测,返回为true,这是由于咱们已经对 cat 单独设置了一个它本身的 walk 方法。对应的,由于 dog 对象并未被赋以一个单独的 walk 方法,因此检测结果为 false。另外,若是对 cat 采用 .hasOwnProperty(hasOwnProperty),返回的一样会是 false。这是由于 hasOwnProperty 其实是 Obiect 对象的方法,而 cat 对象由 Object 处继承而来。

如今有一个家伙须要咱们好好的去考虑一下:this。在构造函数内的 this,其永远指向构造函数的实例化对象而非构造函数的 prototype 对象。可是在原型内定义的函数则遵循另外一个法则:若是该方法是直接的由原型方式来调用,则该方法内的 this 指向的是这个原型对象自己;若是该方法由这个原型的实例化对象来引用,则方法内的 this 关键字就会指向这个实例化对象。

var Animal = function(name) {
    this.name = name;
};

Animal.prototype.name = 'Animal';

Animal.prototype.getName = function() {
    return this.name;
};

// 直接使用原型方法来调用“getName”
Animal.prototype.getName(); // 返回 'Animal'

var cat = new Animal('Cat');
cat.getName(); // 返回 'Cat'

这里咱们对代码进行了一些小的修改,以便 Animal.prototype 能够有其本身的 name 属性。当咱们直接用原型方式调用 getName 时,返回的是 Animal.prototype 的 name 属性。但当咱们经过实例化对象去执行 cat.getName() 时,返回的就是 cat 的 name 属性。

原型和实例是不一样的对象,它们之间惟一的联系是:针对原型作的修改会反射到全部该原型的实例对象,但对某具体实例对象的修改却只对该实例对象自己起做用。

记住在 Javascript 中同时存在着基本数据类型和复合数据类型。如字符串、数字以及布尔值等都属于基本数据类型:当它们被做为参数传递给函数或被赋值于一个变量时,被使用的都是它们的拷贝。而像数组、函数、对象这样的复合数据类,被使用的则是它们的引用

// 建立一个对象
var object = {name : 'Mark'};

// 把这个对象“拷贝”给另外一个变量
var copy = object;

console.log(object.name); // 'Mark'
console.log(copy.name); // 'Mark'

// 更改copy对象的name值
copy.name = 'Joseph';

console.log(object.name); // 'Joseph'
console.log(copy.name); // 'Joseph'

当 var copy = object 被执行时,没有新的对象被建立出来。copy 变量其实只是指向了 object 所指向的同一个对象。object 和 copy 如今都是指向同一个对象,天然从 copy 处对其指向对象作的改动,object 也会获得反射。

对象能够拥有复合数据类型的成员,对象自身的 prototype 也一样如此。因此便出现了下面这个须要被注意的问题:当给一个指向复合数据类型的原型增长新的成员时,由于全部该原型的实例对象也都指向该原型自己,因此对原型的改动也会被继承。

var Animal = function() {};

Animal.prototype.data = {
    name : 'animal',
    type : 'unknow'
};

Animal.prototype.setData = function(name, type) {
    this.data.name = name;
    this.data.type = type;
};

Animal.prototype.getData = function() {
    console.log(this.data.name + ': ' + this.data.type);
};

var cat = new Animal();
cat.setData('Cat', 'Mammal');
cat.getData(); // 'Cat: Mammal'

var shark = new Animal();
shark.setData('Shark', 'Fish');
shark.getData(); // 'Shark: Fish'

cat.getData(); // 'Shark: Fish'

由于咱们的 cat 和 shark 对象都没有本身的 data 属性,因此它们从 Animal.prototype 处继承而来,因此 cat.data 和 shark.data 都指向了 Animal.prototype 中定义的 data 对象,对任何一个实例的 data 对象的更改都会引发咱们不但愿看到的行为。

最简单的解决办法就是将 data 属性从 Animal.prototype 中移除并在每一个实例对象中单独定义它们。经过构造函数来实现这一点是很简单的。

var Animal = function() {
    this.data = {
        name : 'animal',
        type : 'unknow'
    };
};

Animal.prototype.setData = function(name, type) {
    this.data.name = name;
    this.data.type = type;
};

Animal.prototype.getData = function() {
    console.log(this.data.name + ': ' + this.data.type);
};

var cat = new Animal();
cat.setData('Cat', 'Mammal');
cat.getData(); // 'Cat: Mammal'

var shark = new Animal();
shark.setData('Shark', 'Fish');
shark.getData(); // 'Shark: Fish'

cat.getData(); // 'Cat: Mammal'

由于此时构造函数内的 this 关键字在此处是指向实例化对象的,因此 this.data 也就为每个对象单独赋予了一个 data 属性,且不会影响到构造函数的原型。进而会看到,最后的输出结果也正是我须要的那样。

原型链(The Prototype Chain)

在 Javascript 中,Object 是基础对象模型。其余对象不管是具有如何不一样的构造,都是会从 Object 对象处得到继承。下面的代码足够帮助咱们来理解这一点:

var object = new Object();

console.log(object instanceof Object); // true

由于咱们是按照 Object 的构造函数来建立的 object 对象,因此咱们能够说 object 对象的内部属性 proto 指向的就是 Object 的 prototype 属性。如今,再来看下面这段代码。

var Animal = function()

{};

var cat = new Animal();

console.log(cat instanceof Animal); // true
console.log(cat instanceof Object); // true
console.log(typeof cat.hasOwnProperty()); // 'function'

由于使用 new Animal() 的缘故,因此咱们知道 cat 其实是一个 Animal 的实例。并且咱们还知道全部对象都有一个继承自 Object 的 hasOwnProperty 属性。因而咱们就要问了,既然 object 对象的 proto 属性如今指向的是 Animal 的原型,那这里又是怎么作到的 object 能在未涉及 Object 构造函数的状况下还能同时从 Animal 和 Object 得到继承呢?

答案就在原型之间。默认状况下,构造函数的 prototype 对象是一个不含任何方法只含有其构造函数中设置的属性的基本对象。这听起来很熟悉不是吗?这和咱们使用 new Object() 创造出来的对象是同样的!实际上咱们的代码还能够像下面这样来写。

var Animal = function() {};

Animal.prototype = new Object();

var cat = new Animal();

console.log(cat instanceof Animal); // true
console.log(cat instanceof Object); // true
console.log(typeof cat.hasOwnProperty()); // 'function'

如今就已经很清晰了,Animal.prototype 由 Object.prototype 处继承而来。对于一个实例而言,除了会从它自身的 prototype 对象继承以外,还会从 它原型的原型的 prototype 对象处继承。

感到费解?那就经过对上面的代码进行分析来增强一下对这点的理解。咱们的 cat 对象是由 Animal 对象实例化而来,因此 cat 会继承 Animal.prototype 的属性和方法。而 Animal.prototype 是由 Object 实例化而来,因此 Animal.prototype 会继承 Object.prototype 的属性和方法。进而 cat对象 会同时继承 Animal.prototype 和 Object.prototype 的属性和方法,因此咱们说 cat 是间接继承(indirectly inherits)了 Object.prototype 对象。

咱们的 cat 对象的 proto 属性指向了 Animal.prototype 对象;而 Animal 的 proto 属性则指向 Object.prototype 对象。这种 prototype 原型之间持续的链向被称为原型链(prototype chain)。进而咱们说 cat 对象的原型链展度为从其自身一直到 Object.prototype。

注意:全部对象原型链的终点都是 Object.prototype,且 Object 的 proto 属性不指向任何一个对象——不然原型链就会变得没有边界而致使基于原型链的上溯流程变得没法终止。Object.prototype 对象自己非由任何构造函数产生,而是由解析器内置的方法建立,这使得 Object.prototype 成为惟一一个不是由 Object 实例化而来的对象。

沿着一个对象的原型链查找属性或方法的行为咱们称之为遍历(traversal)。当解析器遇到 cat.hasOwnProperty 语句时,解析器首先在当前对象的 prototype 对象中查找相关方法。若是没有,则顺序的在原型链上下一个对象—— Animal.prototype 上查找。仍是没有,则继续在下一个对象的 prototype 上查找,以此类推。一旦解析器找到了它要的方法,解析器就会使用当前找到的这个方法,其在原型链上的遍历也会中止。若是解析器在整个原型链上都找不到它须要的方法,它就会返回 undefined。在咱们的例子中,解析器最后在 Object.prototype 对象上找到了 hasOwnProperty 方法。

一个对象老是属于至少一个构造函数的实例:不管是使用对象字面量仍是对象构造函数创造出来的对象,总都属于 Object 的实例。对那些非直接由 Object 构造函数创造出来的对象而言,它们既是直接建立它们的构造函数的实例,同时仍是它们原型链上全部 prototype 对象对应的构造函数的实例。

有考量的原型链(Deliberate Chains)

一旦咱们要建立更为复杂的对象,原型链就会变得很是有用。好比咱们如今要建立一个 Animal 对象:全部的动物都有名字(name),全部的动物还要可以吃东西(eat)来活下去。OK,下面是咱们的代码:

var Animal = function(name) {
    this.name = name;
};

Animal.prototype.eat = function() {
    console.log('The ' + this.name + ' is eating.');
};

var cat = new Animal('cat');
cat.eat(); // 'The cat is eating'

var bird = new Animal('bird');
bird.eat(); // 'The bird is eating'

目前为止一切都还好。不过如今须要动物们能发出声音,因而咱们须要增长新的方法。显然,这些动物发出的声音应该是不同的:猫咪的叫声是“meow”,小鸟的叫声是“tweet”。咱们能够为每个动物实例单独设置发声的方法,但显然在面对一个须要创造多个猫咪和小鸟的需求面前,这种作法是不合事宜的。咱们彷佛还能够经过为 Animal.prototype 增长方法来达到猫咪和小鸟等实例都具有发声的能力,但这仍是在浪费精力:由于猫咪不会发出“tweet”的声音,小鸟也不会“meow”的叫。

那咱们为每一个实例对象自身的构造函数单独设置方法行不行呢?咱们能够制造出 Cat、Bird 的构造器并为其分别设置不一样的发声方式。而“吃”的能力则仍是从 Animal.prototype 那继承而来:

var Animal = function(name) {
    this.name = name;
};

Animal.prototype.eat = function() {
    console.log('The ' + this.name + ' is eating.');
};

var Cat = function() {};

Cat.prototype = new Animal('cat');

Cat.prototype.meow = function() {
    console.log('Meow!');
};

var Bird = function() {};

Bird.prototype = new Animal('bird');

Bird.prototype.tweet = function() {
    console.log('Tweet!');
};

var cat = new Cat();
cat.eat(); // 'The cat is eating'
cat.meow(); // 'Meow!'

var bird = new Bird();
bird.eat(); // 'The bird is eating'
bird.tweet(); // 'Tweet!'

能够看到,咱们保留了原有的 Animal 构造函数,而且基于它新建了另外两个更具体的构造函数——Cat 和 Bird。以后咱们分别为 Cat 和 Bird 设置了它们本身的发声方式。这样,咱们最终的实例对象猫咪和小鸟就都能发出它们各自不一样的叫声了。

在基于类的程序语言中,这种直接继承了其实例化来源的类的特征,且更具针对性的分支被称为子类(subclassing)。Javascript,则是一门基于原型的语言,并无类的概念,就其本质而言,咱们惟一所作的就是创造了一个有考量的原型链(deliberate prototype chain)。这里之因此用“有考量”这个词,是由于咱们显然是有意的设计了哪些对象应该出如今咱们的实例原型链上。

原型链上的成员数量没有限制,你还能够经过丰富原型链上的对象来知足更有针对性的需求。

var Animal = function(name) {
    this.name = name;
};

Animal.prototype.eat = function() {
    console.log('The ' + this.name + ' is eating.');
};

var Cat = function() {};

Cat.prototype = new Animal('cat');

Cat.prototype.meow = function() {
    console.log('Meow!');
};

var Persian = function() {
   this.name = 'persian cat';
};

Persian.prototype = new Cat();

Persian.prototype.meow = function() {
    console.log('Meow...');
};

Persian.prototype.setColor = function() {
    this.color = color;
};

Persian.prototype.getColor = function() {
    return this.color;
};

var king = new Persian();
king.setColor('black');
king.getColor(); // 'black'
king.eat(); // 'The persian cat is eating'
king.meow(); // 'Meow...'

console.log(king instanceof Animal); // true
console.log(king instanceof Cat); // true
console.log(king instanceof Persian); // true

这里咱们创造了一个名为 Persian(波斯猫) 的 Cat 分支。你会注意到这里咱们设置了一个 Persian.prototype.meow 的方法,这个方法在 Persian 的实例中会覆盖掉 Cat.prototype.meow。若是你检查一下,会发现 king 对象分别是 Animal、Cat 和 Persian 的实例,这也说明了咱们原型链的设计是正确的。

原型链真正的威力在于继承与原型链遍历的结合。由于原型链上全部的 prototype 对象都是链起来的,因此原型链上某一点的改变会当即反射到它所指向的其余成员对象。若是咱们给 Animal.prototype 新增一个方法,那么全部 Animal 的实例都会新增长上这个方法。这位咱们批量的为对象扩充方法提供了简易快捷的方式。

若是你的程序正变得越发庞大,那么有考量的原型链会帮助你的代码更具结构性。不一样于把全部的代码都塞进一个 prototype 对象中,你能够建立多重的具有良好设计的 prototype 对象,这对减小代码量、提高代码的可维护性都颇有好处。

简化原型的编程(Simplified Prototypal Programming)

如今你应该已经意识到 Javascript 的面向对象风情有其独到的范式。Javascript 所谓的“基于原型的程序语言”很大程度上是仅限于名义上的。Javascript 中有着本应是在基于类的语言中才会出现的构造函数和 new 关键字的组合,同时将从原型——这个显著的原型式语言的特征——处继承来的东西做为其用以实现针对性 prototype 对象的依据,而这些更具针对性的 prototype 对象,则是那么的相似类式语言中的子类。这门语言在对象机制实现方面的设计必定程度上受到了当时程序语言潮流的影响:在这门语言被建立的那个时代,基于类的程序语言处于正统的标准地位。因此,最终的决定就是为这门新语言赋予一些同类式语言类似的特征。

尽管如此,Javascript 依然是一门灵活的语言。虽然咱们不能改变在其核心中定义的对象的实现机制,但咱们依然可以使用现有手段令这门语言散发出更纯粹的原型式风格(固然咱们在下一章中会看到另外一种流派——如何使这门语言在实际中更具有类式风格)。

在咱们如今所讨论的简化原型的范畴内,让咱们把视线从 Javascript 自己那具有复合性特征的原型上先移开,只先关注对象自己。不一样于先建立一个构造函数以后再设置其 prototype,咱们使用真的对象做为原型来建立新的对象,并将其prototype属性“克隆”到新建立的对象身上。为了更明确的说明咱们要作的,这里先举一个例子,这个例子来自另外一个纯粹的原型式程序语言 IO:

Animal := Object clone
Animal name := "animal"

Cat := Animal clone
Cat name := "cat"

myCat := Cat clone

虽然这不是一本关于 IO 语言的书,但咱们仍是从基础讲起。同 Javascript 同样,IO 中的基础对象也是 Object。不过,这里的 Object 并非一个构造器(厄,一个函数),而是一个真正的对象。在咱们代码的开始部分,咱们创造了一个新的对象—— Animal,这个新对象由源对象 Object 处克隆而来。由于在 IO 语言中,空格用来访问属性,因此 Object clone 语句的含义就是“使用 Object 的 clone 方法并执行它”。以后咱们为 Animal 的 name 属性设置了一个字符型的值,经过克隆 Animal 建立了一个名为 Cat 的新对象,同时我也为这个 Cat 对象设置了 name 属性,最后咱们克隆 Cat 获得一个 myCat 对象。

咱们能够在 Javascript 中实现相似的事:

var Animal = function() {};
Animal.prototype = new Object();
Animal.prototype.name = 'animal';

var Cat = function() {};
Cat.prototype = new Object();
Cat.prototype.name = 'cat';

var myCat = new Cat();

很像,但却不彻底同样。在 IO 的例子中,最终的 myCat 是直接由 Cat、Animal、Object 处克隆而来的,这些都是纯粹的对象而非构造器。但在咱们的 Javascript 的例子中,最终的 myCat 对象则是由Cat、Animal、Object 等对象的 prototype 属性继承而来,Cat、Animal、Object 等也都是函数而非对象。换句话说。IO 没有构造函数的概念,一切都是直接从对象克隆而来。但 Javascript 却有构造函数,且克隆的是 prototype。

若是咱们能控制内部属性 proto,那么咱们就能在 Javascript 中实现和 IO 同样特性。 例如,假如咱们有一个 Animal 对象和一个 Cat 对象,咱们能够改变 Cat 对象的 proto属性使之直接链向 Animal 对象(而非链向 Animal 的 prototype 对象)自己,这样 Cat 就能直接继承 Animal 对象。

由于 proto 属性是内置属性不能直接修改它,但一些 Javascript 解析器却引入了一个和其相似的名为 proto 的属性。一个对象的 proto 属性被用做更改其内置的 proto 属性,以使其能够直接链向其余对象。

var Animal = {
    name : 'animal',
    eat : function() {
        console.log('The ' + this.name + ' is eating.')
    }
};

var Cat = {name : 'cat'};
Cat.__proto__ = Animal;

var myCat = {};
myCat.__proto__ = Cat;

myCat.eat(); // 'The cat is eating.'

这里不存在构造函数,Animal 和 Cat 对象直接由字面量建立。经过 Cat.__proto__ = Animal 语句咱们告诉解析器 Cat 的 proto 属性直接指向 Animal 对象。最后 myCat 对象都直接从 Cat 和 Animal 处获得继承,在 myCat 的原型链上也不存在任何为 prototype 的对象。这个简化的原型模型不包含任何的构造器或原型属性,而是替代的将真实的对象自己放置其原型链上。

相似的,你可使用 Object.create 方法来达到一样的效果,这个新函数目前已经被 ECMAScript 5 正式引入。它只接受一个参数,该参数为一个对象,其执行的结果是建立一个空对象,而这个对象的 proto 属性将被指向做为参数传入的那个对象。

var Animal = {
    name : 'animal',
    eat : function() {
        console.log('The ' + this.name + ' is eating.')
    }
};

var Cat = Object.create(Animal);
Cat.name = 'cat';

var myCat = Object.create(Cat);
myCat.eat(); // 'The cat is eating.'

注意这里的 Object.create 方法和 IO 里的 clone 方法很相像,实际上,它们实现的也是同一件事。咱们可使用 Object.create 方法很是高仿的实现 IO 语言的那个片断:

var Animal = Object.create({});
Animal.name = 'animal';

var Cat = Object.create(Animal);
Cat.name = 'cat';

myCat = Object.create(Cat);

不幸的是,虽然上面的两种方式都很美妙,但它们却不能兼容全部平台。__proto__ 属性目前还不属于正式的 ECMAScript 规范,因此并非全部的解析器都对其提供支持。而 Object.create() 方法,虽然是规范中的一员,但该规范倒是指 ECMAScript 5。因该规范是2009年才颁布的,因此目前也不是全部解析器都能提供完整的支持。若是你但愿写出具备更好兼容性的代码(尤为是 web app 程序),就尤为要记住这2种方式都不是通用方案。

如今有一种方案可使较为古老的解析器也能支持 Object.create 方法。就是记住 Javascript 对象经过引用来起做用,若是你将一个对象存储在变量 x 中,以后操做 y = x,那么 y 和 x 将同时指向同一个对象。同时,一个函数的 prototype 属性也是一个对象,而这个对象的初始值能够很轻易的经过被分配给一个新的对象值来覆盖:

var Animal = {
    name : 'animal',
    eat : function() {
        console.log('The ' + this.name + ' is eating.')
    }
};

var AnimalProto = function() {};
AnimalProto.prototype = Animal;

var Cat = new AnimalProto();
console.log(typeof cat.purr); // 'undefinded'

Animal.purr = function() {};
console.log(typeof cat.purr); // 'function'

这段代码如今看来应该有些眼熟了吧。咱们首先建立了一个有着2个成员(一个name 属性、一个 eat 方法)的 Animal 对象,以后咱们建立了一个名为 AnimalProto 的“跳板级”构造函数,并将它的 prototype 属性设置为 Animal 对象。由于引用的缘故,AnimalProto.prototype 属性 和 Animal 如今都指向了同一个对象。这就意味着,当咱们建立了 cat 实例时,它其实是直接继承自 Animal 对象 —— 这就像是使用 Object.create 方法创造出来的同样。

采用这个点子,咱们能够模拟出 Javascript 解析器所不支持的 Object.create 方法。

if (!Object.create) Object.create = function(proto) {
    var Intermediate = function() {};
    Intermediate.prototype = proto;
    return new Intermediate();
};

var Animal = {
    name : 'animal',
    eat : function() {
        console.log('The ' + this.name + ' is eating.')
    }
};

var Cat = Object.create(Animal);
console.log(typeof cat.purr); // 'undefinded'

Animal.purr = function() {};
console.log(typeof cat.purr); // 'function'

最开始,咱们使用一个 IF 语句来判断当前解析器是否支持 Object.create 方法。若是支持,则直接执行下面的语句,若是不支持,就模拟一个该方法:它首先创造一个名为 Intermediate 的构造器,以后将该构造器的 prototype 属性指向做为参数传入的那个对象。最后该函数返回一个 Intermediate.prototype 的实例。由于这里咱们使用的方法都是当下解析器所支持的,因此咱们能够说这个模拟的 Object.create 方法是具有普适性的。

总结(The Wrap Up)

在这一章,咱们详细的讨论了有关 Javascript 对象机制的全部话题,并展现了它和其余语言之间的区别。虽然它是一门基于原型的语言,但由于其自身的一些独特性,使其其实是兼具类式和原型式语言的特征。咱们看到了如何使用字面量和构造器的 prototype 属性来新建对象。咱们还展现了继承的奥秘、Javascript 原型链上的遍历是如何工做的。最后咱们还实践了一个将 Javascript 自己的原型混杂性隐藏起来的简便原型式模型。

由于 Javascript 的核心是一门面向对象的语言,因此在这里所写的针对该点的知识,会在咱们开发复杂应用时候提供莫大的帮助。虽然面向对象自己已经超越了本书所要讲述的范围,但我依然但愿我在这里所提供的信息,能够为你在该话题上的深刻学习提供一点帮助。

招贤纳士(Recruitment)

招人,前端,隶属政采云前端大团队(ZooTeam),50 余个小伙伴正等你加入一块儿浪~ 若是你想改变一直被事折腾,但愿开始能折腾事;若是你想改变一直被告诫须要多些想法,却无从破局;若是你想改变你有能力去作成那个结果,却不须要你;若是你想改变你想作成的事须要一个团队去支撑,但没你带人的位置;若是你想改变既定的节奏,将会是“5年工做时间3年工做经验”;若是你想改变原本悟性不错,但老是有那一层窗户纸的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但愿参与到随着业务腾飞的过程,亲手参与一个有着深刻的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我以为咱们该聊聊。任什么时候间,等着你写点什么,发给 ZooTeam@cai-inc.com

相关文章
相关标签/搜索