摘要:函數的調用有五種模式方法調用模式,函數調用模式,構造器調用模式,調用模式以及回調模式,下面分別對這幾種模式進行說明。構造器調用模式構造函數的調用方式被稱為構造器調用模式,這是模擬類繼承式語言的一種調用方式。
函數的調用有五種模式:方法調用模式,函數調用模式,構造器調用模式,apply/call調用模式以及回調模式,下面分別對這幾種模式進行說明。
1.函數調用與方法調用模式:
1.1 聲明一個函數并調用它就是函數調用模式,這是最簡單的調用,但其中也關系到this的指向問題。普通函數是將this默認綁定到全局對象,而箭頭函數時不綁定this的,在函數所在的父作用域外面this指向哪里,在箭頭函數內部this也指向哪里。
function show(name) { console.log(name); } show("shotar"); // shotar // 普通函數的符符作用域this指向全局作用域,在調用的時候再一次綁定this到全局作用域 function say() { console.log(this); } say(); // 瀏覽器環境輸出window node環境輸出global // 箭頭函數父作用域的this指向全局對象,調用的時候并沒有綁定this,而是繼承父作用域后指向全局對象 var sayName = () => { console.log(this); } sayName() // window
1.2 方法調用時將一個函數作為對象的方法調用,作為方法調用的函數會將this綁定到該對象,但如果方法內部再嵌套一個函數,內部函數再次調用的時候又屬于函數調用模式,此時this又將綁定到全局對象。
window.name = "Jane"; // node環境下是global var obj = { name: "shotar", sayName: function() { console.log(1, this.name); sayWindowName(); function sayWindowName() { console.log(2, this.name); } } }; obj.sayName(); // 1, shotar 2, Jane
如果想讓內部函數(sayWindowName)指向該對象也很簡單,在此列舉三種方法。第一種是在外部將this保存到一個變量里面,再在內部函數中使用即可。
window.name = "Jane"; var obj = { name: "shotar", sayName: function() { var _this = this; console.log(1, this.name); sayWindowName(); function sayWindowName() { console.log(2, _this.name); } } }; obj.sayName(); // 1, shotar 2, shotar
第二種解決辦法是使用ES6的箭頭函數,箭頭函數不綁定this,父作用域的this是哪個對象在箭頭函數中的this仍然是哪個對象(注意:箭頭函數只能使用函數字面量的形式命名函數名,調用也要在語句之后)。
window.name = "Jane"; var obj = { name: "shotar", sayName: function() { console.log(1, this.name); var sayWindowName = () => { console.log(2, _this.name); }; sayWindowName(); } }; obj.sayName(); // 1, shotar 2, shotar
第三種使用call或apply方法是改變內部函數的this值。
window.name = "Jane"; var obj = { name: "shotar", sayName: function() { console.log(1, this.name); sayWindowName.call(this); // 或 sayWindowName.apply(this); function sayWindowName() { console.log(2, this.name); } } }; obj.sayName(); // 1, shotar 2, shotar
在此說明一下阮大大在ES6標準入門里面列舉的關于箭頭函數this指向的例子,因為在foo函數的作用域下指向window的,使用函數調用模式調用foo函數,setTimeout內的箭頭函數不綁定this,還是指向父作用域foo函數所指向的this。foo是普通函數,他將this指向全局對象,因此箭頭函數也指向全局變量。這時會打印undefined,為什么又會打印出undefined呢,這是因為在聲明id的時候使用了var關鍵字,他是一個變量并不是全局對象(window或global)的屬性,如果將var id = 21;這句改為window.id = 21;(或者global.id = 21)后將打印出21。使用call方法調用會改變this的值,下面到call/apply調用模式的時候會講到。
function foo() { setTimeout(() => { console.log("id:", this.id); }, 100); } var id = 21; foo(); foo.call({ id: 42 }); // id: 42
1.3 關于函數this指向問題
普通函數的this是會被綁定的,根據調用方式的不同綁定不同的對象到this(this只能綁定對象),而箭頭函數是不綁定this的。有這樣一道面試題:
window.bar = 2 var obj = { bar: 1, foo: function() { return this.bar; } }; var foo = obj.foo; console.log(obj.foo()); // 1 console.log(foo()); // 2
JavaScript的this設計很內存里的數據結構有很大的關系。當把一個對象賦給一個變量的時候,大家都知道是引用關系,上面的obj是一個地址,指向那個對象,而在對象存儲的時候,其屬性(方法)的值也是同樣的存儲形式,每個屬性對應一個屬性描述對象,舉例來講,上面obj的bar屬性其實是以下面的形式保存起來的。
bar: { [[value]]: 1, [[configurable]]: true, [[enumerable]]: true, [[writable]]: true }
其屬性的值被保存在[[value]]中。但如果屬性的值是個對象(函數也是對象)呢?此時JavaScript引擎會將對象的地址保存在描述符對象的[[value]]位置,像上面的foo屬性(方法)則是這樣保存的:
foo: { [[value]]: 對象的地址, [[configurable]]: true, [[enumerable]]: true, [[writable]]: true }
函數是個多帶帶的值,因此他可以在任何不同的上下文環境中執行,也正因為如此,有必要需要一種機制能夠在函數內部獲得當前的執行上下文(context),因此this就出現了。在上面的那道面試題中,是將該函數的地址賦給變量foo。通過foo變量調用時,其是在全局作用域下執行,因此this指向全局對象。如圖1:
而使用obj.foo執行時,函數是在obj環境下運行,如圖2,所以this是指向obj的。上面提到普通函數是綁定this值,this值指得是當前運行環境,當在obj環境下調用時指向obj,而在全局調用時指向全局對象。所以this是在調用時才確定值,并不是在聲明時就綁定值。
2.call/apply調用模式
call和apply都是Function.prototype中的方法,可以通過Function.prototype.hasOwnProperty("call")驗證。因此每一個函數或者方法都可通過call或apply調用,call和apply都是函數上的方法,每聲明一個函數,就像prototype屬性一樣,都會有call和apply方法。每個函數或方法都可以通過call或者apply改變當前的執行上下文,他們的第一個參數就是要將this綁定的值。區別是后面的傳參形式不同,前者是將參數逐個傳入調用的函數中,而apply是將參數作為一個數組傳給要調用的函數。就拿那道面試題做例子:
window.bar = 2 var obj = { bar: 1, foo: function() { return this.bar; } }; var foo = obj.foo; // ① foo.call(obj); // 1 // ② obj.foo.call(window); // 2 // ③ foo.call({bar: 3}) // 3
①如果foo是普通的調用,其this是指向全局對象的,而通過call改變將this綁定到obj后,this將指向obj。我們可以這樣理解,foo是這樣調用的obj.foo()
②這種調用方式我們可以這樣理解,foo是obj的方法,就當他是一個普通的函數,相當于window.foo這樣調用,那么this就是指向全局對象的。
③這種調用方式是將{bar: 3}作為this的綁定對象,這樣調用foo就相當于{bar: 3}.foo(),this指向{bar: 3}。
3.構造器調用模式:
構造函數的new調用方式被稱為構造器調用模式,這是模擬類繼承式語言的一種調用方式。在使用new操作符調用函數時,函數內部將this綁定到一個新對象并返回。如下
var Person = function(name) { this.name = name; }; var shotar = new Person(shotar); // 為了區別于普通函數,約定構造函數的首字母大寫。使用new操作內部會替你做以下操作: Person(name) { // 以下都是使用new操作符時內部做的事 // var obj = new Object(); // this = obj; // obj.name = name; // obj.prototype = Person.prototype; // return obj; }
如果構造函數內部返回了一個不是對象的值,則new會忽略其返回值而返回新建的對象,如果返回的是一個對象則將其返回。另外,如果不使用new操作符調用,并不會在編譯時報錯,這是非常糟糕的事情,因此,我們通常會在調用的時候檢查是否為new操作符調用,如下:
function Person(name) { if (this instanceof Person) { this.name = name; } else { return new Person(name); } }
4.回調模式
回調函數是在滿足某種情況或者達到某種要求時立即調用。回調函數通常作為函數的參數傳入,其本質也還是一種普通的函數,只是在特定的情況下執行而已,先看一個例子:
function sayName(obj) { var fullName = ""; if (obj.firstName && obj.lastName) { fullName = typeof obj.computedFullName === "function" ? obj.computedFullName() : obj.lastName + " " + obj.firstName; return fullName; } var obj = { firstName: "Sanfeng", lastName: "Zhang", computedFullName: function() { return this.lastName + " " + this.firstName; } }; sayName(obj); // Zhang Sanfeng
此處的computedName就是一個回調函數,在給sayName函數傳值的時候,我們傳入了一個對象,前兩個屬性都是直接在sayName中使用,如果滿足這兩個屬性都有值,那就調用obj的computedName方法(也就是函數),在此處調用就稱他為回調函數,回調函數常用于異步操作的場合,比如ajax請求,當請求成功并返回數據時再執行回調函數。一般也用于同步阻塞的場景下,比如執行某些操作后執行回調函數。請先看下面的異步情況的例子:
function ajax(callback) { var xhr = new XMLHttpReauest(); if (xhr.readystate === 4 && xhr.status === 200) { typeof callback === "function" && callback(); } else { alert("請求失敗!") } xhr.open("get", url); xhr.send(); } var fn = function() { alert("請求成功!"); }; ajax(fn);
這里會有一個問題,如何給回調函數傳參,讓回調函數在里面處理一些問題,這里我們就可以用到call或者apply方法了。比如有這樣一個問題:統計若干個人的考試成績,只有90分以上的才發獎學金,請看下面同步阻塞的例子:
function startGive(arr, giveMoney) { // 先把分數超過90分的過濾出來 let adult = arr.filter(item => item > 90); // 將過濾結果傳入回調函數,發獎金給他們 return giveMoney.call(null, adult); } let giveBonuses = function(arr) { return arr.map(item => item + "giveMoney"); }; console.log(startGive([70, 80, 92, 96, 85], giveBonuses)); // [ "92giveMoney", "96giveMoney" ]
上面的例子主要是在將分數在90分以上的過濾出來之后再執行操作。回調傳參還可以通過傳遞匿名函數的形式接收該參數,如下例子:
function fn(arg1, arg2, callback){ var num = Math.ceil(Math.random() * (arg1 - arg2) + arg2); callback(num); } fn(10, 20, function(num){ console.log("Callback called! Num: " + num); });
5.總結
本文講了關于函數調用的五種模式。五種模式包括函數調用模式、方法調用模式、call/apply調用模式、構造器調用模式和回調模式。其中前三種調用模式類似,主要會涉及到this的指向問題,第四種調用方式總返回一個對象,并將this綁定到此對象。回調模式屬于前四種模式中的一種,可以是函數調用模式,也可以是方法調用模式,回調的使用很靈活,其主要場景是用于異步操作或同步阻塞操作的場合。
本文參考《JavaScript語言精粹》一書的函數章節及阮大大的《JavaScript 的 this 原理》一文撰寫而出,文中若有表述不妥或是知識點有誤之處,歡迎留言指正批評!
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/99489.html
摘要:前言初學總會對指向感到疑惑,想要深入學習,必須先理清楚和相關的幾個概念。中總是指向一個對象,但具體指向誰是在運行時根據函數執行環境動態綁定的,而并非函數被聲明時的環境。除去不常用的和的情況,具體到實際應用中,指向大致可以分為以下種。 前言 初學javascript總會對this指向感到疑惑,想要深入學習javascript,必須先理清楚和this相關的幾個概念。javascript中t...
摘要:前言曾經被中的弄暈了,今天整理總結一下在嚴格模式下的幾種指向。嚴格模式構造函數中的事件處理函數中的在嚴格模式下,在事件處理函數中,指向觸發事件的目標對象。 前言 曾經被 JavaScript 中的 this 弄暈了,今天整理總結一下在嚴格模式下 this 的幾種指向。 1. 全局作用域中的this 在嚴格模式下,在全局作用域中,this指向window對象 use stric...
摘要:雖然這個模式運行效果很不錯,但是如果嵌套了太多的回調函數,就會陷入回調地獄。當需要跟蹤多個回調函數的時候,回調函數的局限性就體現出來了,非常好的改進了這些情況。 JavaScript引擎是基于單線程 (Single-threaded) 事件循環的概念構建的,同一時刻只允許一個代碼塊在執行,所以需要跟蹤即將運行的代碼,那些代碼被放在一個任務隊列 (job queue) 中,每當一段代碼準...
摘要:三種使用構造函數創建對象的方法和的作用都是在某個特殊對象的作用域中調用函數。這種方式還支持向構造函數傳遞參數。叫法上把函數叫做構造函數,其他無區別適用情境可以在特殊的情況下用來為對象創建構造函數。 一、工廠模式 工廠模式:使用字面量和object構造函數會有很多重復代碼,在此基礎上改進showImg(https://segmentfault.com/img/bVbmKxb?w=456&...
摘要:探討判斷橫豎屏的最佳實現前端掘金在移動端,判斷橫豎屏的場景并不少見,比如根據橫豎屏以不同的樣式來適配,抑或是提醒用戶切換為豎屏以保持良好的用戶體驗。 探討判斷橫豎屏的最佳實現 - 前端 - 掘金在移動端,判斷橫豎屏的場景并不少見,比如根據橫豎屏以不同的樣式來適配,抑或是提醒用戶切換為豎屏以保持良好的用戶體驗。 判斷橫豎屏的實現方法多種多樣,本文就此來探討下目前有哪些實現方法以及其中的優...
閱讀 1074·2021-11-19 09:40
閱讀 2213·2021-11-15 18:00
閱讀 1267·2021-10-18 13:34
閱讀 2248·2021-09-02 15:40
閱讀 1533·2019-08-30 14:01
閱讀 1113·2019-08-30 11:11
閱讀 2482·2019-08-29 15:26
閱讀 722·2019-08-29 14:15