JavaScript函數(shù)(arguments,this)

JavaScript因?yàn)槠湔Z法松散浸间,導(dǎo)致函數(shù)(尤其是this)看似簡單玩徊,其實(shí)里面花頭很多僧叉。本篇介紹一下JavaScript函數(shù)及其調(diào)用方法拌滋。

  • 函數(shù)聲明和函數(shù)表達(dá)式
  • arguments
  • this
  • this補(bǔ)充說明

函數(shù)聲明和函數(shù)表達(dá)式

JavaScript里對象字面量產(chǎn)生的對象將被連接到Object.prototype,函數(shù)對象將被連接到Function.prototype(但該對象本身也連接到Object.prototype)雌芽。先看一下函數(shù)聲明和函數(shù)表達(dá)式(分匿名和命名):

function count(a,b){ return a*b; }              //函數(shù)聲明
var d1 = function(n) { return n*2; };           //匿名函數(shù)表達(dá)式
var d2 = function double(n) { return n*2; };    //命名函數(shù)表達(dá)式

console.log(count(3,4));    //12
console.log(d1(3));         //6
console.log(d2(3));         //6
console.log(double(3));    //error授艰,double未定義

上面代碼可以看出函數(shù)聲明和函數(shù)表達(dá)式在后續(xù)的調(diào)用中,效果是沒有差別的世落。除語法不同外淮腾,兩者的區(qū)別在于JS解析器讀取的順序。

解析器會(huì)事先讀取函數(shù)聲明屉佳,即使你把函數(shù)聲明放在代碼的末端也沒關(guān)系谷朝。而對于函數(shù)表達(dá)式,同其它基本類型的變量一樣武花,只有在執(zhí)行到該行語句時(shí)才解析圆凰。因此用函數(shù)表達(dá)式時(shí),必須確保它在調(diào)用語句之前体箕,否則會(huì)報(bào)錯(cuò)专钉。

再看匿名和命名函數(shù)表達(dá)式的區(qū)別。上例中命名函數(shù)表達(dá)式將函數(shù)綁定到變量d2上累铅,而非變量double上跃须,因此double(3);會(huì)出現(xiàn)未定義error。

那命名函數(shù)表達(dá)式有什么用呢争群?比如上面的變量double有什么用呢回怜?函數(shù)名double可用于在函數(shù)內(nèi)部做遞歸,但可惜仍舊沒必要换薄,因?yàn)樽兞縟2同樣也可以在函數(shù)內(nèi)部遞歸玉雾。因此命名函數(shù)表達(dá)式真正的作用在于調(diào)試,JavaScript環(huán)境提供對Error對象的棧追蹤功能轻要,可以用double進(jìn)行棧追蹤复旬。

但命名函數(shù)表達(dá)式仍舊有很多問題,類似with一樣冲泥。因此通常推薦用匿名函數(shù)表達(dá)式驹碍,不推薦用命名函數(shù)表達(dá)式:

var d1 = function(n) { return n*2; };           //Yes,推薦
var d2 = function double(n) { return n*2; };    //No凡恍,不推薦

arguments

每個(gè)函數(shù)都接受2個(gè)附加參數(shù):thisarguments志秃。先看arguments。JS的函數(shù)參數(shù)其實(shí)就是個(gè)類似數(shù)組的arguments對象嚼酝,是對形參的一個(gè)映射浮还,但是值是通過索引來獲取的。因此JS的函數(shù)天然支持可變參數(shù)闽巩。

arguments對象看似像數(shù)組钧舌,但請不要使用arguments.shift()等方法來修改arguments担汤。修改arguments對象將可能導(dǎo)致命名參數(shù)失去意義。

例如person(name, age)洼冻,參數(shù)name是arguments[0]的別名崭歧,age是arguments[1]的別名,如果用shift移除arguments后撞牢,name仍舊是arguments[0]的別名率碾,age仍舊是arguments[1]的別名,函數(shù)開始失控屋彪。

因此播掷,如果你無論如何要修改arguments,需要先將arguments對象轉(zhuǎn)化為真正的數(shù)組:

var args = [].slice.call(arguments);

之后對args對象進(jìn)行shift()等操作撼班。這也常見于獲取可變參數(shù)值,同樣需要上述那樣將arguments對象轉(zhuǎn)化為真正的數(shù)組垒酬。

另外每個(gè)arguments對象都有兩個(gè)額外的屬性:arguments.calleearguments.caller砰嘁。前者指向使用該arguments對象被調(diào)用的函數(shù)。后者指向調(diào)用該arguments對象的函數(shù)勘究。

