首頁 web前端 js教程 深入理解javascript函數參數與閉包

深入理解javascript函數參數與閉包

Dec 22, 2016 pm 01:46 PM
js 函數參數 閉包

最近在學習javascript的函數,函數是javascript的一等對象,想要學好javascript,就必須深刻理解函數。本人把學習的過程整理成文章,一是為了加深自己函數的理解,二是提供讀者學習的途徑,避免走彎路。內容有些多,但都是筆者對於函數的總結。

1.函數參數

  1.1:參數是什麼

  1.2:參數的省略

  1.3:參數預設值🜀

  1.6:arguments物件

2.閉包

  2.1:閉包定義

  2.2:立即調用的函數表達式(IIFE, Immediately invoked function expression)

1.函數參數

需要為函數傳遞額外的數據,不同的外部數據會得到不同的結果,而這種外部數據就叫做參數。

function keith(a){
return a+a;
}
console.log(keith(3)); //6
登入後複製

上面程式碼中,給keith函數傳遞了參數a,並且傳回了a+a表達式。

1.2:參數的省略

函數參數不是必須的,javascript規格允許省略呼叫時傳遞的實際參數。

function keith(a, b, c) {
return a;
}
console.log(keith(1, 2, 3)); //1
console.log(keith(1)); //1
console.log(keith()); // 'undefined'
登入後複製

上面程式碼中,keith函數定義了三個參數,但是在呼叫時無論傳遞了多少個參數,javascript都不會報錯。被省略的參數的預設值就變成undefined。了解函數定義與函數作用域 的都知道,函數的length屬性會傳回參數個數。要注意的是,length屬性與實際參數的數量無關,只是傳回形式參數的數量。

(實際參數:呼叫時傳遞的參數。     形式參數:定義時傳遞的參數。)

但是沒有辦法省略只靠前的元素,而保留靠後的元素。如果一定要省略前面的元素,只有顯示傳入undefined。

function keith(a, b) {
return a;
}
console.log(keith(, 1)); //SyntaxError: expected expression, got ','
console.log(keith(undefined, 2)); //'undefined'
登入後複製

上面程式碼中,如果省略了第一個參數,瀏覽器就會報錯。如果給第一個參數傳遞undefined,則不會報錯。

1.3:預設值

在JavaScript中,函數參數的預設值是undefined。然而,在某些情況下設定不同的預設值是有用的。一般策略是在函數的主體測試參數值是否為undefined,如果是則賦予一個值,如果不是,則傳回實際參數傳遞的值。

function keith(a, b) {
(typeof b !== 'undefined') ? b = b: b = 1;
return a * b;
}
console.log(keith(15)); //15
console.log(keith(15, 2)) //30
登入後複製

上面程式碼中,做了個判斷。當在呼叫時沒有傳入b參數,則預設為1。

從ECMAScript 6開始,定義了預設參數(default parameters)。使用預設參數,在函數體的檢查就不再需要了。

function keith(a, b = 1) {
return a * b;
}
console.log(keith(15)); //15
console.log(keith(15, 2)) //30
登入後複製

1.4:參數傳遞方式

函數參數的傳遞方式有兩種,一個是傳值傳遞,一個是傳址傳遞。

當函數參數是原始資料型別時(字串,數值,布林值),參數的傳遞方式為傳值傳遞。也就是說,在函數體內修改參數值,不會影響函數外部。

var a = 1;
function keith(num) {
num = 5;
}
keith(a);
console.log(a); //1
登入後複製

上面程式碼中,全域變數a是一個原始型別的值,傳入函數keith的方式是傳值傳遞。因此,在函數內部,a的值是原始值的拷貝,無論怎麼修改,都不會影響到原始值。

但是,如果函數參數是複合類型的值(陣列、物件、其他函數),則傳遞方式是傳址傳遞(pass by reference)。也就是說,傳入函數的是原始值的位址,因此在函數內部修改參數,將會影響原始值。

var arr = [2, 5];
function keith(Arr) {
Arr[0] = 3;
}
keith(arr);
console.log(arr[0]); //3
登入後複製

上面程式碼中,傳入函數keith的是參數物件arr的位址。因此,在函數內部修改arr第一個值,會影響到原始值。

注意,如果函數內部修改的,不是參數物件的某個屬性,而是替換掉整個參數,這時不會影響到原始值。

var arr = [2, 3, 5];
function keith(Arr) {
Arr = [1, 2, 3];
}
keith(arr);
console.log(arr); // [2,3,5]
登入後複製

上面程式碼中,在函數keith內部,參數物件arr被整個替換成另一個值。這時不會影響到原始值。這是因為,形式參數(Arr)與實際參數arr有一個賦值關係。

1.5:同名參數

