首页 小组 问答 话题 好文 用户 唠叨 我的社区

[教程]JavaScript 闭包Closure 与Scope 作用域

小蜗锅Lv.1普通用户
2024-10-18 14:36:46
0
37

前言

在解释闭包(Closure)之前,要先知道作用域范围链是什么,才能更好的解释闭包。在ES6 以前,作用域只有global 全域以及function 里的作用域,定义变数则都是使用var来宣告。在ES6 时出现了const与let,同时也增加了新的作用域block,var渐渐的被取代不再被使用,下面先来了解一下作用域是什么。

Scope(作用域)

对于作用域我自己的解释是:

把作用域当成一个只进不出国家,外面的人可以顺利地进来,但是只要是在国家内出生的人民,一辈子都无法出去...

转换成实际上的解释就是,在作用域范围内生成的变数,无法被外面的区域做使用,但外面的区域变数,是可以被里面的作用域做使用的。

而作用域的范围可以分为三种:

1. 全域(global scope)

2. 函式作用域(function scope)

3. 区块作用域(block scope)

全域(global scope)

顾名思义,就是一个最外层的区域,没有比全域再更外层的了,而在全域宣告的变数或是函式,就称全域变数,而全域变数可以在任何地方被使用。

范例:

const global = 'global variable'
function str(){
    console.log(global); 
}
str(); //output: 'global variable'

特别注意的是如果没有宣告变数而直接赋值,不管在哪个作用域,则该变数都会变成全域变数,这样就很容易出问题,所以尽量一定要宣告变数。

范例:

//Ex1: function
function test() {
    //函式內未宣告直接賦值
    a = 3; 
}
test(); 
console.log(a); //output: 3 

//Ex2: block
for (let i = 0; i < 5; i++) {
    //區塊內未宣告直接賦值
    b = 5; 
}
console.log(b); //output: 5

函式作用域(function scope)

一样顾名思义,可以马上理解就是在函式(Function)内的区域,也就是执行区域{}内就是函式的作用域,对应函式作用域的宣告就是var。

这时候就有一个问题了,刚刚提到如果没有宣告变数,则该变数会变全域变数,那在Function 的参数区域()里面的变数也不用宣告,那里面的参数会变全域变数吗?

这边就要提一下函式的生成流程,不过因为有点复杂,所以只先大概提一下以供解释上面的问题。

可以先想像在函式生成时,会产生该函式的执行环境(Execution Context),以下简称EC,EC 里面储存了跟函式有关的资讯,顺带一提除了函式,全域执行时也有一个全域执行环境(global EC),每个EC 生成时都会有相对应的变数物件(Variable Object),以下简称VO ,宣告的变数及函式都会被储存在VO 里面。

而函式的VO 另外称作执行物件(Activation Object),以下简称AO ,AO 跟VO 一样,差别在于AO 除了储存{}内的变数还储存了()内的参数,因为AO 是对应函式的EC 而存在,所以不用宣告参数也会在函式的作用域里面。

范例:

function age(number) {
    var str = 'My age is:';
    console.log(str,number);
}
age(5);               //output: My age is: 5
console.log(number);  //ReferenceError: number is not defined
console.log(str);    //ReferenceError: str is not defined

区块作用域(block scope)

在ES6 新增的作用域,那区块(block)是指什么呢?区块指的是{}大括号范围内的区域都称之区块,像是if 及switch 判断式、while 及for 回圈,包括function 的大括号范围都是区块。不过不是指var宣告变数变成区块变数,他还是只能在function 里才有作用域的效果(可怜的var)。

而对应的区块变数宣告就是新增的const跟let,在大括号内宣告(当然包含function)都会有作用域的效果。

那在回圈的小括号()里宣告的变数呢?

范例:

//Ex1:
for (var j = 0; j < 5; j++) {
    console.log(j); //output: 0 1 2 3 4
}
console.log(j);     // output: 5

//Ex2:
for (let i = 0; i < 5; i++) {
    console.log(i); //output: 0 1 2 3 4
}
console.log(i);     // ReferenceError: i is not defined

可以看到有两个for 回圈,分别在小括号里面用var及let分别做宣告,可以发现在小括号里宣告var可以在global 抓到变数,而用let做宣告,无法在global 抓到变数。所以可以证实在回圈的小括号()内宣告变数,是一个区块变数

所以说不管是在回圈的()里面还是在{}里面宣告变数(使用区块宣告),都会有作用域的效果,都只能在{}内才能被使用,到了外层则无法抓到该变数。

至于常看到的解释,只有在{}宣告区块变数,就是区块作用域,我想会这么说是因为比较好解释?毕竟变数只能在{}里调用,()也只是宣告变数而已。所以说在{}内就是区块作用域其实没错,只是要记得在回圈的()内宣告区块变数也会在{}有区块作用域的效果喔!

以上这只是我的猜测,如果有人知道为什么会这样解释可以在底下留言!

var、const、let 快速比较

了解了作用域后var、const、let就很容易理解拉!

  • var就是在函式内的宣告,会产生function scope

  • let、const则是在区块内宣告,会产生block scope

var大家都很熟了,那let跟const同样都是block scope,有什么差别呢?

let:

  1. 可以宣告变数不赋值,这点跟var一样。

  2. 可以更改变数的内容。

  3. 相同变数在同一层无法重新宣告。

范例:

let a;
console.log(a); // output: undefined

a = 2
console.log(a); // output: 2

let a = 3;      // SyntaxError: Identifier 'a' has already been declared

const:

  1. 不可以宣告变数不赋值。

  2. 不可以更改变数的值。

  3. 相同变数在同一层无法重新宣告。

