JS基礎總結(3)——作用域和閉包

lzg9527發表於2020-01-21

前言

農曆2019即將過去,趁著年前幾天上班事情少,整理了一下javascript的基礎知識,在此給大家做下分享,喜歡的大佬們可以給個小贊。本文在github也做了收錄。

本人github: github.com/Michael-lzg

JS 作用域

Javascript 變數的作用域無非就是兩種:全域性變數和區域性變數。Javascript 語言的特殊之處,就在於函式內部可以直接讀取全域性變數。

全域性作用域(Global Scope)

在程式碼中任何地方都能訪問到的物件擁有全域性作用域,一般來說一下幾種情形擁有全域性作用域:

  1. 最外層函式和在最外層函式外面定義的變數擁有全域性作用域
var name = 'Jack' // 全域性定義
function foo() {
  var age = 23 // 區域性定義
  function inner() {
    // 區域性函式
    console.log(age) //age 23
  }
  inner()
}

console.log(name) // yuan
console.log(age) // Uncaught ReferenceError: age is not defined,在外部沒有這個變數
foo() // 內嵌函式的列印23
inner() // Uncaught ReferenceError: inner is not defined 因為內嵌函式,找不到這個函式
複製程式碼
  1. 所有末定義直接賦值的變數自動宣告為擁有全域性作用域
var name = 'yuan'
function foo() {
  age = 23 // 全域性定義
  var sex = 'male' // 區域性定義
}
foo()
console.log(age) //  23
console.log(sex) // sex is not defined
複製程式碼
  1. 所有 window 物件的屬性擁有全域性作用域 一般情況下,window 物件的內建屬性都都擁有全域性作用域,例如 window.alert()、window.location、window.top 等等。

作用域鏈

當程式碼在一個環境中執行時,會建立變數物件的一個作用域鏈(作用域形成的鏈條)

  1. 作用域鏈的前端,始終都是當前執行的程式碼所在環境的變數物件
  2. 作用域鏈中的下一個物件來自於外部環境,而在下一個變數物件則來自下一個外部環境,一直到全域性執行環境
  3. 全域性執行環境的變數物件始終都是作用域鏈上的最後一個物件

當在內部函式中,需要訪問一個變數的時候,首先會訪問函式本身的變數物件,是否有這個變數,如果沒有,那麼會繼續沿作用域鏈往上查詢,直到全域性作用域。如果在某個變數物件中找到則使用該變數物件中的變數值。

內部環境可以通過作用域鏈訪問所有外部環境,但外部環境不能訪問內部環境的任何變數和函式。

變數提升

ES6 之前我們一般使用 var 來宣告變數,變數提升如下例子:

function test() {
  console.log(a) //undefined
  var a = 123
}

// 它的實際執行順序如下
function test() {
  var a
  console.log(a)
  a = 123
}
test()
複製程式碼

函式提升

javascript 中不僅僅是變數宣告有提升的現象,函式的宣告也是一樣。具名函式的宣告有兩種方式:

  • 函式宣告式
  • 函式字面量式
//函式宣告式
function bar() {}
//函式字面量式
var foo = function() {}
複製程式碼

函式提升是整個程式碼塊提升到它所在的作用域的最開始執行

console.log(bar)
function bar() {
  console.log(1) //ƒ bar () { console.log(1)}
}

// 實際執行順序
function bar() {
  console.log(1)
}
console.log(bar)
複製程式碼

閉包

閉包就是能夠讀取其他函式內部變數的函式,函式沒有被釋放,整條作用域鏈上的區域性變數都將得到保留。由於在 javascript 語言中,只有函式內部的子函式才能讀取區域性變數,因此可以把閉包簡單理解成“定義在一個函式內部的函式”

產生一個閉包

建立閉包最常見方式,就是在一個函式內部建立另一個函式。閉包的作用域鏈包含著它自己的作用域,以及包含它的函式的作用域和全域性作用域。

function fn() {
  var a = 1,
    b = 2

  function f1() {
    return a + b
  }
  return f1
}
複製程式碼

上面例子中的 f1 就是一個閉包

閉包的應用

  1. 設計私有的方法和變數。 任何在函式中定義的變數,都可以認為是私有變數,因為不能在函式外部訪問這些變數。私有變數包括函式的引數、區域性變數和函式內定義的其他函式。把有權訪問私有變數的公有方法稱為特權方法(privileged method)。
function Animal() {
  // 私有變數
  var series = '哺乳動物'
  function run() {
    console.log('Run!!!')
  }

  // 特權方法
  this.getSeries = function() {
    return series
  }
}
複製程式碼
  1. 匿名函式最大的用途是建立閉包。減少全域性變數的使用。從而使用閉包模組化程式碼,減少全域性變數的汙染。
var objEvent = objEvent || {}
(function() {
  var addEvent = function() {
    // some code
  }
  function removeEvent() {
    // some code
  }

  objEvent.addEvent = addEvent
  objEvent.removeEvent = removeEvent
})()
複製程式碼

addEvent 和 removeEvent 都是區域性變數,但我們可以通過全域性變數 objEvent 使用它,這就大大減少了全域性變數的使用,增強了網頁的安全性。

  1. 定義模組,我們將操作函式暴露給外部,而細節隱藏在模組內部。
function module() {
	var arr = [];
	function add(val) {
		if (typeof val == 'number') {
			arr.push(val);
		}
	}
	function get(index) {
		if (index < arr.length) {
			return arr[index]
		} else {
			return null;
		}
	}
	return {
		add: add,
		get: get
	}
}
var mod1 = module();
mod1.add(1);
mod1.add(2);
mod1.add('xxx');
console.log(mod1.get(2));
複製程式碼

使用閉包的注意點

  1. 由於閉包會使得函式中的變數都被儲存在記憶體中,記憶體消耗很大,所以不能濫用閉包,否則會造成網頁的效能問題,在 IE 中可能導致記憶體洩露。解決方法時,在退出函式之前,將不使用的區域性變數全部刪除。
function makeAdder(x) {
  return function(y) {
    return x + y
  }
}

var add5 = makeAdder(5)
var add10 = makeAdder(10)

console.log(add5(2)) // 7
console.log(add10(2)) // 12

// 釋放對閉包的引用
add5 = null
add10 = null
複製程式碼

add5 和 add10 都是閉包。它們共享相同的函式定義,但是儲存了不同的環境。在 add5 的環境中,x 為 5。而在 add10 中,x 則為 10。最後通過 null 釋放了 add5 和 add10 對閉包的引用。

  1. 閉包會在父函式外部,改變父函式內部變數的值。所以,如果你把父函式當作物件(object)使用,把閉包當作它的公用方法,把內部變數當作它的私有屬性,這時一定要小心,不要隨便改變父函式內部變數的值。

推薦文章

關注的我的公眾號不定期分享前端知識,與您一起進步!

JS基礎總結(3)——作用域和閉包

相關文章