var str = 'Hello world!'; // var str = new String('Hello world!')
str.substr(2, 4);
str.indexOf('world');
我們經(jīng)??梢杂玫降淖址瘮?shù) substr、replace、indexOf等,是因?yàn)?String 對(duì)象上的 prototype 預(yù)先定義了這些方法。str 為 String 的一個(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?.toString 、 Function?.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]
}