Javascript中的的对象——原型形式(Prototype)

本文原文泉源:《Object-Oriented JavaScript》By Stoyan Stefanov
本文翻译泉源:赤石俊哉 原创翻译
版权说明: 假如您是原文的原作者而且不愿望此文被公然,能够联络作者删除。本文翻译由 赤石俊哉 翻译整顿,您能够用于进修交换的目标,然则制止用于其他用处,因擅自滥用激发的版权纠葛本人概不负责。

原型形式(Prototype)

在这一章节中你将会进修运用“函数(function)”对象中的prototype属性。在JavaScript的进修过程当中,明白prototype的事情道理是很主要的一个部份。毕竟,JavaScript被分类为是一个基于原型形式对象模子的言语。实在原型形式并不难,然则它是一种新的看法而且每每须要花些时刻去明白。它是JavaScript中的一部份(闭包是另一部份),一旦你“get“了他们,他们就会变得很轻易明白也是很有意义的。在本书的盈余部份中,强烈建议多打多试这些示例。那样会越发轻易地进修和记着这些看法。
本章中将会议论以下话题:

  • 每一个函数都有一个prototype属性,而且它包括了一个对象。

  • 向prototype中增加属性。

  • 运用向prototype中增加的属性。

  • 函数自身属性以及原型属性的辨别。

  • 每一个对象保存在prototype中的私密链接——__proto__

  • 要领:isPrototypeOf(),hasOwnProperty(),propertyIsEnumerable()

  • 怎样增强内建对象,比如数组(array)和字符串(string)。

原型属性

JavaScript中的函数是对象,而且包括了要领和属性。包括我们罕见的一些的要领,像apply()call()等,罕见的属性,像lengthconstructor等。另有一个属性就是prototype

当你定义了一个简朴的函数foo()以后,你能够像其他对象一样,直接接见这个函数的属性:

>>> function foo(a, b){return a * b;}
>>> foo.length
2

>>> foo.constructor
Function()

prototype这个属性在你定义函数的时刻就竖立好了。他的初始值是一个空对象。

>>> typeof foo.prototype
"object"

你能够运用属性和要领来扩大这个空对象。他们不会对foo()函数自身发生任何影响。他们只会在当你运用foo()作为组织函数的时刻被运用。

运用原型形式增加要领和属性

在前面的章节中,已进修过了怎样定义一个构建新对象时运用的组织函数。最主要的头脑是在函数中挪用new时,接见this变量,它包括了构建函数返回的对象。扩大(增加要领和属性)this对象是在对象被竖立时增加功用的一种要领。
让我们来看个例子,Gadget()组织要领中运用this来增加两个属性和一个要领到它竖立的对象里。

function Gadget(name, color){
    this.name = name;
    this.color = color;
    this.whatAreYou = function(){
        return 'I am a ' + this.color + ' ' + this.name;
    }
}

向组织函数的prototype中增加要领和属性是在对象被竖立的时刻为对象增加功用的另一种体式格局。接下来再增加两个属性pricerating和一个getInfo()要领。因为prototype包括一个对象,所以你能够像如许增加:

Gadget.prototype.price = 100;
Gadget.prototype.rating = 3;
Gadget.prototype.getInfo = function(){
    return 'Rating: ' + this.rating + ', Price: ' + this.price;
};

你也能够经由过程另一种体式格局到达一样的目标,就是完整掩盖掉原型属性,将它换成你挑选的对象:

Gadget.prototype = {
    price: 100,
    rating: 3,
    getInfo: function() {
        return `Rating: ` + this.rating + ', Price:' + this.price;
    }
};

运用原型属性的要领和属性

你增加到组织函数的原型属性中的一切要领和属性你都是能够直接在运用这个组织函数组织新对象以后,直接运用的。比如,假如你运用Gadget()构建函数,竖立了一个newtoy对象,你能够直接接见已定义的一切要领和属性。

>>> var newtoy = new Gadget('webcam', 'black');
>>> newtoy.name;
"webcam"

>>> newtoy.color;
"black"

>>> newtoy.whatAreYou();
"I am a black webcam"

>>> newtoy.price;
100

>>> newtoy.rating;
3

