天天看點

[Effective JavaScript 筆記]第20條:使用call方法自定義接收者來調用方法

[Effective JavaScript 筆記]第20條:使用call方法自定義接收者來調用方法

函數或方法的接收者(即綁定到特殊關鍵字this的值)是由調用者的文法決定的。方法調用文法将方法被查找的對象綁定到this變量,(可參閱之前文章《了解函數調用、方法調用及構造函數調用之間的不同》)。有時需要使用自定義接收者來調用函數,因為該函數可能并不是期望的接收者對象的屬性。可以将方法作

不好的實踐

函數或方法的接收者(即綁定到特殊關鍵字this的值)是由調用者的文法決定的。

方法調用文法将方法被查找的對象綁定到this變量,(可參閱之前文章《了解函數調用、方法調用及構造函數調用之間的不同》)。

有時需要使用自定義接收者來調用函數,因為該函數可能并不是期望的接收者對象的屬性。

可以将方法作為一個新的屬性添加到接收者對象中。使用如下代碼:

var obj={
   temporary:function(a,b,c){console.log('obj')} 
}
var f=function(arg1,arg2,arg3){console.log('f')};
var arg1=0,arg2=1,arg3=2;

obj.temporary=f;//辨別1
var result=obj.temporary(arg1,arg2,arg3);
delete obj.temporary;
      

辨別1,如果temporary的屬性名在對象中已經存在,則會對對象造成功能的破壞。而且取任何名稱,都沒法確定不和原來的對象名重名,可能這個對象并不是自己建立的。對象可以對屬性進行當機或密封以防止修改和添加(詳細見附錄)任何新屬性。

函數對象call方法

基本描述:用途是在特定的作用域下來執行函數,實際上等于設定函數體内this對象的值。

函數對象具有一個内置的方法call來自定義接收者。

f.call(obj,arg1,arg2,arg3)
      

此行為和直接調用函數自身很相似。

f(arg1,arg2,arg3)
      

不同點在于,第一個參數指定了一個顯式的接收者對象(即指定函數内部this的指向)

1、當調用的方法已經被删除、修改或者覆寫時,call方法就可以派上用場了。

以hasOwnProperty方法為例,因為這個方法是Object的方法,所有對象都可以通路調用這個方法。

var obj={
    foo:'good'
};
obj.hasOwnProperty('foo');//true
      

然後當這個方法被覆寫時

obj.hasOwnProperty=1;
obj.hasOwnProperty('foo');//Uncaught TypeError: obj.hasOwnProperty is not a function(…)
      

這時,call方法就可以派上用場了

var hasOwnProperty={}.hasOwnProperty;
delete obj.hasOwnProperty;
hasOwnProperty.call(obj,'foo');//true
hasOwnProperty.call(obj,'hasOwnProperty');//false      

2、定義高階函數時call方法也特别實用。

高階函數的一個慣用法是接收一個可選的參數作為調用該函數的接收者。

示例:有一個表示鍵值對清單的對象,提供了名為forEach的方法。

var table={
    entries:[],
    addEntry:function(key,value){
        this.entries.push({key:key,value:value});
    },
    forEach:function(f,thisArg){
        var entries=this.entries;
        for(var i=0,n=entries.length;i<n;i++){
            var entry=entries[i];
            f.call(thisArg,entry.key,entry.value,i);
        }
    }
};
      

上面這個例子裡允許table對象的使用者,将一個方法作為table.forEach的回調函數f,并為該方法提供一個合理的接收者。例如,實作将table的内容複制到另一個中。

table1.forEach(table2.addEntry,table2);
      

我們把上面的代碼直接帶入上面的代碼大家可以晰看到它的執行過程。

var entries=table1.entries;
for(var i=0,n=entries.length;i<n;i++){
    var entry=entries[i];
    table2.addEntry.call(table2,entry.key,entry.value,i);
}
      

這段代碼從table2中提取addEntry方法,forEach方法将table2作為接收者,并反複調用該addEntry方法。

提示

  • 使用call方法自定義接收者來調用函數
  • 使用call方法可以調用在給定的對象中不存在的方法
  • 使用call方法定義高階函數允許使用者給回調函數指定接收者

附錄一:對象屬性

注:以下内容出自《javascript進階程式設計語言》第3版

屬性類型

ECMAScript-262第5版在定義隻有内部才用的特性時,描述了屬性的特征。描述這些特性是為了實作JS引擎用的,是以js中不能直接通路它們。為了表示特性是内部值,該規範把它們放到了兩對兒方括号中。

例如:[[Enumerable]]。

ECMAScript中有兩種屬性:資料屬性和通路器屬性。

資料屬性

資料屬性包含一個資料值的位置。在這個位置可以讀取和寫入值。

資料屬性有4個描述其行為的特性。

  • [[Configurable]]:表示能否通過delete删除屬性進而重新定義屬性,能否修改屬性的特性,或者能否把屬性修改為通路器屬性。
  • [[Enumerable]]:表示能否通過for-in循環傳回屬性。像前面例子中那樣直接在對象上定義的屬性,它們的這個特性預設值為true。
  • [[Writable]]:表示能否修改屬性的值。
  • [[Value]]:包含這個屬性的資料值。讀取屬性值的時候,這個位置讀;寫入屬性值的時候,把新值儲存在這個位置。這個特性的預設值為undefined。