如果有同名參數,則取最後面出現的那個值,如果未提供最後一個參數的值,則取值變成undefined。

function keith(a, a) {
return a;
}
console.log(keith(1, 3)); //3
console.log(keith(1)); //undefined
登入後複製

如果想存取同名參數中的第一個參數,則使用arguments物件。

function keith(a, a) {
return arguments[0];
}
console.log(keith(2));  //2
登入後複製

1.6 arguments物件

JavaScript 中每個函數內都能存取一個特別變數 arguments。這個變數維護著所有傳遞到這個函數中的參數列表。

arguments 物件包含了函數運行時的所有參數,arguments[0]就是第一個參數,arguments[1]就是第二個參數,以此類推。這個物件只有在函數體內部,才可以使用。

可以存取arguments物件的length屬性,判斷函數呼叫時到底帶幾個參數。

function keith(a, b, c) {
console.log(arguments[0]); //1
console.log(arguments[2]); //3
console.log(arguments.length); //4
}
keith(1, 2, 3, 4);
登入後複製

arguments物件與陣列的關係

arguments 对象不是一个数组(Array)。 尽管在语法上它有数组相关的属性 length,但它不从 Array.prototype 继承,实际上它是一个类数组对象。因此,无法对 arguments 变量使用标准的数组方法,比如 push, pop 或者 slice。但是可以使用数组中的length属性。

通常使用如下方法把arguments对象转换为数组。

var arr = Array.prototype.slice.call(arguments);

2.闭包

2.1:闭包定义

要理解闭包,需要先理解全局作用域和局部作用域的区别。函数内部可以访问全局作用域下定义的全局变量,而函数外部却无法访问到函数内部定义(局部作用域)的局部变量。

var a = 1;
function keith() {
 return a;
 var b = 2;
 }
 console.log(keith()); //1
 console.log(b); //ReferenceError: b is not defined
登入後複製

上面代码中,全局变量a可以在函数keith内部访问。可是局部变量b却无法在函数外部访问。

如果需要得到函数内部的局部变量,只有通过在函数的内部,再定义一个函数。

function keith(){
var a=1;
function rascal(){
return a;
}
return rascal;
}
var result=keith();
console.log(result()); //1
function keith(){
var a=1;
return function(){
return a;
};
}
var result=keith();
console.log(result()) //1
登入後複製

上面代码中,两种写法相同,唯一的区别是内部函数是否是匿名函数。函数rascal就在函数keith内部,这时keith内部的所有局部变量,对rascal都是可见的。但是反过来就不行,rascal内部的局部变量,对keith就是不可见的。这就是JavaScript语言特有的”链式作用域”结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。函数keith的返回值就是函数rascal,由于rascal可以读取keith的内部变量,所以就可以在外部获得keith的内部变量了。

闭包就是函数rascal,即能够读取其他函数内部变量的函数。由于在JavaScript语言中,只有函数内部的子函数才能读取内部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。闭包最大的特点,就是它可以“记住”诞生的环境,比如rascal记住了它诞生的环境keith,所以从rascal可以得到keith的内部变量。

闭包可以使得它诞生环境一直存在。看下面一个例子,闭包使得内部变量记住上一次调用时的运算结果。

function keith(num) {
return function() {
return num++;
};
}
var result = keith(2);
console.log(result()) //2
console.log(result()) //3
console.log(result()) //4
登入後複製

上面代码中,参数num其实就相当于函数keith内部定义的局部变量。通过闭包,num的状态被保留了,每一次调用都是在上一次调用的基础上进行计算。从中可以看到,闭包result使得函数keith的内部环境,一直存在。

通过以上的例子,总结一下闭包的特点:

1:在一个函数内部定义另外一个函数,并且返回内部函数或者立即执行内部函数。

2:内部函数可以读取外部函数定义的局部变量

3:让局部变量始终保存在内存中。也就是说,闭包可以使得它诞生环境一直存在。

闭包的另一个用处,是封装对象的私有属性和私有方法。

function Keith(name) {
var age;
function setAge(n) {
age = n;
}
function getAge() {
return age;
}
return {
name: name,
setAge: setAge,
getAge: getAge
};
}
var person = Keith('keith');
person.setAge(21);
console.log(person.name); // 'keith'
console.log(person.getAge()); //21
登入後複製

2.2:立即调用的函数表达式(IIFE)

通常情况下,只对匿名函数使用这种“立即执行的函数表达式”。它的目的有两个:一是不必为函数命名,避免了污染全局变量;二是IIFE内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量。

循环中的闭包

一个常见的错误出现在循环中使用闭包,假设我们需要在每次循环中调用循环序号

for(var i=0;i<10;i++){
setTimeout(function(){
console.log(i); //10
}, 1000)
}
登入後複製

