深入理解 Javascript 原型

原文github地址

var str = 'Hello world!';    // var str = new String('Hello world!')
str.substr(2, 4);
str.indexOf('world');

我們經(jīng)??梢杂玫降淖址瘮?shù) substr、replaceindexOf等,是因?yàn)?String 對(duì)象上的 prototype 預(yù)先定義了這些方法。strString 的一個(gè)實(shí)例。

JavaScript標(biāo)準(zhǔn)庫(kù)中常用的內(nèi)置對(duì)象上 prototype 的方法還有:
Array?.prototype?.push、 Array?.prototype?.push
Date?.prototype?.get?Date 、 Date?.prototype?.get?Year
Function?.prototype?.toStringFunction?.prototype?.call
...

我們使用構(gòu)造函數(shù)創(chuàng)建一個(gè)對(duì)象:

function User() {
}
var user = new User();
user.name = '張三';
console.log(user.name) // 張三

在這個(gè)例子中,User 是一個(gè)構(gòu)造函數(shù),我們使用 new 創(chuàng)建了一個(gè)實(shí)例對(duì)象 user

prototype


JavaScript 不包含傳統(tǒng)的類繼承模型,而是使用 prototype 原型模型。
每個(gè)函數(shù)都有一個(gè) prototype 屬性,換句話說(shuō), prototype 是函數(shù)才會(huì)有的屬性

function User() {
}
User.prototype.name = '張三';
var user1 = new User();
var user2 = new User();
console.log(user1.name) // 張三
console.log(user2.name) // 張三

函數(shù)的 prototype 屬性指向了一個(gè)對(duì)象,這個(gè)對(duì)象正是調(diào)用該構(gòu)造函數(shù)而創(chuàng)建的 實(shí)例 的原型。也就是說(shuō)這個(gè)例子中 User 的屬性 prototype 對(duì)象是 user1 和 user2 的原型。

既然函數(shù)的 prototype 屬性指向了一個(gè)對(duì)象,我們可以重寫原型對(duì)象

function User() {}
User.prototype = {
    name: '張三',
    greeting: function() {
        console.log('hello!')
    }
};
var user = new User();
console.log(user.name);   // 張三
user.greeting();  // hello!

這樣,我們就可以 new User 對(duì)象以后,就可以調(diào)用 greeting 方法了。

然而將原子類型賦給 prototype 的操作將會(huì)被忽略

function User() {}
User.prototype = 1 // 無(wú)效

我們還可以在賦值原型 prototype 的時(shí)候使用 function 立即執(zhí)行的表達(dá)式來(lái)賦值:

function User() {}
User.prototype = function() {
    name = '張三',
    greeting = function() {
        console.log('hello!', this.name)
    }
    return {
        name: name,
        greeting: greeting
    }
}();
(new User()).greeting();

它的好處就是可以封裝私有的 function,通過(guò) return 的形式暴露出簡(jiǎn)單的使用名稱,以達(dá)到public/private的效果。

上述使用原型的時(shí)候,都是直接賦值原型對(duì)象,這樣會(huì)覆蓋之前已定義好的原型,導(dǎo)致之前原型上的方法或?qū)傩詠G失,所以通常分開(kāi)設(shè)置/覆蓋 一個(gè)已知函數(shù)的 prototype

User.prototype.update = function() {} 

_proto_


所有 JavaScript 對(duì)象(null除外)都有的一個(gè) __proto__ 屬性,這個(gè)屬性指向該對(duì)象的原型

function User() {
}
var user1 = new User();
var user2 = new User();
console.log(user1.__proto__ === User.prototype); // true
console.log(user1.__proto__ === user2.__proto__); // true

不管你創(chuàng)建多少個(gè) User 對(duì)象實(shí)例,他們的原型指向的都是同一個(gè) User.prototype

注: _proto_ 并不是語(yǔ)言本身的特性,這是各大廠商具體實(shí)現(xiàn)時(shí)添加的私有屬性,不建議在生產(chǎn)中使用該屬性,我們可以使用ES5的方法 Object.getPrototypeOf 方法來(lái)獲取實(shí)例對(duì)象的原型。

Object.getPrototypeOf(user) === User.prototype

當(dāng)查找一個(gè)對(duì)象的屬性時(shí),JavaScript 會(huì)向上遍歷原型鏈,直到找到給定名稱的屬性為止。

到查找到達(dá)原型鏈的頂部 - 也就是 Object.prototype - 但是仍然沒(méi)有找到指定的屬性,就會(huì)返回 undefined。

function User() {
    this.name = '張三'
}

User.prototype.name = '李四';

Object.prototype.age = 20

var user = new User();
console.log(user.name);  // 張三
console.log(user.age);  // 20

delete user.name
console.log(user.name);  // 李四

如果一個(gè)屬性在原型鏈的上端,則對(duì)于查找時(shí)間將帶來(lái)不利影響。特別的,試圖獲取一個(gè)不存在的屬性將會(huì)遍歷整個(gè)原型鏈。

并且,當(dāng)使用 for in 循環(huán)遍歷對(duì)象的屬性時(shí),原型鏈上的所有屬性都將被訪問(wèn)。

所以在使用 for in loop 遍歷對(duì)象時(shí),推薦總是使用 hasOwnProperty 方法, 這將會(huì)避免原型對(duì)象擴(kuò)展帶來(lái)的干擾。

for(var i in obj) {
    if (obj.hasOwnProperty(i)) {
        console.log(i);
    }
}

構(gòu)造函數(shù)


