明白 JavaScript(四)

第四篇拖了良久了,真是有点不好意思。实话实说,迁延良久的缘由主假如没想好怎样写,因为这一篇的主题比较有挑战性:原型和基于原型的继承——啊~我终究说出口了,这下没托言迁延了==

原型

我(个人)不喜欢的,就是讲原型时上来就拿类做比较的,所以我不会如许讲。不过我确切讲过组织器函数,在这方面和类多多少少有共通的地方。我的发起是:忘记类。有很多看法以为“类”学的众多是面向对象的过分生长,是一种悲哀,以至于有太多的开发者险些把面向对象和类划上了等号。在进修原型之前,我请你先记着并品尝这句话:

面向对象设想的精华在于“笼统”二字,类是完成实体笼统的一种手腕,但不是唯一一种。

prototype__proto__

事前声明:永久,永久不要在实在的代码里运用 __proto__ 属性,在本文里用它纯粹是用于研讨!很快我们会讲到它的替代品,抱歉请忍受。

在 JavaScript 里,函数是对象(等学完了这一篇,无妨研讨一下函数终究是怎样就成了对象的?),对象嘛,毫无不测的就会有属性(要领也是属性),然后毫无不测的 prototype 就是函数的一个属性,末了毫无不测的 prototype 属性也是一个对象。瞧,何等水到渠成的事变:

function foo() {}
foo.prototype;    // 内里有啥本身去看

好吧,那 prototype 有啥用?呃,假如你把函数就看成函数来用,那它压根没用。不过,若你把函数看成组织器来用的话,新天生的对象就可以直接接见到 prototype 对象里的属性。

// 要充任组织器了,按通例把首字母大写
function Foo() {}
var f = new Foo();
f.constructor;    // function Foo() {}

想一下,fconstructor 属性哪里来的?假如你想不明白,请用 console.dir(Foo.prototype) 一探终究。

这说明了一个题目:

函数的原型属性不是给函数本身用的,而是给用函数充任组织器建立的对象运用的。

使人迷惑的是,prototype 属性存在于 Foo 函数对象内,那末由 Foo 建立的实例对象 f 是怎样接见到 prototype 的呢?是经由过程复制 prototype 对象吗?接着上面的代码我们继承来看:

f.__proto__;                      // Foo {}
Foo.prototype;                    // Foo {}
f.__proto__ === Foo.prototype;    // true

哦~不是复制过来的,而是一个叫做 __proto__ 的属性指向了组织器的 prototype 对象呀。

没错!这就是原型机制的精华地点,让我们来总结一下一切的细节(包含隐含在表象之下的):

  1. 函数具有 prototype 属性,然则函数本身不用它
  2. 函数充任组织器的时刻可以建立出新的对象,这须要 new 操作符的合营。其事情原理我已在第一篇做了大部份的论述
  3. 我还没有说起的是:new 在建立新对象的时刻,会赋予新对象一个属性指向组织器的 prototype 属性。这个新的属性在某些浏览器环境内叫做 __proto__
  4. 当接见一个对象的属性(包含要领)时,起首查找这个对象本身有无该属性,假如没有就查找它的原型(也就是 __proto__ 指向的 prototype 对象),假如还没有就查找原型的原型(prototype 也有它本身的 __proto__,指向更上一级的 prototype 对象),依此类推一向找到 Object 为止

OK,上面的第四点事实上就是 JavaScript 的对象属性查找机制。因而可知:

原型的意义就在于为对象的属性查找机制供应一个方向,也许说一条线路

一个对象,它有很多属性,个中有一个属性指向了别的一个对象的原型属性;而后者也有一个属性指向了再别的一个对象的原型属性。这就像一条一环套一环的锁链一样,而且从这条锁链的任何一点寻觅下去,末了都能找到链条的出发点,即 Object;因而,我们也把这类机制称作:原型链

如今,我愿望一致一下所运用的术语(至少在本文局限内):

  • 函数的 prototype 属性:我们叫它 原型属性原型对象
  • 对象的 __proto__ 属性:我们叫它 原型