上面代码中,不会符合我们的预期,输出数字0-9。而是会输出数字10十次。

当匿名函数被调用的时候,匿名函数保持着对全局变量 i 的引用,也就是说会记住i循环时执行的结果。此时for循环结束,i 的值被修改成了10。

为了得到想要的效果,避免引用错误,我们应该使用IIFE来在每次循环中创建全局变量 i 的拷贝。

for(var i = 0; i < 10; i++) {
 (function(e) {
 setTimeout(function() {
 console.log(e); //1,2,3,....,10
 }, 1000);
 })(i);
 }
登入後複製

外部的匿名函数会立即执行,并把 i 作为它的参数,此时函数内 e 变量就拥有了 i 的一个拷贝。当传递给 setTimeout 的匿名函数执行时,它就拥有了对 e 的引用,而这个值是不会被循环改变的。


本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

C++ lambda 表達式中閉包的意思是什麼? C++ lambda 表達式中閉包的意思是什麼? Apr 17, 2024 pm 06:15 PM

在C++中,閉包是能夠存取外部變數的lambda表達式。若要建立閉包,請擷取lambda表達式中的外部變數。閉包提供可重複使用性、資訊隱藏和延遲求值等優點。它們在事件處理程序等實際情況中很有用,其中即使外部變數被銷毀,閉包仍然可以存取它們。

C++ 函式中閉包的優點和缺點是什麼? C++ 函式中閉包的優點和缺點是什麼? Apr 25, 2024 pm 01:33 PM

閉包是一種巢狀函數,它能存取外層函數作用域的變量,優點包括資料封裝、狀態保持和靈活性。缺點包括記憶體消耗、效能影響和調試複雜性。此外,閉包還可以建立匿名函數,並將其作為回調或參數傳遞給其他函數。

C++ Lambda 表達式如何實作閉包? C++ Lambda 表達式如何實作閉包? Jun 01, 2024 pm 05:50 PM

C++Lambda表達式支援閉包,即保存函數作用域變數並供函數存取。語法為[capture-list](parameters)->return-type{function-body}。 capture-list定義要捕獲的變量,可以使用[=]按值捕獲所有局部變量,[&]按引用捕獲所有局部變量,或[variable1,variable2,...]捕獲特定變量。 Lambda表達式只能存取捕獲的變量,但無法修改原始值。

C++ 函式參數詳解:不定參數傳遞的實作方式與優缺點 C++ 函式參數詳解:不定參數傳遞的實作方式與優缺點 Apr 28, 2024 am 09:48 AM

C++不定參數傳遞:透過...運算子實現,可接受任意數量的附加參數,優點包括靈活性、可擴展性和簡化程式碼,缺點包括效能開銷、除錯困難和類型安全。常見實戰案例包括printf()和std::cout,它們使用va_list處理可變數量的參數。

golang函數閉包在測試中的作用 golang函數閉包在測試中的作用 Apr 24, 2024 am 08:54 AM

Go語言函數閉包在單元測試中發揮著至關重要的作用:捕獲值:閉包可以存取外部作用域的變量,允許在巢狀函數中捕獲和重複使用測試參數。簡化測試程式碼:透過擷取值,閉包消除了對每個循環重複設定參數的需求,從而簡化了測試程式碼。提高可讀性:使用閉包可以組織測試邏輯,使測試程式碼更清晰、更易於閱讀。

函數指標和閉包對Golang效能的影響 函數指標和閉包對Golang效能的影響 Apr 15, 2024 am 10:36 AM

函數指針和閉包對Go性能的影響如下:函數指針:稍慢於直接調用,但可提高可讀性和可復用性。閉包:通常更慢,但可封裝資料和行為。實戰案例:函數指標可最佳化排序演算法,閉包可建立事件處理程序,但會帶來效能損失。

閉包在 Java 中是如何實現的? 閉包在 Java 中是如何實現的? May 03, 2024 pm 12:48 PM

Java中的閉包允許內部函數存取外部的作用域變量,即使外部函數已經退出。透過匿名內部類別實現,內部類別持有一個外部類別的引用,使外部變數保持活動。閉包增強了程式碼靈活性,但需要注意記憶體洩漏風險,因為匿名內部類別對外部變數的參考會保持這些變數的活動狀態。

golang匿名函數和閉包的優缺點總結 golang匿名函數和閉包的優缺點總結 May 05, 2024 am 09:54 AM

匿名函數簡潔、匿名,但可讀性差、調試困難;閉包能封裝資料、管理狀態,但可能導致記憶體消耗和循環引用。實戰案例:匿名函數可用於簡單數值處理,閉包可實現狀態管理。

See all articles