其實(shí)arguments.callee除允許匿名函數(shù)遞歸調(diào)用自身外矮湘,并沒有什么太大用處。但可惜用函數(shù)名也能實(shí)現(xiàn)遞歸口糕,所以它真沒什么用處:

//用arguments.callee來遞歸
var factorial = (function(n) {
    return (n <= 1) ? 1 : (n * arguments.callee(n - 1));    //遞歸
});

//但也可以直接用函數(shù)名來遞歸
function factorial(n) {
    return (n <= 1) ? 1 : (n * factorial(n - 1));   
}

用arguments.caller可以跟蹤棧信息缅阳,但它不可靠,如果某函數(shù)在棧中出現(xiàn)了不止一次景描,很容易陷入死循環(huán)十办,大多數(shù)環(huán)境已經(jīng)移除了此特性。

JS嚴(yán)格模式下禁止使用arguments.callee和arguments.caller超棺,因此這兩個(gè)屬性就不多廢話了向族。

this

arguments介紹完后,再來看看this棠绘。在JS中this取決于調(diào)用的方式件相,不同的函數(shù)調(diào)用方式,this綁定的對象也不同氧苍。有5種調(diào)用方式:

  • 方法調(diào)用
  • 函數(shù)調(diào)用
  • 構(gòu)造器調(diào)用
  • apply / call / bind調(diào)用
  • =>箭頭函數(shù)調(diào)用

方法調(diào)用:當(dāng)函數(shù)作為對象方法時(shí)夜矗,函數(shù)里的this被綁定到該對象上

var myNum = {
    value: 0,
    increment: function(inc) {  //函數(shù)作為對象方法
        this.value += inc;
    }
};
myNum.increment(2);  
console.log(myNum.value);   //2,this被綁定到myNum

函數(shù)調(diào)用:函數(shù)非對象方法時(shí)让虐,this被綁定到全局對象window紊撕。這其實(shí)是語言設(shè)計(jì)上的一個(gè)錯(cuò)誤(或曰特性),導(dǎo)致this不能調(diào)用內(nèi)部函數(shù)澄干。要調(diào)用內(nèi)部函數(shù)逛揩,可以將that = this保存起來柠傍。

function double(n){ return n*2; }      //普通函數(shù),this綁定到全局對象window

//錯(cuò)誤的例子
myNum.count = function() {
    var helper = function() {
        this.value = double(this.value);
    };
    helper();
}
myNum.count();
console.log(myNum.value);     //value不變

//正確的例子:
myNum.count = function() {
    var that = this;       
    var helper = function() {
        that.value = double(that.value);   //現(xiàn)在參數(shù)是myNum.value
    };
    helper();
}
myNum.count();
console.log(myNum.value);     //4

錯(cuò)誤的例子中辩稽,期望this綁定的是對象myNum惧笛,但由于double是普通函數(shù),因此this綁定的是window逞泄,而window顯然沒有value患整。即helper里this是window,因此double(this.value);不會(huì)被執(zhí)行喷众。最終myNum的value值并沒有變各谚。

正確的例子在對象myNum方法里,this綁定的是myNum對象到千,因此先用that將this保存起來昌渤。然后在內(nèi)部傳遞的都是that,回避了helper函數(shù)內(nèi)this發(fā)生改變的問題憔四。

構(gòu)造函數(shù)調(diào)用:用new調(diào)用構(gòu)造函數(shù)膀息,會(huì)先創(chuàng)建一個(gè)連接到構(gòu)造函數(shù)的prototype的新對象,再將this會(huì)綁定到該新對象

var Name = function(n) { 
    this.name = n; 
}
Name.prototype.getName = function() {
    return this.name;
}
var myName = new Name("Jack");      //this綁定到myName對象
console.log(myName.getName());          //Jack

apply / call / bind調(diào)用:允許我們自己綁定想要的this

var friend = {
    name: "Betty"
};
console.log(Name.prototype.getName.apply(friend));    //Betty
console.log(Name.prototype.getName.call(friend));     //Betty
console.log(Name.prototype.getName.bind(friend)());   //Betty

=>箭頭函數(shù)調(diào)用:ES6里的箭頭函數(shù)里的this指向定義時(shí)所在的對象了赵,而非使用時(shí)所在的對象潜支。這意味著=>里的this是固定不變的流济。例如:

function foo() {
  setTimeout(() => {
    console.log('id:', this.id);
  }, 100);
}
var id = 10;
foo.call({ id: 20 }); // id: 20