比方:

  • Foo 的原型属性(或原型对象) = Foo.prototype
  • f 的原型 = f.__proto__

一致术语的缘由在于,只管 Foo.prototypef.__proto__ 是等价的,然则 prototype__proto__ 并不一样。当斟酌一个牢固的对象时,它的 prototype 是给原型链的下方运用的,而它的 __proto__ 则指向了原型链的上方;因而,一旦我们说“原型属性”也许“原型对象”,那末就暗示着这是给它的子子孙孙们用的,而说“原型”则是暗示这是从它的父辈继承过来的。

再换一种说法:对象的原型属性或原型对象不是给本身用的,而对象的原型是可以直接运用的。

__proto__ 的题目

既然 __proto__ 可以接见到对象的原型,那末为何制止在实际中运用呢?

这是一个设想上的失误,致使 __proto__ 属性是可以被修正的,同时意味着 JavaScript 的属性查找机制会因而而“瘫痪”,所以猛烈的不发起运用它。

假如你确切要经由过程一个对象接见其原型,ES5 供应了一个新要领:

Object.getPrototypeOf(f)    // Foo {}

这是平安的,只管放心运用。斟酌到低版本浏览器的兼容性题目,可以运用 es5-shim

自有属性和原型属性的区分

因为对象的原型是一个援用而不是赋值,所以变动原型的属性会马上作用于一切的实例对象。这一特征异常适用于为对象定义实例要领:

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

Person.prototype.greeting = function () {
    return "你好,我叫" + this.name;
};

var p1 = new Person("张三");
var p2 = new Person("李四");

p1.greeting();    // 你好,我叫张三
p2.greeting();    // 你好,我叫李四

/* 转变实例要领的行动:*/

Person.prototype.greeting = function () {
    return "你好,我叫" + this.name + ",很愉快熟悉你!";
};

/* 视察其影响:*/

p1.greeting();    // 你好,我叫张三,很愉快熟悉你!
p2.greeting();    // 你好,我叫李四,很愉快熟悉你!

但是,转变自有属性则差别,它只会对新建立的实例对象产生影响,接上例:

function Person(name) {
    this.name = "超人";
}

/* 不影响已存在的实例对象 */
p1.greeting();    // 你好,我叫张三,很愉快熟悉你!

/* 只影响新建立的实例对象 */
var p3 = new Person("王五");
p3.greeting();    // 你好,我叫超人,很愉快熟悉你!

这个例子看起来有点无厘头,没啥大用,不过它的精力在于:在实际天下中,庞杂对象的行动也许会依据状况对其举行重写,然则我们不愿望转变对象的内部状况;也许,我们会完成继承,去掩盖父级对象的某些行动而不引向其他雷同的部份。在这些状况下,原型会赋予我们最大水平的天真性。

我们怎样晓得属性是自有的照样来自于原型的?上代码~

p1.hasOwnProperty("name");        // true
p1.hasOwnProperty("greeting");    // false

p1.constructor.prototype.hasOwnProperty("greeting");     // true
Object.getPrototypeOf(p1).hasOwnProperty("greeting");    // true

代码很简朴,就不用过分诠释了,注重末了两句实际上等价的写法。

警惕 constructor

适才的这一句代码:p1.constructor.prototype.hasOwnProperty("greeting");,实在暗含了一个风趣的题目。

对象 p1 可以接见本身的组织器,这要感谢原型为它供应了 constructor 属性。接着经由过程 constructor 属性又可以反过来接见到原型对象,这似乎是一个圈圈,我们来实验一下:

p1.constructor === p1.constructor.prototype.constructor;    // true
p1.constructor === p1.constructor.prototype.constructor.prototype.constructor;    // true

还真是!不过我们不是因为好玩才研讨这个的。

只管我们说:变动原型对象的属性会马上作用于一切的实例对象,然则假如你完整掩盖了原型对象,事变就变得诡异起来了:(浏览接下来的例子,请一句一句考证本身心中所想)

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