范例:

const a;       //SyntaxError: Missing initializer in const declaration

const b = 2;
console.log(b); //output: 2
b = 3;
console.log(b); //TypeError: Assignment to constant variable.

const c = 2;
const c = 3;   //SyntaxError: Identifier 'c' has already been declared

来做一个表格整理:

特性

var

let

const

作用域

function

block

block

宣告变数是否需要赋值

宣告后是否可以更改内容

宣告后是否可以重新宣告

是否有hoisting 效果

Scope chain(范围链)

在进入到Closure(闭包)之前,还需要了解的一个概念,就是范围链(Scope Chain),先来看一道经典题目:

var number = 1;

function b() {
  console.log(number);
}

function a() {
  var number = 100;
  b();
}
a();

宣告一个全域变数number = 1,建立funA 里面重新宣告number = 100,并包了一个funB ,则funB 里面console.log(number)会跑出什么答案呢?


答案是:1
有没有答对呢~如果没答对是正常的,我一开始也没答对XD,大部分的人一开始一定都会觉得,欸~~ funB 不是在funA 里面呼叫,那funB 抓外面的变数number不就是100 吗?

而这个就要牵扯到前面提到的执行环境EC 了,当函式建立执行环境时,同时也建立了外部环境参考(Reference to Outer Environment),也就是谁才是function外的环境,而JavaScript 的外部环境参考,是依照静态作用域(Static Scope)又称词汇作用域(lexical Scope)为准则。

什么是静态作用域呢?简单讲就是物理上的程式码范围,虽然funB 是在funA 里面被呼叫,但是funB 在建立执行环境时(也就是funB 被建立的位置),外层的环境就是global,所以 才会是抓到globalnumber的number = 1。

而既然有静态作用域,当然也有动态作用域(Dynamic Scope)啦,在某些语言的情况下刚刚那个问题,答案就会是100 没错喔,是不是很酷!

动态的意思就是,外部参考环境是根据被呼叫的当下,所在的环境就是外部执行环境,恰好跟静态作用域相反。

那看回原本的那个范例,funB 要console.log(number),可是funB 里面没有number那怎么办呢?这时候他就会往外找看有没有人定义number是什么?直到找到最外层的global。

而这个往外找的机制就称作Scope Chain。

Closure(闭包)

终于可以进入到闭包了,有了上面的观念,就可以比较好解释闭包了。
先来看一个范例:

function count() {
    var number = 5;
    function addTen() {
        console.log(number + 10);
    }
    addTen();
}
count(); //output: 15

这是一个普通的例子,呼叫一个count的function ,里面定义一个变数number的值为5 ,然后建立一个addTen的function,印出number + 10,然后执行addTen。

执行后就会印出15 。

这时候问题来了,那如果是在countfunction 里面回传addTen的function 呢?

function count() {
    var number = 5;
    function addTen() {
        console.log(number + 10);
    }
    return addTen;
}
var answer = count();
answer(); //output: 15

跟刚刚不同的是,这次不执行addTen,而是回传它,并且把它传入一个新的变数answer,然后执行answer,神奇的事情发生了,按照之前讲的外部参考环境的规则,应该会跑出ReferenceError: number is not defined才对。

var answer = count(); 
// count() 回傳 addTen,而 addTen = function(){console.log(number)}

//等同於:
var answer = function(){
    console.log(number)
}
answer();

怎么还是跑出15 呢?就好像变数被存在function 里面一样,没错!这就是闭包常见的解释,明明已经执行完count了,但里面的变数却能被answer存取到!而造成这样的原因有两个

第一个原因就要提到刚刚讲的范围链(Scope Chain)了,已经知道Scope Chain就是当抓不到变数时,就会往外层找,直到global 层,而前面也提到funciton 的执行环境EC 会产生对应的执行物件AO。但其实EC 还会产生对应的Scope Chain,里面装了AO 以及function 的[[Scope]]属性,可以简化成以下的式子:

scope chain = activation object + [[Scope]]

AO 我们已经知道是function 里的参数以及变数,那[[Scope]]应该可以大致猜到是什么了吧?没错!!就是往外找的所有AO + VO 的物件。

所以看回刚刚的例子:

function count() {
    var number = 5;
    function addTen() {
        console.log(number + 10);
    }
    return addTen;
}

当建立了addTen的function 时,也建立了一个[[Scope]]的属性,里面装了外层的所有AO + VO,就把{ number: 5 }装进了[[Scope]]里面,所以只要addTen这个function 存在,永远可以透过它去抓到{ number: 5 }。

那除此之外,第二个原因当然就是因为在function 里面回传一个function,如果只是单纯的在count里面放一个addTen,怎么在count的外面抓到addTen的[[Scope]]呢?

所以说闭包(Closure)并不是一个像作用域阿,范围链等有明确定义的东西,它比较像一个现象,因为Scope Chain以及回传function所产生的效果。

那闭包有什么好处呢?我们可以把一些不想被更动的变数藏在function 里面,这样就无法因为外面的程式码而改变数值,但是需要的时候,还是可以得到藏在function 里的变数。

结论

在实际写程式时,并不会特别的想说要怎么把Closure应用在程式码,它比较像一个JavaSctipt自动产生的一个现象,我们不能透过Closure 去更改函式里的变数,但是是可以得到函式里的变数值,除非特殊情况才会特别使用它。

所以下次在看到Closure 时,就要马上想到是因为function有[[Scope]]的属性且回传function,所造成的现象!

小蜗锅
小蜗锅

48 天前

签名 : 拿人手短,js方面的不懂问我,为了100块钱的赞助豁出去了。   37       0
评论
站长交流