上例中崖咨,setTimeout的參數(shù)是一個(gè)箭頭函數(shù),該箭頭函數(shù)被定義在普通函數(shù)foo內(nèi)术健。如果不是ES6的箭頭函數(shù)络断,而是ES5普通函數(shù)的話裁替,參照上面函數(shù)調(diào)用的this的說明, 100毫秒后執(zhí)行時(shí)this應(yīng)該指向全局對象window妓羊,應(yīng)該輸出10胯究。但ES6的箭頭函數(shù)里this總是指向被定義時(shí)所在的對象({id: 20}),所以結(jié)果是20躁绸。

為了更清晰地分辨ES6箭頭函數(shù)和ES5普通函數(shù)對this綁定的區(qū)別裕循,再看一個(gè)例子:

function Timer() {
  this.count1 = 0;
  this.count2 = 0; 
  setInterval(function () { this.count1++; }, 1000); // 普通函數(shù)
  setInterval(() => this.count2++, 1000);               // 箭頭函數(shù)
} 

var timer = new Timer();
setTimeout(() => console.log('count1: ', timer.count1), 3100); // count1: 0
setTimeout(() => console.log('count2: ', timer.count2), 3100); // count2: 3

上例中,Timer函數(shù)內(nèi)部的兩個(gè)定時(shí)器净刮,分別用了ES6箭頭函數(shù)和ES5普通函數(shù)剥哑。前者的this指向運(yùn)行時(shí)所在的作用域window,后者的this指向定義時(shí)所在的作用域Timer函數(shù)淹父。結(jié)果普通函數(shù)內(nèi)的count一次都沒被更新株婴,而箭頭函數(shù)的內(nèi)的count被正確更新了3次。

ES6的箭頭函數(shù)顯然更能預(yù)防this錯(cuò)誤綁定的問題,因此推薦用ES6的新語法來寫JS困介。例如以前在定義回調(diào)函數(shù)前大审,總是先寫:

var _this = this;
var that = this;
var self = this;

你一定見過這些先將this保存起來,再在回調(diào)函數(shù)里需要用this的地方用_this / that / self來代替座哩,就是為了解決(更精確地說是回避)回調(diào)函數(shù)執(zhí)行時(shí)this綁定的問題⊥椒觯現(xiàn)在用ES6的箭頭函數(shù)就不需要這么麻煩了:

var handler = {
  id: '123456',
  init: function() {
    document.addEventListener('click',
    event => this.doSomething(event.type), false);
  },
  doSomething: function(type) {console.log('Handling ' + type + ' for ' + this.id);}
};

上例中init方法內(nèi)用了箭頭函數(shù),因此內(nèi)部的this根穷,總是指向handler對象姜骡。否則,回調(diào)函數(shù)運(yùn)行時(shí)屿良,由于this指向的是window圈澈,所以this.doSomething會(huì)報(bào)錯(cuò)。

究其本質(zhì)尘惧,ES6的箭頭函數(shù)能將this綁定固化康栈,并不是增加了什么新語法,本質(zhì)上就是ES5的語法糖喷橙。箭頭函數(shù)沒有自己的this谅将,它的this其實(shí)就是外層代碼塊的this。將箭頭函數(shù)用Babel轉(zhuǎn)碼一下:

// ES6的代碼
function foo() {
  setTimeout(() => {
    console.log('id:', this.id);
  }, 100);
}

// 轉(zhuǎn)碼后ES5的代碼
function foo() {
  var _this = this;
  setTimeout(function () {
    console.log('id:', _this.id);
  }, 100);
}

戲法拆穿就顯得了無生趣了重慢,仍舊是ES5那套_this / that / self的把戲。

function foo() {
  return () => {
    return () => {
      return () => {
        console.log('id:', this.id);
      };
    };
  };
}
var f = foo.call({id: 1});
var t1 = f.call({id: 2})()(); // id: 1
var t2 = f().call({id: 3})(); // id: 1
var t3 = f()().call({id: 4}); // id: 1

上例中逊躁,看似嵌套了很多箭頭函數(shù)似踱,有點(diǎn)暈。但只要知道了這是ES5老把戲的語法糖的話稽煤,就不會(huì)暈了核芽。整個(gè)foo里只有一個(gè)this,指向foo對象酵熙。不論嵌套多少層轧简,語法糖里箭頭函數(shù)沒有自己的this,只有_this即指向foo的this匾二。

箭頭函數(shù)沒有自己的this哮独,因此不能作為構(gòu)造函數(shù)被使用,new會(huì)報(bào)錯(cuò)察藐。也沒有自身的arguments皮璧,super,new.target分飞,它們指向的都是外層代碼塊的對應(yīng)變量悴务。也不能使用yield用作Generator函數(shù)。也不能用call() / apply() / bind()這些方法去改變this的指向譬猫。