通過(guò) new 關(guān)鍵字方式調(diào)用的函數(shù)都被認(rèn)為是構(gòu)造函數(shù)

function User() { }
var user = new User();
// user.constructor === user.__proto__.constructor 
console.log(user.constructor === User ); // true
console.log(User.prototype.constructor === User); // true

原型 constructor 屬性指向構(gòu)造函數(shù),在構(gòu)造函數(shù)內(nèi)部,this 指向新創(chuàng)建的對(duì)象 Object

如果被調(diào)用的函數(shù)沒(méi)有顯式的 return 表達(dá)式,則隱式的會(huì)返回 this 對(duì)象 - 也就是新創(chuàng)建的對(duì)象。

顯式的 return 表達(dá)式將會(huì)影響返回結(jié)果,但總是會(huì)返回的是一個(gè)對(duì)象。

function Bar() {
    return 2;
}
var bar = new Bar();  // 返回新創(chuàng)建的對(duì)象,而不是數(shù)字的字面量 2
console.log(bar.constructor === Bar);  // true
function Foo() {
    this.a = 1;
    
    return {
        b: 2
    };
}

Foo.prototype.c = 3;

var foo = new Foo(); // 返回的對(duì)象 {b: 2}
console.log(foo.constructor === Foo);  // false
console.log(foo.a);  // undefined
console.log(foo.b);  // 2
console.log(foo.c);  // undefined

這里得到的 foo 是函數(shù)返回的對(duì)象,而不是通過(guò)new關(guān)鍵字新創(chuàng)建的對(duì)象。new Foo() 并不會(huì)改變返回的對(duì)象 foo 的原型, 也就是返回的對(duì)象 foo 的原型不會(huì)指向 Foo.prototype 。 因?yàn)闃?gòu)造函數(shù)的原型會(huì)被指向到新創(chuàng)建的對(duì)象,而這里的 Foo 沒(méi)有把這個(gè)新創(chuàng)建的對(duì)象返回,而是返回了一個(gè)包含 b 屬性的自定義對(duì)象。

如果 new 被遺漏了,則函數(shù)不會(huì)返回新創(chuàng)建的對(duì)象。

function Foo() {
    this.abc = 1; // 獲取設(shè)置全局參數(shù) this===window
}
Foo(); // undefined

為了不使用 new 關(guān)鍵字,經(jīng)常會(huì)使用工廠模式創(chuàng)建一個(gè)對(duì)象。

function Foo() {
    var obj = {};
    obj.value = 'blub';

    var private = 2;
    obj.setValue = function(value) {
        this.value = value;
    }

    obj.getPrivate = function() {
        return private;
    }
    return obj;
}

上面的方式看起來(lái)出錯(cuò),并且可以使用閉包來(lái)達(dá)到封裝私有變量, 但是隨之而來(lái)的是一些不好的地方。

  • 為了實(shí)現(xiàn)繼承,工廠方法需要從另外一個(gè)對(duì)象拷貝所有屬性
  • 新創(chuàng)建的實(shí)例不能共享原型對(duì)象上的方法/屬性,會(huì)占用更多的內(nèi)存

繼承


下面通過(guò) call 實(shí)現(xiàn)繼承,并將父級(jí) prototype 給子 prototype

function Parent() {
    this.value = 1
}
Parent.prototype.method = function() {
    console.log('value: ' + this.value)
}

function Child() {
    // this -> new Child()
    Parent.call(this);  // 調(diào)用Parent構(gòu)造函數(shù)
}

var c1 = new Child();
console.log(c1.value);  // 1

c1.method(); // 報(bào)錯(cuò):Uncaught TypeError: c1.method is not a function

Child.prototype = Parent.prototype;  //繼承父方法
var c2 = new Child();

c2.method();  // 'value: 1'

但是這樣繼承存在一個(gè)問(wèn)題,接上面代碼繼續(xù)

Child.prototype.fn = function() {
    console.log('abc')
}

var p = new Parent();
p.fn();  // 'abc'

因?yàn)?Parent.prototype === Child.prototype,原型是同一個(gè)引用,可以直接將子類prototype 與 父類分離

for(var i in Parent.prototype) {
    Child.prototype[i] = Parent.prototype[i]
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 前言 原型,作為前端開(kāi)發(fā)者,或多或少都有聽(tīng)說(shuō)。你可能一直想了解它,但是由于各種原因還沒(méi)有了解,現(xiàn)在就跟隨我來(lái)一起探...
    無(wú)亦情閱讀 731評(píng)論 0 8
  • 王福朋 - 博客園 —— 《 深入理解javascript原型和閉包》 目錄:深入理解javascript原型和閉...
    帥而不花__美而不浪閱讀 1,632評(píng)論 0 2
  • 作為一門被長(zhǎng)期誤解的編程語(yǔ)言,javascript一直被人所詬病.但是如果你真正的了解它之后,你會(huì)深深的愛(ài)上它. ...
    余歌_非魚閱讀 2,070評(píng)論 3 9
  • 1.對(duì)象是什么 對(duì)象就是若干屬性的集合。 在JS中一切引用類型都是對(duì)象:數(shù)組是對(duì)象,函數(shù)是對(duì)象,對(duì)象還是對(duì)象。對(duì)象...
    liushaung閱讀 1,288評(píng)論 0 2
  • 前言 在軟件工程中,代碼重用的模式極為重要,因?yàn)樗麄兛梢燥@著地減少軟件開(kāi)發(fā)的成本。在那些主流的基于類的語(yǔ)言(比如J...
    sponia_joker閱讀 319評(píng)論 0 2

友情鏈接更多精彩內(nèi)容