一些基礎概念
變量的生命周期
- 全局作用域下的變量,定義的時候開始存在,頁面關閉或者刷新就消失。
- 函數內部的變量,在你寫的時候是不存在的,一般來說,只有調用這個函數的時候,這個變量才會被聲明。那么這個函數內部的a什么時候消失呢,當a所在的作用域不見的時候a就消失。
function fn () {
var a = 1;
// return undefined; 所有函數不寫return時,默認有這一句
}
// 瀏覽器執行到這一行,此時a不存在
fn();//這個時候a就誕生了
//fn()執行完了之后,a就消失了
fn();//這時候會產生一個新的a,并不是原來的a
//fn() 執行完成后,新的a又消失了
- 如果這個變量被引用著,就不能回收
function fn () {
var a = 1;
var b = 2;
window.xxx = a;
// return undefined; 所有函數不寫return時,默認有這一句
}
// 瀏覽器執行到這一行,此時a不存在
fn();//這個時候a就誕生了
//此時fn()執行完,b消失,a還存在,因為a被引用了。
console.log(window.xxx); //1
//等window消失a也就消失了
//其實可以認為函數執行完成后,a這個變量名是消失了的,只是這段內存沒消失,還在繼續被引用,等window消失的時候再釋放這段內存
var作用域
- 就近原則
var a;
function fn1 () {
var a;
function fn2 () {
var a;
a = 1; //給f2的a賦值
}
}
function f2() {};
function f1 () {
function f2 () {}
f2(); //調用的是f1內部的f2
};
- 詞法作用域
var a;
function fn1 () {
var a;
function fn2 () {
var a;
a = 1; //給f2的a賦值
}
}
不需要執行代碼,分析語句的詞法就能判斷作用域叫詞法作用域
- 同名的不同變量
立即執行函數
- 如果想得到一個獨立的作用域,必須要聲明一個函數
-
如果想運行函數內部的代碼,必須要執行函數
立即執行函數即創建一個匿名函數作用域并且立即執行。前面可以是! + - 等符號。 - 關于有沒有var聲明的區別:
var a = 100;
!function() {
var a = 1;
!function() {
a = 2; // 沒有用var聲明,所以不是當前作用域的a,而是上一級的作用域的a,所以改變了上一級a的值
console.log(a);//2
}();
console.log(a);//2 a的值被改變 輸出2
}();
console.log(a);//100
- 立即執行函數的傳參:
var a = 1;
!function(a) { //這里的a是個形參
console.log(a); //外面傳入一個a,里面的作用域輸出2
}(2);
console.log(a); //全局作用域下的a還是1
//上面的形參a其實相當于下面這么寫
!function(){
var a = arguments[0];
console.log(a);//外面傳入一個a,里面的作用域輸出2
}(2);
傳參例子2
var a = 100;
!function(a){
console.log(a);
}(a);
//這里的兩個a,傳入的a是外部傳入的值,全局變量a,而匿名函數的a僅僅是個形參 等于不寫參數,里面一個var a = arguments[0];
//只是在執行函數的時候恰好把傳入參數的a賦值給了內部的a
變量(聲明)提升
- 瀏覽器在執行代碼之前,會把所有聲明提升到作用域的頂部
var a = 100;
var a = function () {};
function a() {};
console.log(a); //function
//等價于
var a;
var a;
function a () {};
a = 100;
a = function() {};
console.log(a); // 所以是function
- 函數內部的變量提升
//函數內部的聲明提升,不會提升到全局或者外部的作用域的,就只會提升到當前作用域頂部
function f1() {
// var a = 100; 變量提升等價于
var a;
a = 100;
}
f1();
//另外,上述代碼和下面的是一樣的
function f1() {
a = 100;
var a;
}
//只有在當前作用域中有var,就不會跑出當前作用域
//如果是
var a = 1;
function f1 () {
a = 100;
// 后面語句里沒有用var聲明a,那么這個a就不是當前f1下的a。會在f1的上級作用域中尋找a,并且改變a的值
}
f1();
console.log(a); //100
- 帶有迷惑性的變量提升
先進行提升,在考慮代碼
var a = 100; // 第一個a
function f1 () {
var b =2;
if (b === 1) {
var a; //第二個a
}
a = 99;
}
//上述例子中,f1內部寫的a = 99指的是誰?
// 進行變量提升,首先明確JS沒有塊級作用域,所以什么if for都不是新的作用域,變量提升的時候也不用考慮if的判斷條件
//總而言之,先進行提升,在考慮代碼,即f1內部等價于
var a = 100;
function f1 () {
var b;
var a;
b = 2;
if(b === 1){
}
a = 99;
}
//提升之后自然發現指的是f1內部的a
閉包
var items = document.querySelectorAll('li');
for(var i = 0; i < items.length; i ++) {
items[i].onclick = function () {
console.log(i);
}
}
上述例子分析:
var items = document.querySelectorAll('li');
for(var i = 0; i < items.length; i ++) {
items[i].onclick = function () {
console.log(i);
}
}
console.log(i);
//進行變量提升
var items;
var i;
//這里提升了i,整個作用域中只有這一個i,所以循環到6之后,所有的i的值都為6
items = document.querySelectorAll('li');
for(i = 0; i < items.length; i ++) {
items[i].onclick = function () {
console.log(i); //C
}
}
console.log(i); // D
// 變量提升完成后 D是一定比C先執行的
// 當D執行時 i已經循環為6,所以后面C輸出的都是6
//所以如果要想使得每次輸出的值不一樣,就得使得每次的i不是同一個i
//要取得不同的i,就要創建新的作用域,那么我們來創建新的作用域:
var items;
var i;
items = document.querySelectorAll('li');
for(i = 0; i < items.length; i ++) {
function temp (i) {//創建一個新的函數作用域,在這個作用域中傳入每次循環得到的i的值,就能得到不一樣的i了
// 還要注意前面提到的問題,temp(i);中的i和函數的形參i不是同一個東西,具體見立即執行函數的傳參
items[i].onclick = function () {
console.log(i); //接收到每次的i值然后賦給i,這樣每次循環的i的值就不一樣了
}
}
temp(i); // 傳入每次循環的i值
}
//那么前面提到的立即執行函數可以簡化代碼,不用寫temp(),直接寫個立即執行函數并且傳入i就好了
var items;
var i;
items = document.querySelectorAll('li');
for(i = 0; i < items.length; i ++) {
!function(i) { //寫成立即執行函數簡化代碼
items[i].onclick = function () {
console.log(i); //接收到每次的i值然后賦給i,這樣每次循環的i的值就不一樣了
}
}(i);
}
//上述代碼就等價于下面的,temp函數時一個返回函數的函數,創造一個函數用于綁定事件
var items;
var i;
items = document.querySelectorAll('li');
for(i = 0; i < items.length; i ++) {
function temp(i) { // 這里temp(i)其實也是要提升的,但是只要知道內部的i跟外部i不一樣就好了
return function () {
console.log(i);
}
}
var fn = temp(i); //傳入每次的i的值 fn必須為一個函數才能賦給onclick事件,所以temp的返回值必須也是一個函數
items[i].onclick = fn;
}
// 有了返回值為一個函數的思想,我們可以直接給onclick賦給一個返回函數的函數,這樣創建了一個新的作用域保存每次循環的i值,如下:
var items;
var i;
items = document.querySelectorAll('li');
for(i = 0; i < items.length; i ++) {
items[i].onclick = function(i) {
//函數內部作用域的i和外部的i不同
return function (){
console.log(i);
}
}(i); //傳入每次循環的i值
}
總結一下思路:
- 要得到每次循環的i值,就必須得創建新的作用域。因為全局作用域當中只有一個i,是不會變的,需要一個新的作用域來獲取每次循環的i的值。
- 我們給onclick綁定的一定是一個函數,所以賦給onclick一個立即執行函數之后,這個立即執行的值,即return的東西一定是一個函數。這就是為什么一定要return一個函數。
通過作用域鏈理解閉包
第一個例子:
//通過作用域鏈理解閉包
//下例中定義一個數組,遍歷數組給每一項賦給一個函數,最后輸出的 fnArr[1]的執行結果() 發現結果都是2,不管是fnArr[1]還是fnArr[0]
// var fnArr = [];
// for (var i = 0; i < 2; i ++) {
// fnArr[i] = function() {
// return i;
// }
// }
// console.log( fnArr[0]() ); //2
// console.log( fnArr[1]() ); //2
//分析: 當我們寫一段函數體或者函數名的時候,就是一段代碼,一個指針或者一個地址,只有后面加了括號(),它才會真正的去執行
//沒有執行的話,就相當于沒有任何的作用,就是一段代碼。
//所以在上面的for循環中,賦給fnArr[i]的時候沒有執行,當循環結束時,i的值為2,此時調用fnArr[1]()或者fnArr[0]并且輸出,
// 此時函數才會真正的執行,但是這個是函數里面沒有i,只會向外部的全局作用域中尋找i,即for循環中的i,為2,所以不管fnArr[0]還是fnArr[1]都輸出2
//針對上面的例子,我們提出了對代碼的改裝要求,要求fnArr[0]就是輸出0,fnArr[1]就是輸出1。要做到這個效果,就要用到閉包
// 第一種改裝方法:
// var fnArr = [];
// for (var i = 0;i < 2; i ++) {
// (function(i){
// fnArr[i] = function () {
// return i;
// }
// })(i);
// }
// console.log( fnArr[0]() ); //0
// console.log( fnArr[1]() ); //1
// 針對第一種改裝方法,就等價于下面這么寫,
// 第一種改裝方法的改寫1,去掉for循環:
// var fnArr = [];
// (function(i){
// fnArr[i] = function () {
// return i;
// }
// })(0);
// (function(i){
// fnArr[i] = function () {
// return i;
// }
// })(1);
// console.log( fnArr[0]() );
// console.log( fnArr[1]() );
// 這種寫法還有立即執行函數,不好理解,接著改寫2,去掉立即執行函數,就等價于:
// 第一種改裝方法的改寫2:
var fnArr = [];
function fn1 (i) {
fnArr[i] = function fn11() {
return i;
}
}
function fn2 (i) {
fnArr[i] = function fn22 () {
return i;
}
}
fn1(0);
fn2(1);
console.log( fnArr[0]() );
console.log( fnArr[1]() );
// 那么通過作用域鏈來分析改寫2:
globalContext = {
AO : {
// 活動對象,變量提升
fnArr: undefined,
fn1: function,
fn2: function,
}
}
fn1.[[scope]] = globalContext.AO;
fn2.[[scope]] = globalContext.AO;
fn1Context = {
AO: {
i: 0,
fn11: function,
},
scope: fn1.[[scope]]
}
fn11.[[scope]] = fn1Context.AO;
//運行到fn1的時候,沒有自身的AO里面沒有fnArr,所以就要到scope里面去找,fn1.[[scope]]是globalContext的AO,找到了fnArr
//此時globalContext的AO的fnArr的值就由undefined變為[fn11],然后什么都沒發生,退出來進入fn2
fn2Context = {
AO : {
i: 1,
fn22: function,
},
scope: fn2.[[scope]],
}
fn22.[[scope]] = fn2Context.AO;
//此時進入fn2,尋找fnArr和上面一樣,過程結束后,globalContext的AO里面的fnArr的值為[fn11,fn22];
//現在 console.log( fnArr[0]() ); 要輸出fnArr[0]() ,即在globalContext中找到AO里面的fnArr[0]并且執行,此時進入fn11的context
fn11Context = {
AO: {
// AO是空的
},
scope: fn11.[[scope]],
}
// fn11沒有AO,所以只能通過scope來尋找i,fn11.[[scope]]就是fn1Context.AO, 所以找到了i,i為0,
// 一樣的,當你輸出fnArr[i]的時候,結果是調用fn22,然后找到fn2中的AO的i,為1
fn22Context = {
AO: {
// AO是空的
},
scope: fn22.[[scope]],
}
//那么對比我們一開始沒有達成效果的例子,那個例子是因為在當前函數中找不到i,所以直接去全局中找到了i
//我們現在改寫了之后,在函數和全局之間又加了一層包裝,在scope的中途就能獲取到i,所以能夠達成效果,這就是閉包
第二個例子:
//
function fn () {
var s = 1;
function sum () {
++s;
console.log(s);
}
return sum;
}
var mySum = fn();
mySum(); //2
mySum(); //3
mySum(); //4
var mySum2 = fn();
mySum2(); //2
mySum2(); //3
//上述寫法等價于
// function fn () {
// var s = 1;
// return function () {
// ++s;
// console.log(s);
// }
// }
//同樣的,使用作用域鏈的偽代碼來分析:
globalContext = {
AO : {
fn: function,
mySum: undefined,
mySum2: undefined,
}
}
fn.[[scope]] = globalContext.AO;
//mySum = fn()時,進入fn的執行上下文
fnContext = {
AO: {
s: 1,
sum: function,
}
scope: fn.[[scope]]
}
sum.[[scope]] = fnContext.AO;
//執行完成之后退出fn,此時globalContext的AO里面的mySum的值由undefined變為fn(),即sum,調用mySum(),即執行sum,此時進入sum的context
sumContext = {
AO : {
// 為空
},
scope: sum.[[scope]],
}
// sum沒有AO,找不到s,所以到scope里面,即fn的AO里面找到s,此時s的值由1變為2.
// 再次執行mySum(),此時sum還是沒有s,繼續到scope中去找,即找到了fnContext的AO的s,此時s為2,所以由2變為3
// 然后執行到了mySum2 = fn(),此時,會初始化一個執行上下文,此時的s為1
fnContext = {
AO: {
s: 1,
sum: function,
}
scope: fn.[[scope]],
}
sum.[[scope]] = fnContext.AO;