this補(bǔ)充說明

這一節(jié)并無任何新的內(nèi)容讯檐,只不過對this進(jìn)一步補(bǔ)充說明一下羡疗。我們知道對象都有prototype俗稱原型對象。那prototype里的this綁定誰呢别洪?其實(shí)原則沒有變叨恨,從上面構(gòu)造函數(shù)調(diào)用的例子就能看出this仍舊是綁定調(diào)用的對象。

為了更清晰一點(diǎn)蕉拢,將上面構(gòu)造函數(shù)調(diào)用的例子稍微改一下:

var Name = function() {};
Name.prototype =  {
    name: "(not set)",
    setName: function(n) {
        this.name = n;
    }
}

var myName = new Name();
console.log(myName.name);                   //(not set)
console.log(myName.hasOwnProperty("name"));    //false
console.log(myName.hasOwnProperty("setName"));   //false

myName.setName("Jack");
console.log(myName.name);                       //Jack
console.log(myName.hasOwnProperty("name"));    //true
console.log(myName.hasOwnProperty("setName"));   //false

先看第一段結(jié)果代碼特碳,Name本身沒有任何屬性,name和setName是在它的原型prototype中定義的晕换。因此用hasOwnProperty來檢查全是false午乓。這與我們的預(yù)想完全一致,沒什么可奇怪的闸准。

再看第二段結(jié)果代碼益愈,由于執(zhí)行了myName.setName("Jack");。原型prototype中的this不是綁定原型對象夷家,而是綁定調(diào)用的對象蒸其。即setName中的this綁定的是對象myName,會(huì)給對象增加一個(gè)name屬性库快。所以hasOwnProperty("name")會(huì)為true摸袁。

明白這些原理后,再回過頭看看以前不明白的代碼里this义屏,that靠汁,self等就輕松多了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末闽铐,一起剝皮案震驚了整個(gè)濱河市蝶怔,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌兄墅,老刑警劉巖踢星,帶你破解...
    沈念sama閱讀 212,383評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異隙咸,居然都是意外死亡沐悦,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門五督,熙熙樓的掌柜王于貴愁眉苦臉地迎上來所踊,“玉大人,你說我怎么就攤上這事概荷★醯海” “怎么了?”我有些...
    開封第一講書人閱讀 157,852評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長继薛。 經(jīng)常有香客問我修壕,道長,這世上最難降的妖魔是什么遏考? 我笑而不...
    開封第一講書人閱讀 56,621評(píng)論 1 284
  • 正文 為了忘掉前任慈鸠,我火速辦了婚禮,結(jié)果婚禮上灌具,老公的妹妹穿的比我還像新娘青团。我一直安慰自己,他們只是感情好咖楣,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,741評(píng)論 6 386
  • 文/花漫 我一把揭開白布督笆。 她就那樣靜靜地躺著,像睡著了一般诱贿。 火紅的嫁衣襯著肌膚如雪娃肿。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,929評(píng)論 1 290
  • 那天珠十,我揣著相機(jī)與錄音料扰,去河邊找鬼。 笑死焙蹭,一個(gè)胖子當(dāng)著我的面吹牛晒杈,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播孔厉,決...
    沈念sama閱讀 39,076評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼桐智,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了烟馅?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,803評(píng)論 0 268
  • 序言:老撾萬榮一對情侶失蹤然磷,失蹤者是張志新(化名)和其女友劉穎郑趁,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體姿搜,經(jīng)...
    沈念sama閱讀 44,265評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡寡润,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,582評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了舅柜。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片梭纹。...
    茶點(diǎn)故事閱讀 38,716評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖致份,靈堂內(nèi)的尸體忽然破棺而出变抽,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 34,395評(píng)論 4 333
  • 正文 年R本政府宣布绍载,位于F島的核電站诡宗,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏击儡。R本人自食惡果不足惜塔沃,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,039評(píng)論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望阳谍。 院中可真熱鬧蛀柴,春花似錦、人聲如沸矫夯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽茧痒。三九已至肮韧,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間旺订,已是汗流浹背弄企。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評(píng)論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留区拳,地道東北人拘领。 一個(gè)月前我還...
    沈念sama閱讀 46,488評(píng)論 2 361
  • 正文 我出身青樓,卻偏偏與公主長得像樱调,于是被迫代替她去往敵國和親约素。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,612評(píng)論 2 350

推薦閱讀更多精彩內(nèi)容