var p1 = new Person("张三");

Person.prototype.greeting = function () {
    return "你好,我叫" + this.name;
};

p1.name;                      // 张三
p1.greeting();                // 你好,我叫张三
p1.constructor === Person;    // true

/* so far so good, but... */

Person.prototype = {
    say: function () {
        return "你好,我叫" + this.name;
    }
};

p1.say();                    // TypeError: Object #<Person> has no method 'say'
p1.constructor.prototype;    // Object { say: function }

呃?Person 的原型属性里明显有 say 要领呀?原型对象不是立即见效的吗?

原型继承

如果只为了建立一种对象,原型的作用就没法悉数发挥出来。我们会进一步应用原型和原型链的特征来拓展我们的代码,完成基于原型的继承。

原型继承是一个异常大的话题局限,慢慢地你会发明,只管原型继承看起来没有类继承那末的规整(相对而言),然则它却越发天真。无论是单继承照样多继承,以至是 Mixin 及其他连名字都说不上来的继承体式格局,原型继承都有方法完成,而且每每不止一种方法。

不过让我们先从简朴的最先:

function Person() {
    this.klass = '人类';
}

Person.prototype.toString = function () {
    return this.klass;
};

Person.prototype.greeting = function () {
    return '大家好,我叫' + this.name + ', 我是一位' + this.toString() + '。';
};

function Programmer(name) {
    this.name = name;
    this.klass = '程序员';
}

Programmer.prototype = new Person();
Programmer.prototype.constructor = Programmer;

这是一个异常好的例子,它向我们展现了以下要点:

var someone = new Programmer('张三');

someone.name;          // 张三
someone.toString();    // 程序员
someone.greeting();    // ‌大家好,我叫张三, 我是一位程序员。

我来捋一遍:

  1. 倒数第二行,new Person() 建立了对象,然后赋给了 Programmer.prototype 因而组织器原型属性就变成了 Person 的实例对象。
  2. 因为 Person 对象具有重写过的 toString() 要领,而且这个要领返回的是宿主对象的 klass 属性,所以我们可以给 Programmer 定义一个 greeting() 要领,并在个中运用继承而来的 toString()
  3. someone 对象挪用 toString() 要领的时刻,this 指向的是它本身,所以可以输出 程序员 而不是 人类

还没完,继承看:

// 因为 Programmer.prototype.constructor = Programmer; 我们才获得:
someone.constructor === Programmer;    ‌// true

// 这些效果表现了何谓“链式”原型继承
‌‌someone instanceof Programmer;         ‌// true
‌‌someone instanceof Person;             //‌ true
‌‌someone instanceof Object;             ‌// true

要领重载

上例实在已完成了对 toString() 要领的重载(这个要领的鼻祖对象是 Object.prototype),秉持一样的精力,我们本身写的子组织器一样可以经由过程原型属性来重载父组织器供应的要领:

Programmer.prototype.toString = function () {
    return this.klass + "(码农)";
}

var codingFarmer = new Programmer("张三");
codingFarmer.greeting();    // 大家好,我叫张三, 我是一位程序员(码农)。

属性查找与要领重载的抵牾

头脑活泼回响反映快的同砚也许已在想了:

为何肯定要把父类的实例赋给子类的原型属性,而不是直接用父类的原型属性呢?

好题目!这个主意异常有原理,而且这么一来我们还可以削减属性查找的次数,因为向上查找的时刻跳过了父类实例的 __proto__,直接找到了(如上例)Person.prototype

但是不这么做的来由也很简朴,假如你这么做了:

Programmer.prototype = Person.prototype;

因为 Javascript 是援用赋值,因而等号两头的两个属性即是指向了同一个对象,那末一旦你在子类对要领举行重载,连带着父类的要领也一同变化了,这就失去了重载的意义。因而只要在肯定不须要重载的时刻才可以这么做。

    原文作者:n͛i͛g͛h͛t͛i͛r͛e͛
    原文地址: https://segmentfault.com/a/1190000000400182
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