示例:

var person={
    name:'Nicholas'
}
      

這裡建立的一個名為name的屬性,為它指定的值是'Nicholas'。

也就是說person對象的name屬性的[[Value]]特性将被設定為'Nicholas',對這個值的任何修改都将反映在這個位置。

修改屬性的方法

修改屬性預設特性的方法,必須使用 ES5的Object.defineProperty()方法。

這個方法接收三個參數:

  • 屬性所在的對象
  • 屬性的名字
  • 一個描述符對象

描述符對象的屬性必須是:configurable,enumerable,writable和value。設定其中的一或多個值,可以修改對應的特性值。

例如:

var person={};
Object.defineProperty(preson,'name',{
    writable:false,
    value:'li lei'
});
person.name;//"li lei"
person.name='han mei mei';
person.name;//"li lei"
      

這裡把name屬性設定為隻讀的屬性,是以無法對name進行修改。嚴格模式下會報錯。

下面是一個不可配置的示例:

var person={};
Object.defineProperty(preson,'name',{
    configurable:false,
    value:'li lei'
});
person.name;//"li lei"
delete person.name;
person.name;//"li lei"
      

把configurable設定為false,表示不能從對象中删除屬性。如果在嚴格模式下,上面的代碼會報錯。一旦把屬性定義為不可配置的,就不能再把它變回為可配置的。此時再調用Object.defineProperty()方法修改除writable之外的特性,都會導緻錯誤。

注意:在調用Object.defineProperty()方法時,如果不指定,configurable,enumerable,writable特性的預設值都是false。多數情況下,都沒必要利用Object.defineProperty()方法提供的這些的進階功能。對于了解js對象非常有用。

通路器屬性

通路器屬性不包含資料值;它們包含一對兒getter和setter函數(不過,這兩個函數都不是必需的)。

讀取通路器屬性時,會調用getter函數,這個函數負責傳回有效的值;在寫入通路器屬性時,會調用setter函數并傳入新值,這個函數負責決定如何處理資料。

通路器屬性有如下4個特性:

[[Configurable]]:表示能否通過delete删除屬性進而重新定義屬性,能否修改屬性的特性或者能否把屬性修改為資料屬性。對于直接在對象上定義的屬性,這個特性的預設值為true。

[[Enumerable]]:表示能否通過for-in循環傳回屬性。對于直接在對象上定義的屬性,這個特性的預設值為true。

[[Get]]:在讀取屬性時調用的函數。預設值為undefined。

[[Set]]:在寫入屬性時調用的函數。預設值為undefined。

通路器屬性不能直接定義,必須使用Object.defineProperty()來定義。

示例:

var book={
    _year:2004,
    edition:1
};

Object.defineProperty(book,'year',{
    get:function(){
        return this._year;
    },
    set:function(newVal){
        if(newVal>2004){
            this._year=newVal;
            this.edition+=newVal-2004;
        }
    }
});
book.year=2005;
book.edition;//2
book._year;//2005
book.year;//2005
      

 定義多個屬性

直接上代碼

var book={};
Object.defineProperties(book,{
	_year:{
		value:2004
	},
	edition:{
		value:1
	},
	year:{
		get:function(){
			return this._year;
		},
		set:function(newVal){
			if(newVal>2004){
				this._year=newVal;
				this.edition+=newVal-2004;
			}
		}
	}
})
      

Object.defineProperties()方法,可能通過描述符一次定義多個屬性。

接收兩個對象參數:

  • 第一個對象是要添加和修改其屬性的對象
  • 第二個對象的屬性與第一個對象中要添加或修改的屬性一一對應

讀取屬性的特性

Object.getOwnPropertyDescriptor()方法,可以取得給定屬性的描述符。

接收兩個參數:

  • 要讀取其描述符的屬性名稱

傳回值是一個對象,視屬性的類型不同,對象的屬性也不同,具體參見上面的屬性類型。

示例

var book={};
Object.defineProperties(book,{
	_year:{
		value:2004
	},
	edition:{
		value:1
	},
	year:{
		get:function(){
			return this._year;
		},
		set:function(newVal){
			if(newVal>2004){
				this._year=newVal;
				this.edition+=newVal-2004;
			}
		}
	}
})
var descriptor=Object.getOwnPropertyDescriptor(book,'_year');
descriptor;//Object {value: 2004, writable: false, enumerable: false, configurable: false}
descriptor=Object.getOwnPropertyDescriptor(book,'year');
descriptor;//Object {configurable:false,enumerable:false,get:function(){...},set:function(newVal){...}
      

 

版權聲明

翻譯的文章,版權歸原作者所有,隻用于交流與學習的目的。

原創文章,版權歸作者所有,非商業轉載請注明出處,并保留原文的完整連結。

繼續閱讀