>>> newtoy.getInfo();
"Rating:3, Price: 100"

有一点很主要的是,原型属性是”活的“,在JavaScript中对象的通报是经由过程援用来举行的。为此,原型范例不是直接复制到新对象中的。这意味着什么呢?这意味着,我们能够在任何时刻修正任何对象的原型属性(以致你都能够在新建对象以后举行修正),它们都是见效的。
让我们继续来看一个例子,增加下面的要领到原型属性内里:

Gadget.prototype.get = function(what){
    return this[what];
};

只管我们在定义get()要领之前已生成了newtoy对象,但是newtoy照旧能够接见这个新的要领:

>>> newtoy.get('price');
100

>>> newtoy.get('color');
"black"

“函数自身属性”与“原型属性”的对照

在上面的getInfo()例子中,运用了this来从内部指向对象自身,运用Gadget.prototype也能够到达一样的目标:

Gadget.prototype.getInfo = function(){
    return 'Rating: ' + Gadget.prototype.rating + ', Price: ' + Gadget.prototype.price;
};

这有啥不一样呢?在回复这个题目之前,我们先来测试一下看看原型属性是怎样事情的吧。
让我们再拿出我们的newtoy对象:

>>> var newtoy = new Gadget('webcam', 'black');

当你尝试接见newtoy的一个属性,运用表达式newtoy.name,JavaScript引擎将会阅读对象的一切属性,寻觅一个叫作name,假如找到它,它的值就会被返回。

>>> newtoy.name
'webcam'

什么?你想尝试着接见rating属性?JavaScript引擎会搜检newtoy中的一切属性,然后没有找到一个叫作rating的。然后剧本引擎就会鉴别出,组织函数中的原型属性曾尝试着竖立这个对象(就像你运用newtoy.constructor.prototype的时刻一样)。假如属性在原型属性中找到了这个属性,就会运用原型属性中的这个属性。

>>> newtoy.rating
3

这和你直接接见原型属性一样。每一个对象都有一个组织函数的属性,它是对竖立该对象运用的组织函数的援用。所以,在这个例子中:

>>> newtoy.constructor
Gadget(name, color)

>>> newtoy.constructor.prototype.rating
3

如今,让我们再来看看第一步,每一个对象都有一个组织函数。原型属性是一个对象,所以,它也应当也有一个组织函数。进而它的组织函数又有一个原型属性……

>>> newtoy.constructor.prototype.constructor
Gadget(name, color)
>>> newtoy.constructor.prototype.constructor.prototype
Object price=100 rating=3

这个轮回将会延续下去,详细有多长取决于这个原型属性链有多长。然则末了会闭幕于一个内建的Object()对象。它是最外层父类。在这个例子中,假如你尝试着运用newtoy.toString(),而newtoy他没有本身的toString()要领,而且它的原型属性对象里也没有,他就会一向往上找,末了会挪用Object对象的toString()要领。

>>> newtoy.toString()
"[object Object]"

运用函数自身的属性掩盖原型属性的属性

如上面所演示的,假如你的对象没有一个确实的本身的属性,能够运用一个原型链上层的对象。假如对象和原型属性内里有雷同名字的属性,自身的属性会被优先运用。
接下来我们来模仿一个属性同时存在于自身属性和原型属性中:

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

>>> Gadget.prototype.name = 'foo';
"foo"

竖立一个新对象,接见它的name属性,它会给你对象自身的name属性。

>>> var toy = new Gadget('camera');
>>> toy.name;
"camera"

假如你删除这个属性,那末原型属性中运用雷同名字的属性就会“表现出来”:

>>> delete toy.name;
true

>>> toy.name;
"foo"

固然,你能够从新竖立它的自身属性:

>>> toy.name = 'camera';
>>> toy.name;
"camera"

遍历属性

假如你愿望列出一个对象的一切属性,你能够运用一个for-in轮回。在第二章节中,进修了怎样遍历一个数组内里的一切元素:

var a = [1, 2, 3];
for (var i in a)
{
    console.log(a[i]);
}

数组是一个对象,所以能够推导出for-in遍历对象的时刻:

var o = {p1: 1, p2: 2};
for (var i in o) {
    console.log(i + '=' + o[i]);
}

