js函数、作用域和闭包
JavaScript-作用域、塊級作用域、上下文、執行上下文、作用域鏈
一、函數
1、函數定義
函數是一段可以反復調用的代碼塊。函數可以接收輸入的參數,不同的參數會返回不同的值
2、函數的聲明方式
主要講兩種:
2.1 用function命令聲明函數
function命令后面是函數名,函數名后面是一對圓括號,里面是傳入函數的參數,函數體放在大括號里面
2.2 用函數表達式聲明函數
把匿名函數賦值給變量
3、函數參數
3.1參數定義
參數:從外部傳入函數,支撐函數運行的外部數據
3.2參數的傳遞規則
可以多傳、少傳參數,被省略的參數就是undefined。傳遞參數是按照順序來適配的。
3.3 arguments 對象
1)用途:arguments 對象可以在函數體內部讀取所有參數
2)使用規則:arguments對象包含了函數運行時的所有參數,arguments[0]就是第一個參數,arguments[1]就是第二個參數,以此類推。這個對象只有在函數體內部,才可以使用。
3)arugments對象長這樣(下圖是我傳遞了三個值)
3)通過arguments對象的length屬性,可以判斷函數調用時到底帶幾個參數
4) 舉個例子
4、返回值
4.1 用return實現返回函數的操作后的數值,不寫return語句,函數默認返回undefined
4.2 JavaScript 引擎遇到return語句,就直接返回return后面的那個表達式的值,后面即使還有語句,也不會得到執行。
4.3返回值的應用
函數可以調用自身,這就是遞歸(recursion)。下面就是通過遞歸,計算斐波那契數列的代碼。
5、函數聲明前置
和變量的聲明會前置一樣,函數聲明同樣會前置的。分成兩種情況:
5.1用function聲明的函數
使用function聲明的函數整個函數都會提升到代碼頭部。
所以你在聲明函數前,調用了函數,都不會報錯的,如下圖
5.2用函數表達式聲明函數
不會把整個函數提升,只會把定義的變量提升到頭部。
相當于如下代碼。對于sum2就是一個未賦值的變量,為undefined,是不能作為函數執行的,所以報錯了
var sum2; sum2(3,4); var sum2 =function (a,b){ return a+b ;}6、立刻執行的函數表達式
對于編輯器來說function (a,b){return a+b ;} 是一個函數聲明語句,而不是一個函數類型的值,所以function (a,b){return a+b ;}加上()是會報錯的
正確的寫法是(function (a,b){return a+b ;})(), ()內部的東西是一個值,加上()代表立刻執行,整個語句相當于一個函數類型的值需要立刻執行
7、命名沖突
當在同一個作用域內定義了名字相同的變量和方法的話,會根據前置順序產生覆蓋
var fn = 3;function fn(){}console.log(fn); // 3相當于
var fn function fn(){} //覆蓋上面的fn = 3 //重新賦值 console.log(fn) function fn(fn){console.log(fn);var fn = 3;console.log(fn); }fn(10) //10 3相當于
function fn(){ var fn =arguments[0];console.log(fn);var fn = 3;console.log(fn); }fn(10) //10 3二、函數作用域
1、定義
作用域(scope)指的是變量存在的范圍。
2、分類:
在 ES5 的規范中,Javascript 只有兩種作用域:
一種是全局作用域,變量在整個程序中一直存在,所有地方都可以讀取;
另一種是函數作用域,變量只在函數內部存在。
3、全局變量和局部變量
函數外部聲明的變量就是全局變量(global variable),它可以在函數內部讀取。
在函數內部定義的變量,外部無法讀取,稱為“局部變量”(local variable)
javaScript 語言特有"鏈式作用域"結構(chain scope),子對象會一級一級地向上尋找所有父對象的變量。所以,父對象的所有變量,對子對象都是可見的,反之則不成立。
4、作用域規則
- {}不產生一個作用域,定義函數才會產生一個函數作用域
- 函數在執行的過程中,先從自己內部找變量
- 如果找不到,再從創建當前函數所在的作用域去找, 以此往上
三、閉包
1、定義:
函數連同它作用域鏈上的要找的這個變量,共同構成閉包
2、特點
閉包最大的特點,就是它可以“記住”誕生的環境,在本質上,閉包就是將函數內部和函數外部連接起來的一座橋梁。
3、用處
閉包的最大用處有兩個
- 可以讀取函數內部的變量
- 暫存數據(讓這些變量始終保持在內存中,即閉包可以使得它誕生環境一直存在)
4、舉個栗子
如果沒有這個閉包,函數執行后,里面speed變量就會被清理掉。但我們聲明了fn這個函數,并把它返回出來賦值給新的變量speedup。因為speedup是全局變量,是一直存在的,故這個fn函數就一直存在,speed變量也不會被清理
function car(){var speed = 0function fn(){speed++console.log(speed)}return fn//重要,如果不return出來,相當于閉包的作用就沒有了 }var speedUp = car() speedUp() //1 speedUp() //25、閉包經典案例
閉包的經典案例是定義一個變量,一個函數,一個return 函數。如上圖
看一下這個案例如何改造
原理解析:for循環每次執行,都把function(){ return i} 這個函數賦值給fnArr[i],但這個函數不執行。因為fnArr[3] =function(){ return i};故當我們調用fnArr[3]() 時,相當于function(){ return i};這個函數立刻執行,這時for循環已經完成,i已經變成了10。故輸出10
如果要輸出3,需要如下改造
var fnArr = [] for (var i = 0; i < 10; i ++) {(function(i){fnArr[i] = function(){return i} })(i) } console.log( fnArr[3]() ) // 3 var fnArr = [] for (var i = 0; i < 10; i ++) {fnArr[i] = (function(j){return function(){return j} })(i) } console.log( fnArr[3]() ) // 3四、作用域鏈
1、執行上下文
2、活動對象
Ao有兩種來源,1、來自var定義的變量,2、傳遞的參數
3、scope屬性
執行函數需要值得時候,就從活動對象AO里面找,找不到就從scope里面去找
4、例子1
1)全局上下文:
globalcontext ={
AO:{
x:10
foo:function
bar:function
},
scope:null
}
2)//聲明foo函數得過程中,foo新增scope屬性并指向了globalContext的Ao
foo.[[scope]] =globalContext.Ao
//聲明bar函數得過程中,bar新增scope屬性并指向了globalContext的Ao
bar.[[scope]] =globalContext.Ao
在執行上下文的聲明的函數,這個函數的[[scope]] 就等于globalContext(執行上下文)的Ao
3)當調用bar的時候,進入了bar的執行上下文
barcontext ={
AO:{
x:30
},
scope:bar.[[scope]] // globalContext.Ao
}創建bar的過程中,bar新增scope屬性并指向了globalContext的Ao
4)當調用foo的時候,進入了foo的執行上下文
foocontext ={
AO:{},
scope:foo.[[scope]] // globalContext.Ao
}
5、例2
var x = 10 function bar(){ var x=30 function foo(){ console.log(x) } foo() } bar()1)全局上下文:
globalcontext ={
AO:{
x:10
bar:function
},
scope:null
}
2)//聲明bar函數得過程中,bar新增scope屬性并指向了globalContext的Ao
bar.[[scope]] =globalContext.Ao
3)當調用bar的時候,進入了bar的執行上下文
barcontext ={
AO:{
x:30
foo:function
},
scope:bar.[[scope]] // globalContext.Ao
}創建foo的過程中,foo新增scope屬性并指向了barcontext的Ao
foo.[[scope]] =balContext.Ao
4)當調用foo的時候,進入了foo的執行上下文
foocontext ={
AO:{},
scope:foo.[[scope]] // balContext.Ao
}
所以console.log(x)是30
6、例子3
封裝一個 Car 對象
car對象封裝4個接口,我們只能通過提供接口來操作數據,不能直接操作數據。
原理:定義一個car對象,設置其等于一個立刻執行的函數表達式 中return出來的內容。
return出來的對象,有四個屬性(setSpeed,get,speedUp,speedDown),四個屬性分別對應了四個函數(setSpeed,get,speedUp,speedDown)。這四個函數就用于操作speed的值。這導致car得不到釋放,return的變量也無法釋放,對應的所有函數都沒有辦法釋放,就生成了一個閉包
7、例4
如下代碼輸出多少?如何連續輸出 0,1,2,3,4
1)原理:我們設置了延時為0的定時器,每次for循環一次的時候,就把函數的代碼添加到異步隊列里面一次。當for循環5次循環完之后,開始執行5次的函數,函數執行時去找i的值,這時候的i的值已經變成5,所以就連續輸出5個5
2)改造
for(var i=0; i<5; i++){(function(i){setTimeout(function(){console.log('delayer:' + i )}, 0) })(i) }原理:通過一個立刻執行的函數表達式,生成一個閉包。由于for循環不會產生一個作用域,所以可以不用return。當然用return也可以
for(var i=0; i<5; i++){setTimeout((function(j){return function(){console.log('delayer:' + j )}}(i)), 0) }8、例5
function makeCounter() {var count = 0return function() {return count++}; }var counter = makeCounter()//**相當于把返回的function() {return count++}這個函數賦值counter** var counter2 = makeCounter();//**然后把第二次返回的function() {return count++}這個函數賦值counter2**console.log( counter() ) // 0 //**counter() 每執行一次,就會返回一個數值加1的counter值** console.log( counter() ) // 1console.log( counter2() ) // 0 console.log( counter2() ) // 1原理:因為形成了一個閉包 , counter和counter2 返回的函數存的不是同一個地址,所以對于counter和counter2對應的活動對象是不一樣的
9、例6寫一個 sum 函數,實現如下調用方式
console.log( sum(1)(2) ) // 3 console.log( sum(5)(-1) ) // 4解析:sum(1)之后能跟著一個(),表示sum(1)是一個還沒有執行的函數,等于function sum(){
return function(){}}。
sum(1)后面接了一個(2)表示返回的函數要接收一個參數,本身也要接受一個參數。function sum(a){
return function(b){}
}
最后根據這個函數的功能,返回a+b的值
總結:函數柯里化-只傳遞給函數一部分參數來調用它,讓它返回一個函數去處理剩下的參數。
10、補全代碼,實現數組按姓名、年紀、任意字段排序
var users = [{ name: "John", age: 20, company: "Baidu" },{ name: "Pete", age: 18, company: "Alibaba" },{ name: "Ann", age: 19, company: "Tecent" } ]users.sort(byName) users.sort(byAge) users.sort(byField('company'))sort后面必須要接受一個函數,所以需要返回一個參數。
function byName(user1, user2){return user1.name > user2.name }function byAge (user1, user2){return user1.age > user2.age }function byFeild(field){return function(user1, user2){return user1[field] > user2[field]} } users.sort(byField('company'))總結
以上是生活随笔為你收集整理的js函数、作用域和闭包的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: mysql错误总结-ERROR 1067
- 下一篇: [2019人工智能实战_廖盈嘉]第1次个