这将会发生:

p1=1
p2=2

须要晓得的几个细节:

  • 不是一切的属性都在for-in轮回中显示出来。比如,数组的length,以及constructor属性就不会被显示出来。被显示出来的属性叫做可罗列的。你能够运用每一个对象都能供应的propertyIsEnumerable()要领来搜检一个属性是不是是可罗列的。

  • 原型链中原型属性假如是可罗列的,也会被显示出来。你能够运用hasOwnProperty()要领来搜检一个属性是自身属性照样原型属性。

  • propertyIsEnumerable()将会对一切原型属性中的属性返回false,只管他们会在for-in轮回中显示出来,也是可罗列的。

为了看看这些函数的效果,我们运用一个简化版本的Gadget()

function Gadget(name, color)
{
    this.name = name;
    this.color = color;
    this.someMethod = function(){
        return 1;
    }
}
Gadget.prototype.price = 100;
Gadget.prototype.rating = 3;

竖立一个新的对象:

var newtoy = new Gadget('webcam', 'black');

假如你运用for-in轮回,你能够看到对象的一切属性,包括那些原型属性的:

for (var prop in newtoy){
    console.log(prop + ' = ' + newtoy[prop];
}

这个效果也包括对象的要领(那是因为要领是恰好范例是函数的属性):

name = webcam
color = black
someMethod = function(){ return 1;}
price = 100
rating = 3

假如你想辨别对象自身属性和原型属性的属性,运用hasOwnProperty(),尝尝这个:

>>> newtoy.hasOwnProperty('name')
true

>>> newtoy.hasOwnProperty('price')
false

让我们再来轮回一次,然则此次只显示自身的属性:

for (var prop in newtoy){
    if (newtoy.hasOwnProperty(prop)){
        console.log(prop + '=' + newtoy[prop]);
    }
}

效果:

name=webcam
color=black
someMethod=function(){return 1;}

接下来让我们尝尝propertyIsEnumerable()。假如自身属性不是内置的属性,这个函数就会返回true:

>>> newtoy.propertyIsEnumerable('name')
true

>>> newtoy.propertyIsEnumerable('constructor')
false

任何从原型链上来的属性都是不可罗列的:

>>> newtoy.propertyIsEnumerable('price')
false

注重,虽然假如你获取了包括在原型属性中对象,而且挪用了它的propertyIsEnumerable(),这个属性是能够罗列的。

>>> newtoy.constructor.prototype.propertyIsEnumberable('price')
true

isPrototypeOf()

每一个对象都有isPrototypeOf()要领。这个要领会通知你指定的对象是谁的原型属性。
我们先写一个简朴的对象monkey

var monkey = {
    hair: true,
    feeds: 'bananas',
    breathes: 'air'
};

接下来,让我们竖立一个Human()组织函数,然后设定它的prototype属性指向monkey

function Human(name){
    this.name = name;
}
Human.prototype = monkey;

假如你竖立一个叫作georgeHuman对象,然后问它:“monkeygeorge的原型属性吗?”,你就会获得true

>>> var george = new Human('George');
>>> monkey.isPrototypeOf(george)
true

隐秘的__proto__链接

如你所知的,当你尝试接见一个不存在与当前对象的属性时,它会查询原型属性的属性。
让我们继续运用monkey对象作为Human()组织函数的原型属性。

var monkey = {
    feeds: 'bananas',
    breathes: 'air'
};
function Human() {}
Human.prototype = monkey;

接下来竖立一个developer对象,然后给他一些属性:

var developer = new Human();
developer.feeds = 'pizza';
developer.hacks = 'JavaScript';

如今,我们来做些查询吧。hacksdeveloper的属性:

>>> developer.hacks
"JavaScript"

feeds能够在对象中被找到:

>>> developer.feeds
"pizza"

breathes不存在于developer对象中,因为有一个隐秘的链接指向原型范例对象,所以转而查找原型范例。

>>> developer.breathes
"air"

能够从developer对象中获得原型属性对象呢?固然,能够啦。运用constructor作为中心对象,就像developer.constructor.prototype指向monkey一样。然则这并非非常牢靠的。因为constructor大多时刻用于供应信息的用处,而且是能够随时被掩盖修正的。你以致能够用一个不是对象的东西掩盖掉它。如许做涓滴不会影响到原型链的功用。

让我们看一些字符串的组织属性:

>>> developer.constructor = 'junk'
"junk"

看上去,prototype已乱成一团了:

>>> typeof developer.constructor.prototype
"undefined"

然则现实却并非如此,因为开发者依然呼吸着“氛围”(developerbreathes属性依然是air):

>>> developer.breathes
"air"

这示意原型属性的隐秘链接依然存在。在火狐阅读器中公然的这个隐秘链接是__proto__属性(proto前后各加两个下划线)。

>>> developer._proto__
Object feeds = bananas breathes=air

你能够在进修的过程当中运用这个隐秘链接,然则现实编码中不引荐运用。因为它不存在于Internet Explorer中,所以你的代码将会变得难以移植。打个比如,假如你运用monkey竖立了一堆对象,而且你如今想在一切的对象中变动一些东西。你能够修正monkey,而且一切的实例都邑继续这些变化。

>>> monkey.test = 1
1

>>> developer.test
1

__proto__不是等效于prototype__proto__是实例的一个属性,只管prototype是组织函数的一个属性。

>>> typeof developer.__proto__
"object"

>>> typeof developer.prototype
"undefined"

再次强调,你能够在Debug或者是进修的时刻运用__proto__,其他时刻不要。

扩大内建对象

内建的一些对象像组织函数Array,String,以致是ObjectFunction()都能够经由过程他们的原型属性来举行扩大。打个比如,你就能够向Array原型属性中增加新要领,而且它们能够在一切的数组中被运用。让我们来尝尝。
在PHP中,有一个函数叫做in_array(),它会通知你假如数组中是不是存在某个值。在JavaScript中,没有inArray()如许的函数,所以我们能够完成它,并增加到Array.prototype中。

Array.prototype.inArray = function(needle) {
    for (var i = 0, len = this.length; i < len; i++) {
        if (this[i] === needle) {
            return true;
        }
    }   
    return false; 
}

如今,一切的数组就都有新的要领了。让我尝尝:

>>> var a = ['red', 'green', 'blue']; 
>>> a.inArray('red');
true

>>> a.inArray('yellow');
false

真是简朴快速!让我再来做一个。设想一下你的顺序能够常常须要反转字符串吧,或许你会以为字符串对象应当有一个内建的reverse()要领,毕竟数组有reverse()要领。你能够轻松地增加reverse()要领给String的原型属性。阅读Array.prototype.reverse()(这和第四章末端的演习类似)。

String.prototype.reverse = function() {
    return Array.prototype.reverse.apply(this.split('')).join('');
}

这个代码运用split()运用字符串生成了一个数组,然后挪用了这个数组上的reverse()要领,生成了一个反转的数组。然后再运用join()将反转的数组变回了字符串。让我们尝尝新的要领:

>>> "Stoyan".reverse();
"nayotS"

扩大内建对象——议论

经由过程原型属性来扩大内建对象是一项强力的手艺,而且你能够用它来将JavaScript塑形成你想要的模样。你在运用这类强有力的要领之前都要彻底地思索清晰你的主意。
看看一个叫做Prototype的JavaScript库,它的作者太爱这个要领了,以致于连库的名字都叫这个了。运用这个库,你能够运用一些JavaScript要领,让运用JavaScript如Ruby言语一样天真。
YUI(雅虎用户界面)库是另一个比较盛行的JavaScript库。它的作者则是明确地阻挡这个范畴。他们不会以任何体式格局变动内建对象。不论你用的是什么库,修正中心对象都只会疑惑库的运用者,而且形成意料之外的毛病。
现实是,JavaScript发生了变化,阅读器也带来了支撑更多功用的新版本。如今你以为须要扩大到原型属性的缺失的功用,或许在来日诰日就变成了内建的要领。因而你的要领能够就不被须要了。然则假如你运用这类要领已写了许多代码而且你的要领又有些不同于内建的新内建完成呢?
最起码来讲你能做的是,在完成一个要领之前先去搜检一下它是不是存在。我们的上一个例子就应当像如许:

if (!String.prototype.reverse) {
  String.prototype.reverse = function() {   
    return Array.prototype.reverse.apply(this.split('')).join(''); 
  } 
}

一些原型属性的圈套

在处置惩罚原型属性的时刻,这两个征象是须要斟酌在内的:

  • <!–The prototype chain is live with the exception of when you completely replace the prototype object.–>

  • prototype.constructor是不牢靠的。

竖立一个简朴的构建函数和两个对象:

>>> function Dog(){ this.tail = true; }
>>> var benji = new Dog();
>>> var rusty = new Dog();

以致在竖立了对象以后,你仍能够向原型属性增加属性,而且对象会运用新的属性。让我们插进要领say()

>>> Dog.prototype.say = function(){ return 'Woof!';}

两个对象都邑运用新的要领:

>>> benji.say();
"Woof!"

>>> rusty.say();
"Woof!"

到此为止,假如你讯问你的对象,用来竖立他们的构建函数是什么,他们还会正确地报告:

>>> benji.constructor;
Dog();

>>> rusty.constructor;
Dog();

一个风趣的征象是假如你问原型属性的组织函数是什么,你依然会获得Dog(),他不算太正确。原型属性是Object()竖立的一个一般对象罢了。运用Dog()组织的不含任何属性的对象。

>>> benji.constructor.prototype.constructor
Dog()

>>> typeof benji.constructor.prototype.tail
"undefined"

如今我们用一个全新的对象完整掩盖原型属性对象:

>>> Dog.prototype = {paws: 4, hair: true};

这证实我们的旧对象不能接见新原型属性的属性。他们仍保持着与旧原型属性对象的隐秘链接。

>>> typeof benji.paws
"undefined"

>>> benji.say()
"Woof!"

>>> typeof benji.__proto__.say
"function"

>>> typeof benji.__proto__.paws
"undefined"

你再竖立新的对象,将会运用更新后的原型属性:

>>> var lucy = new Dog();
>>> lucy.say()
TypeError: lucy.say is not a function

>>> lucy.paws
4

指向新原型属性的私密链接__proto__

>>> typeof lucy.__proto__.say
"undefined"

>>> typeof lucy.__proto__.paws
"number"

新对象的构建函数属性不再被正确地报告出来了。原本应当指向Dog(),然则却指向了Object()

>>> lucy.constructor
Object()
>>> benji.constructor
Dog()

最难辨别的部份是当你查找组织函数的原型属性时:

>>> typeof lucy.constructor.prototype.paws
"undefined"
>>> typeof benji.constructor.prototype.paws
"number"

下面的语句将会修复上面一切的意料之外的征象:

>>> Dog.prototype = {paws: 4, hair: true};
>>> Dog.prototype.constructor = Dog;

当你掩盖原型属性,引荐重置constructor属性。

总结

让我们来总结一下这一章节中进修的几个要点。

  • 一切的函数都有一个叫作prototype的属性,初始情况下,它包括一个空缺的对象。

  • 你能够向原型属性中增加属性和要领。你以致能够将它完整替换成你挑选的对象。

  • 当你运用组织函数穿件对象(运用new),这个对象会有一个隐秘链接指向它的原型属性,而且能够把原型属性的属性当做本身的来用。

  • 比拟原型属性的属性,同名自身的属性具有更高的优先级。

  • 运用hasOwnProperty()要领来辨别自身属性和原型属性的属性。

  • 存在一个原型链:假如你的对象foo没有属性bar,当你运用foo.bar的时刻,JavaScript会从它的原型属性中去寻觅bar属性。假如没有找到,它会继续在原型属性的原型属性中找,然后是原型属性的原型属性的原型属性,而且一步一步向上,直到最高层父类Object

  • 你能够扩大内建组织函数。一切的对象都能够运用你的扩大。说明Array.prototype.flip,然后一切的数组都邑立时具有一个flip()要领。[1,2,3].flip()。在扩大要领和属性之前,搜检是不是存在,为你的代码增加将来的保证。

    原文作者:赤石俊哉
    原文地址: https://segmentfault.com/a/1190000008558983
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