JavaScript 函數(shù)

2023-03-20 15:57 更新

概述

函數(shù)的聲明

函數(shù)是一段可以反復(fù)調(diào)用的代碼塊。函數(shù)還能接受輸入的參數(shù),不同的參數(shù)有唯一對(duì)應(yīng)的返回值。

JavaScript 有三種聲明函數(shù)的方法。

(1)function 命令

function命令聲明的代碼區(qū)塊,就是一個(gè)函數(shù)。function命令后面是函數(shù)名,函數(shù)名后面是一對(duì)圓括號(hào),里面是傳入函數(shù)的參數(shù)。函數(shù)體放在大括號(hào)里面。

function print(s) {
  console.log(s);
}

上面的代碼命名了一個(gè)print函數(shù),以后使用print()這種形式,就可以調(diào)用相應(yīng)的代碼。這叫做函數(shù)的聲明(Function Declaration)。

(2)函數(shù)表達(dá)式

除了用function命令聲明函數(shù),還可以采用變量賦值的寫(xiě)法。

var print = function(s) {
  console.log(s);
};

這種寫(xiě)法將一個(gè)匿名函數(shù)賦值給變量。這時(shí),這個(gè)匿名函數(shù)又稱(chēng)函數(shù)表達(dá)式(Function Expression),因?yàn)橘x值語(yǔ)句的等號(hào)右側(cè)只能放表達(dá)式。

采用函數(shù)表達(dá)式聲明函數(shù)時(shí),function命令后面不帶有函數(shù)名。如果加上函數(shù)名,該函數(shù)名只在函數(shù)體內(nèi)部有效,在函數(shù)體外部無(wú)效。

var print = function x(){
  console.log(typeof x);
};

x
// ReferenceError: x is not defined

print()
// function

上面代碼在函數(shù)表達(dá)式中,加入了函數(shù)名x。這個(gè)x只在函數(shù)體內(nèi)部可用,指代函數(shù)表達(dá)式本身,其他地方都不可用。這種寫(xiě)法的用處有兩個(gè),一是可以在函數(shù)體內(nèi)部調(diào)用自身,二是方便除錯(cuò)(除錯(cuò)工具顯示函數(shù)調(diào)用棧時(shí),將顯示函數(shù)名,而不再顯示這里是一個(gè)匿名函數(shù))。因此,下面的形式聲明函數(shù)也非常常見(jiàn)。

var f = function f() {};

需要注意的是,函數(shù)的表達(dá)式需要在語(yǔ)句的結(jié)尾加上分號(hào),表示語(yǔ)句結(jié)束。而函數(shù)的聲明在結(jié)尾的大括號(hào)后面不用加分號(hào)??偟膩?lái)說(shuō),這兩種聲明函數(shù)的方式,差別很細(xì)微,可以近似認(rèn)為是等價(jià)的。

(3)Function 構(gòu)造函數(shù)

第三種聲明函數(shù)的方式是Function構(gòu)造函數(shù)。

var add = new Function(
  'x',
  'y',
  'return x + y'
);

// 等同于
function add(x, y) {
  return x + y;
}

上面代碼中,Function構(gòu)造函數(shù)接受三個(gè)參數(shù),除了最后一個(gè)參數(shù)是add函數(shù)的“函數(shù)體”,其他參數(shù)都是add函數(shù)的參數(shù)。

你可以傳遞任意數(shù)量的參數(shù)給Function構(gòu)造函數(shù),只有最后一個(gè)參數(shù)會(huì)被當(dāng)做函數(shù)體,如果只有一個(gè)參數(shù),該參數(shù)就是函數(shù)體。

var foo = new Function(
  'return "hello world";'
);

// 等同于
function foo() {
  return 'hello world';
}

Function構(gòu)造函數(shù)可以不使用new命令,返回結(jié)果完全一樣。

總的來(lái)說(shuō),這種聲明函數(shù)的方式非常不直觀,幾乎無(wú)人使用。

函數(shù)的重復(fù)聲明

如果同一個(gè)函數(shù)被多次聲明,后面的聲明就會(huì)覆蓋前面的聲明。

function f() {
  console.log(1);
}
f() // 2

function f() {
  console.log(2);
}
f() // 2

上面代碼中,后一次的函數(shù)聲明覆蓋了前面一次。而且,由于函數(shù)名的提升(參見(jiàn)下文),前一次聲明在任何時(shí)候都是無(wú)效的,這一點(diǎn)要特別注意。

圓括號(hào)運(yùn)算符,return 語(yǔ)句和遞歸

調(diào)用函數(shù)時(shí),要使用圓括號(hào)運(yùn)算符。圓括號(hào)之中,可以加入函數(shù)的參數(shù)。

function add(x, y) {
  return x + y;
}

add(1, 1) // 2

上面代碼中,函數(shù)名后面緊跟一對(duì)圓括號(hào),就會(huì)調(diào)用這個(gè)函數(shù)。

函數(shù)體內(nèi)部的return語(yǔ)句,表示返回。JavaScript 引擎遇到return語(yǔ)句,就直接返回return后面的那個(gè)表達(dá)式的值,后面即使還有語(yǔ)句,也不會(huì)得到執(zhí)行。也就是說(shuō),return語(yǔ)句所帶的那個(gè)表達(dá)式,就是函數(shù)的返回值。return語(yǔ)句不是必需的,如果沒(méi)有的話,該函數(shù)就不返回任何值,或者說(shuō)返回undefined。

函數(shù)可以調(diào)用自身,這就是遞歸(recursion)。下面就是通過(guò)遞歸,計(jì)算斐波那契數(shù)列的代碼。

function fib(num) {
  if (num === 0) return 0;
  if (num === 1) return 1;
  return fib(num - 2) + fib(num - 1);
}

fib(6) // 8

上面代碼中,fib函數(shù)內(nèi)部又調(diào)用了fib,計(jì)算得到斐波那契數(shù)列的第6個(gè)元素是8。

第一等公民

JavaScript 語(yǔ)言將函數(shù)看作一種值,與其它值(數(shù)值、字符串、布爾值等等)地位相同。凡是可以使用值的地方,就能使用函數(shù)。比如,可以把函數(shù)賦值給變量和對(duì)象的屬性,也可以當(dāng)作參數(shù)傳入其他函數(shù),或者作為函數(shù)的結(jié)果返回。函數(shù)只是一個(gè)可以執(zhí)行的值,此外并無(wú)特殊之處。

由于函數(shù)與其他數(shù)據(jù)類(lèi)型地位平等,所以在 JavaScript 語(yǔ)言中又稱(chēng)函數(shù)為第一等公民。

function add(x, y) {
  return x + y;
}

// 將函數(shù)賦值給一個(gè)變量
var operator = add;

// 將函數(shù)作為參數(shù)和返回值
function a(op){
  return op;
}
a(add)(1, 1)
// 2

函數(shù)名的提升

JavaScript 引擎將函數(shù)名視同變量名,所以采用function命令聲明函數(shù)時(shí),整個(gè)函數(shù)會(huì)像變量聲明一樣,被提升到代碼頭部。所以,下面的代碼不會(huì)報(bào)錯(cuò)。

f();

function f() {}

表面上,上面代碼好像在聲明之前就調(diào)用了函數(shù)f。但是實(shí)際上,由于“變量提升”,函數(shù)f被提升到了代碼頭部,也就是在調(diào)用之前已經(jīng)聲明了。但是,如果采用賦值語(yǔ)句定義函數(shù),JavaScript 就會(huì)報(bào)錯(cuò)。

f();
var f = function (){};
// TypeError: undefined is not a function

上面的代碼等同于下面的形式。

var f;
f();
f = function () {};

上面代碼第二行,調(diào)用f的時(shí)候,f只是被聲明了,還沒(méi)有被賦值,等于undefined,所以會(huì)報(bào)錯(cuò)。

注意,如果像下面例子那樣,采用function命令和var賦值語(yǔ)句聲明同一個(gè)函數(shù),由于存在函數(shù)提升,最后會(huì)采用var賦值語(yǔ)句的定義。

var f = function () {
  console.log('1');
}

function f() {
  console.log('2');
}

f() // 1

上面例子中,表面上后面聲明的函數(shù)f,應(yīng)該覆蓋前面的var賦值語(yǔ)句,但是由于存在函數(shù)提升,實(shí)際上正好反過(guò)來(lái)。

函數(shù)的屬性和方法

name 屬性

函數(shù)的name屬性返回函數(shù)的名字。

function f1() {}
f1.name // "f1"

如果是通過(guò)變量賦值定義的函數(shù),那么name屬性返回變量名。

var f2 = function () {};
f2.name // "f2"

但是,上面這種情況,只有在變量的值是一個(gè)匿名函數(shù)時(shí)才是如此。如果變量的值是一個(gè)具名函數(shù),那么name屬性返回function關(guān)鍵字之后的那個(gè)函數(shù)名。

var f3 = function myName() {};
f3.name // 'myName'

上面代碼中,f3.name返回函數(shù)表達(dá)式的名字。注意,真正的函數(shù)名還是f3,而myName這個(gè)名字只在函數(shù)體內(nèi)部可用。

name屬性的一個(gè)用處,就是獲取參數(shù)函數(shù)的名字。

var myFunc = function () {};

function test(f) {
  console.log(f.name);
}

test(myFunc) // myFunc

上面代碼中,函數(shù)test內(nèi)部通過(guò)name屬性,就可以知道傳入的參數(shù)是什么函數(shù)。

length 屬性

函數(shù)的length屬性返回函數(shù)預(yù)期傳入的參數(shù)個(gè)數(shù),即函數(shù)定義之中的參數(shù)個(gè)數(shù)。

function f(a, b) {}
f.length // 2

上面代碼定義了空函數(shù)f,它的length屬性就是定義時(shí)的參數(shù)個(gè)數(shù)。不管調(diào)用時(shí)輸入了多少個(gè)參數(shù),length屬性始終等于2。

length屬性提供了一種機(jī)制,判斷定義時(shí)和調(diào)用時(shí)參數(shù)的差異,以便實(shí)現(xiàn)面向?qū)ο缶幊痰摹胺椒ㄖ剌d”(overload)。

toString()

函數(shù)的toString()方法返回一個(gè)字符串,內(nèi)容是函數(shù)的源碼。

function f() {
  a();
  b();
  c();
}

f.toString()
// function f() {
//  a();
//  b();
//  c();
// }

上面示例中,函數(shù)ftoString()方法返回了f的源碼,包含換行符在內(nèi)。

對(duì)于那些原生的函數(shù),toString()方法返回function (){[native code]}。

Math.sqrt.toString()
// "function sqrt() { [native code] }"

上面代碼中,Math.sqrt()是 JavaScript 引擎提供的原生函數(shù),toString()方法就返回原生代碼的提示。

函數(shù)內(nèi)部的注釋也可以返回。

function f() {/*
  這是一個(gè)
  多行注釋
*/}

f.toString()
// "function f(){/*
//   這是一個(gè)
//   多行注釋
// */}"

利用這一點(diǎn),可以變相實(shí)現(xiàn)多行字符串。

var multiline = function (fn) {
  var arr = fn.toString().split('\n');
  return arr.slice(1, arr.length - 1).join('\n');
};

function f() {/*
  這是一個(gè)
  多行注釋
*/}

multiline(f);
// " 這是一個(gè)
//   多行注釋"

上面示例中,函數(shù)f內(nèi)部有一個(gè)多行注釋?zhuān)?code>toString()方法拿到f的源碼后,去掉首尾兩行,就得到了一個(gè)多行字符串。

函數(shù)作用域

定義

作用域(scope)指的是變量存在的范圍。在 ES5 的規(guī)范中,JavaScript 只有兩種作用域:一種是全局作用域,變量在整個(gè)程序中一直存在,所有地方都可以讀??;另一種是函數(shù)作用域,變量只在函數(shù)內(nèi)部存在。ES6 又新增了塊級(jí)作用域,本教程不涉及。

對(duì)于頂層函數(shù)來(lái)說(shuō),函數(shù)外部聲明的變量就是全局變量(global variable),它可以在函數(shù)內(nèi)部讀取。

var v = 1;

function f() {
  console.log(v);
}

f()
// 1

上面的代碼表明,函數(shù)f內(nèi)部可以讀取全局變量v。

在函數(shù)內(nèi)部定義的變量,外部無(wú)法讀取,稱(chēng)為“局部變量”(local variable)。

function f(){
  var v = 1;
}

v // ReferenceError: v is not defined

上面代碼中,變量v在函數(shù)內(nèi)部定義,所以是一個(gè)局部變量,函數(shù)之外就無(wú)法讀取。

函數(shù)內(nèi)部定義的變量,會(huì)在該作用域內(nèi)覆蓋同名全局變量。

var v = 1;

function f(){
  var v = 2;
  console.log(v);
}

f() // 2
v // 1

上面代碼中,變量v同時(shí)在函數(shù)的外部和內(nèi)部有定義。結(jié)果,在函數(shù)內(nèi)部定義,局部變量v覆蓋了全局變量v。

注意,對(duì)于var命令來(lái)說(shuō),局部變量只能在函數(shù)內(nèi)部聲明,在其他區(qū)塊中聲明,一律都是全局變量。

if (true) {
  var x = 5;
}
console.log(x);  // 5

上面代碼中,變量x在條件判斷區(qū)塊之中聲明,結(jié)果就是一個(gè)全局變量,可以在區(qū)塊之外讀取。

函數(shù)內(nèi)部的變量提升

與全局作用域一樣,函數(shù)作用域內(nèi)部也會(huì)產(chǎn)生“變量提升”現(xiàn)象。var命令聲明的變量,不管在什么位置,變量聲明都會(huì)被提升到函數(shù)體的頭部。

function foo(x) {
  if (x > 100) {
    var tmp = x - 100;
  }
}

// 等同于
function foo(x) {
  var tmp;
  if (x > 100) {
    tmp = x - 100;
  };
}

函數(shù)本身的作用域

函數(shù)本身也是一個(gè)值,也有自己的作用域。它的作用域與變量一樣,就是其聲明時(shí)所在的作用域,與其運(yùn)行時(shí)所在的作用域無(wú)關(guān)。

var a = 1;
var x = function () {
  console.log(a);
};

function f() {
  var a = 2;
  x();
}

f() // 1

上面代碼中,函數(shù)x是在函數(shù)f的外部聲明的,所以它的作用域綁定外層,內(nèi)部變量a不會(huì)到函數(shù)f體內(nèi)取值,所以輸出1,而不是2。

總之,函數(shù)執(zhí)行時(shí)所在的作用域,是定義時(shí)的作用域,而不是調(diào)用時(shí)所在的作用域。

很容易犯錯(cuò)的一點(diǎn)是,如果函數(shù)A調(diào)用函數(shù)B,卻沒(méi)考慮到函數(shù)B不會(huì)引用函數(shù)A的內(nèi)部變量。

var x = function () {
  console.log(a);
};

function y(f) {
  var a = 2;
  f();
}

y(x)
// ReferenceError: a is not defined

上面代碼將函數(shù)x作為參數(shù),傳入函數(shù)y。但是,函數(shù)x是在函數(shù)y體外聲明的,作用域綁定外層,因此找不到函數(shù)y的內(nèi)部變量a,導(dǎo)致報(bào)錯(cuò)。

同樣的,函數(shù)體內(nèi)部聲明的函數(shù),作用域綁定函數(shù)體內(nèi)部。

function foo() {
  var x = 1;
  function bar() {
    console.log(x);
  }
  return bar;
}

var x = 2;
var f = foo();
f() // 1

上面代碼中,函數(shù)foo內(nèi)部聲明了一個(gè)函數(shù)bar,bar的作用域綁定foo。當(dāng)我們?cè)?code>foo外部取出bar執(zhí)行時(shí),變量x指向的是foo內(nèi)部的x,而不是foo外部的x。正是這種機(jī)制,構(gòu)成了下文要講解的“閉包”現(xiàn)象。

參數(shù)

概述

函數(shù)運(yùn)行的時(shí)候,有時(shí)需要提供外部數(shù)據(jù),不同的外部數(shù)據(jù)會(huì)得到不同的結(jié)果,這種外部數(shù)據(jù)就叫參數(shù)。

function square(x) {
  return x * x;
}

square(2) // 4
square(3) // 9

上式的x就是square函數(shù)的參數(shù)。每次運(yùn)行的時(shí)候,需要提供這個(gè)值,否則得不到結(jié)果。

參數(shù)的省略

函數(shù)參數(shù)不是必需的,JavaScript 允許省略參數(shù)。

function f(a, b) {
  return a;
}

f(1, 2, 3) // 1
f(1) // 1
f() // undefined

f.length // 2

上面代碼的函數(shù)f定義了兩個(gè)參數(shù),但是運(yùn)行時(shí)無(wú)論提供多少個(gè)參數(shù)(或者不提供參數(shù)),JavaScript 都不會(huì)報(bào)錯(cuò)。省略的參數(shù)的值就變?yōu)?code>undefined。需要注意的是,函數(shù)的length屬性與實(shí)際傳入的參數(shù)個(gè)數(shù)無(wú)關(guān),只反映函數(shù)預(yù)期傳入的參數(shù)個(gè)數(shù)。

但是,沒(méi)有辦法只省略靠前的參數(shù),而保留靠后的參數(shù)。如果一定要省略靠前的參數(shù),只有顯式傳入undefined。

function f(a, b) {
  return a;
}

f( , 1) // SyntaxError: Unexpected token ,(…)
f(undefined, 1) // undefined

上面代碼中,如果省略第一個(gè)參數(shù),就會(huì)報(bào)錯(cuò)。

傳遞方式

函數(shù)參數(shù)如果是原始類(lèi)型的值(數(shù)值、字符串、布爾值),傳遞方式是傳值傳遞(passes by value)。這意味著,在函數(shù)體內(nèi)修改參數(shù)值,不會(huì)影響到函數(shù)外部。

var p = 2;

function f(p) {
  p = 3;
}
f(p);

p // 2

上面代碼中,變量p是一個(gè)原始類(lèi)型的值,傳入函數(shù)f的方式是傳值傳遞。因此,在函數(shù)內(nèi)部,p的值是原始值的拷貝,無(wú)論怎么修改,都不會(huì)影響到原始值。

但是,如果函數(shù)參數(shù)是復(fù)合類(lèi)型的值(數(shù)組、對(duì)象、其他函數(shù)),傳遞方式是傳址傳遞(pass by reference)。也就是說(shuō),傳入函數(shù)的原始值的地址,因此在函數(shù)內(nèi)部修改參數(shù),將會(huì)影響到原始值。

var obj = { p: 1 };

function f(o) {
  o.p = 2;
}
f(obj);

obj.p // 2

上面代碼中,傳入函數(shù)f的是參數(shù)對(duì)象obj的地址。因此,在函數(shù)內(nèi)部修改obj的屬性p,會(huì)影響到原始值。

注意,如果函數(shù)內(nèi)部修改的,不是參數(shù)對(duì)象的某個(gè)屬性,而是替換掉整個(gè)參數(shù),這時(shí)不會(huì)影響到原始值。

var obj = [1, 2, 3];

function f(o) {
  o = [2, 3, 4];
}
f(obj);

obj // [1, 2, 3]

上面代碼中,在函數(shù)f()內(nèi)部,參數(shù)對(duì)象obj被整個(gè)替換成另一個(gè)值。這時(shí)不會(huì)影響到原始值。這是因?yàn)?,形式參?shù)(o)的值實(shí)際是參數(shù)obj的地址,重新對(duì)o賦值導(dǎo)致o指向另一個(gè)地址,保存在原地址上的值當(dāng)然不受影響。

同名參數(shù)

如果有同名的參數(shù),則取最后出現(xiàn)的那個(gè)值。

function f(a, a) {
  console.log(a);
}

f(1, 2) // 2

上面代碼中,函數(shù)f()有兩個(gè)參數(shù),且參數(shù)名都是a。取值的時(shí)候,以后面的a為準(zhǔn),即使后面的a沒(méi)有值或被省略,也是以其為準(zhǔn)。

function f(a, a) {
  console.log(a);
}

f(1) // undefined

調(diào)用函數(shù)f()的時(shí)候,沒(méi)有提供第二個(gè)參數(shù),a的取值就變成了undefined。這時(shí),如果要獲得第一個(gè)a的值,可以使用arguments對(duì)象。

function f(a, a) {
  console.log(arguments[0]);
}

f(1) // 1

arguments 對(duì)象

(1)定義

由于 JavaScript 允許函數(shù)有不定數(shù)目的參數(shù),所以需要一種機(jī)制,可以在函數(shù)體內(nèi)部讀取所有參數(shù)。這就是arguments對(duì)象的由來(lái)。

arguments對(duì)象包含了函數(shù)運(yùn)行時(shí)的所有參數(shù),arguments[0]就是第一個(gè)參數(shù),arguments[1]就是第二個(gè)參數(shù),以此類(lèi)推。這個(gè)對(duì)象只有在函數(shù)體內(nèi)部,才可以使用。

var f = function (one) {
  console.log(arguments[0]);
  console.log(arguments[1]);
  console.log(arguments[2]);
}

f(1, 2, 3)
// 1
// 2
// 3

正常模式下,arguments對(duì)象可以在運(yùn)行時(shí)修改。

var f = function(a, b) {
  arguments[0] = 3;
  arguments[1] = 2;
  return a + b;
}

f(1, 1) // 5

上面代碼中,函數(shù)f()調(diào)用時(shí)傳入的參數(shù),在函數(shù)內(nèi)部被修改成32

嚴(yán)格模式下,arguments對(duì)象與函數(shù)參數(shù)不具有聯(lián)動(dòng)關(guān)系。也就是說(shuō),修改arguments對(duì)象不會(huì)影響到實(shí)際的函數(shù)參數(shù)。

var f = function(a, b) {
  'use strict'; // 開(kāi)啟嚴(yán)格模式
  arguments[0] = 3;
  arguments[1] = 2;
  return a + b;
}

f(1, 1) // 2

上面代碼中,函數(shù)體內(nèi)是嚴(yán)格模式,這時(shí)修改arguments對(duì)象,不會(huì)影響到真實(shí)參數(shù)ab。

通過(guò)arguments對(duì)象的length屬性,可以判斷函數(shù)調(diào)用時(shí)到底帶幾個(gè)參數(shù)。

function f() {
  return arguments.length;
}

f(1, 2, 3) // 3
f(1) // 1
f() // 0

(2)與數(shù)組的關(guān)系

需要注意的是,雖然arguments很像數(shù)組,但它是一個(gè)對(duì)象。數(shù)組專(zhuān)有的方法(比如sliceforEach),不能在arguments對(duì)象上直接使用。

如果要讓arguments對(duì)象使用數(shù)組方法,真正的解決方法是將arguments轉(zhuǎn)為真正的數(shù)組。下面是兩種常用的轉(zhuǎn)換方法:slice方法和逐一填入新數(shù)組。

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

// 或者
var args = [];
for (var i = 0; i < arguments.length; i++) {
  args.push(arguments[i]);
}

(3)callee 屬性

arguments對(duì)象帶有一個(gè)callee屬性,返回它所對(duì)應(yīng)的原函數(shù)。

var f = function () {
  console.log(arguments.callee === f);
}

f() // true

可以通過(guò)arguments.callee,達(dá)到調(diào)用函數(shù)自身的目的。這個(gè)屬性在嚴(yán)格模式里面是禁用的,因此不建議使用。

函數(shù)的其他知識(shí)點(diǎn)

閉包

閉包(closure)是 JavaScript 語(yǔ)言的一個(gè)難點(diǎn),也是它的特色,很多高級(jí)應(yīng)用都要依靠閉包實(shí)現(xiàn)。

理解閉包,首先必須理解變量作用域。前面提到,JavaScript 有兩種作用域:全局作用域和函數(shù)作用域。函數(shù)內(nèi)部可以直接讀取全局變量。

var n = 999;

function f1() {
  console.log(n);
}
f1() // 999

上面代碼中,函數(shù)f1可以讀取全局變量n。

但是,正常情況下,函數(shù)外部無(wú)法讀取函數(shù)內(nèi)部聲明的變量。

function f1() {
  var n = 999;
}

console.log(n)
// Uncaught ReferenceError: n is not defined

上面代碼中,函數(shù)f1內(nèi)部聲明的變量n,函數(shù)外是無(wú)法讀取的。

如果出于種種原因,需要得到函數(shù)內(nèi)的局部變量。正常情況下,這是辦不到的,只有通過(guò)變通方法才能實(shí)現(xiàn)。那就是在函數(shù)的內(nèi)部,再定義一個(gè)函數(shù)。

function f1() {
  var n = 999;
  function f2() {
  console.log(n); // 999
  }
}

上面代碼中,函數(shù)f2就在函數(shù)f1內(nèi)部,這時(shí)f1內(nèi)部的所有局部變量,對(duì)f2都是可見(jiàn)的。但是反過(guò)來(lái)就不行,f2內(nèi)部的局部變量,對(duì)f1就是不可見(jiàn)的。這就是 JavaScript 語(yǔ)言特有的"鏈?zhǔn)阶饔糜?結(jié)構(gòu)(chain scope),子對(duì)象會(huì)一級(jí)一級(jí)地向上尋找所有父對(duì)象的變量。所以,父對(duì)象的所有變量,對(duì)子對(duì)象都是可見(jiàn)的,反之則不成立。

既然f2可以讀取f1的局部變量,那么只要把f2作為返回值,我們不就可以在f1外部讀取它的內(nèi)部變量了嗎!

function f1() {
  var n = 999;
  function f2() {
    console.log(n);
  }
  return f2;
}

var result = f1();
result(); // 999

上面代碼中,函數(shù)f1的返回值就是函數(shù)f2,由于f2可以讀取f1的內(nèi)部變量,所以就可以在外部獲得f1的內(nèi)部變量了。

閉包就是函數(shù)f2,即能夠讀取其他函數(shù)內(nèi)部變量的函數(shù)。由于在 JavaScript 語(yǔ)言中,只有函數(shù)內(nèi)部的子函數(shù)才能讀取內(nèi)部變量,因此可以把閉包簡(jiǎn)單理解成“定義在一個(gè)函數(shù)內(nèi)部的函數(shù)”。閉包最大的特點(diǎn),就是它可以“記住”誕生的環(huán)境,比如f2記住了它誕生的環(huán)境f1,所以從f2可以得到f1的內(nèi)部變量。在本質(zhì)上,閉包就是將函數(shù)內(nèi)部和函數(shù)外部連接起來(lái)的一座橋梁。

閉包的最大用處有兩個(gè),一個(gè)是可以讀取外層函數(shù)內(nèi)部的變量,另一個(gè)就是讓這些變量始終保持在內(nèi)存中,即閉包可以使得它誕生環(huán)境一直存在。請(qǐng)看下面的例子,閉包使得內(nèi)部變量記住上一次調(diào)用時(shí)的運(yùn)算結(jié)果。

function createIncrementor(start) {
  return function () {
    return start++;
  };
}

var inc = createIncrementor(5);

inc() // 5
inc() // 6
inc() // 7

上面代碼中,start是函數(shù)createIncrementor的內(nèi)部變量。通過(guò)閉包,start的狀態(tài)被保留了,每一次調(diào)用都是在上一次調(diào)用的基礎(chǔ)上進(jìn)行計(jì)算。從中可以看到,閉包inc使得函數(shù)createIncrementor的內(nèi)部環(huán)境,一直存在。所以,閉包可以看作是函數(shù)內(nèi)部作用域的一個(gè)接口。

為什么閉包能夠返回外層函數(shù)的內(nèi)部變量?原因是閉包(上例的inc)用到了外層變量(start),導(dǎo)致外層函數(shù)(createIncrementor)不能從內(nèi)存釋放。只要閉包沒(méi)有被垃圾回收機(jī)制清除,外層函數(shù)提供的運(yùn)行環(huán)境也不會(huì)被清除,它的內(nèi)部變量就始終保存著當(dāng)前值,供閉包讀取。

閉包的另一個(gè)用處,是封裝對(duì)象的私有屬性和私有方法。

function Person(name) {
  var _age;
  function setAge(n) {
    _age = n;
  }
  function getAge() {
    return _age;
  }

  return {
    name: name,
    getAge: getAge,
    setAge: setAge
  };
}

var p1 = Person('張三');
p1.setAge(25);
p1.getAge() // 25

上面代碼中,函數(shù)Person的內(nèi)部變量_age,通過(guò)閉包getAgesetAge,變成了返回對(duì)象p1的私有變量。

注意,外層函數(shù)每次運(yùn)行,都會(huì)生成一個(gè)新的閉包,而這個(gè)閉包又會(huì)保留外層函數(shù)的內(nèi)部變量,所以內(nèi)存消耗很大。因此不能濫用閉包,否則會(huì)造成網(wǎng)頁(yè)的性能問(wèn)題。

立即調(diào)用的函數(shù)表達(dá)式(IIFE)

根據(jù) JavaScript 的語(yǔ)法,圓括號(hào)()跟在函數(shù)名之后,表示調(diào)用該函數(shù)。比如,print()就表示調(diào)用print函數(shù)。

有時(shí),我們需要在定義函數(shù)之后,立即調(diào)用該函數(shù)。這時(shí),你不能在函數(shù)的定義之后加上圓括號(hào),這會(huì)產(chǎn)生語(yǔ)法錯(cuò)誤。

function(){ /* code */ }();
// SyntaxError: Unexpected token (

產(chǎn)生這個(gè)錯(cuò)誤的原因是,function這個(gè)關(guān)鍵字既可以當(dāng)作語(yǔ)句,也可以當(dāng)作表達(dá)式。

// 語(yǔ)句
function f() {}

// 表達(dá)式
var f = function f() {}

當(dāng)作表達(dá)式時(shí),函數(shù)可以定義后直接加圓括號(hào)調(diào)用。

var f = function f(){ return 1}();
f // 1

上面的代碼中,函數(shù)定義后直接加圓括號(hào)調(diào)用,沒(méi)有報(bào)錯(cuò)。原因就是function作為表達(dá)式,引擎就把函數(shù)定義當(dāng)作一個(gè)值。這種情況下,就不會(huì)報(bào)錯(cuò)。

為了避免解析的歧義,JavaScript 規(guī)定,如果function關(guān)鍵字出現(xiàn)在行首,一律解釋成語(yǔ)句。因此,引擎看到行首是function關(guān)鍵字之后,認(rèn)為這一段都是函數(shù)的定義,不應(yīng)該以圓括號(hào)結(jié)尾,所以就報(bào)錯(cuò)了。

函數(shù)定義后立即調(diào)用的解決方法,就是不要讓function出現(xiàn)在行首,讓引擎將其理解成一個(gè)表達(dá)式。最簡(jiǎn)單的處理,就是將其放在一個(gè)圓括號(hào)里面。

(function(){ /* code */ }());
// 或者
(function(){ /* code */ })();

上面兩種寫(xiě)法都是以圓括號(hào)開(kāi)頭,引擎就會(huì)認(rèn)為后面跟的是一個(gè)表達(dá)式,而不是函數(shù)定義語(yǔ)句,所以就避免了錯(cuò)誤。這就叫做“立即調(diào)用的函數(shù)表達(dá)式”(Immediately-Invoked Function Expression),簡(jiǎn)稱(chēng) IIFE。

注意,上面兩種寫(xiě)法最后的分號(hào)都是必須的。如果省略分號(hào),遇到連著兩個(gè) IIFE,可能就會(huì)報(bào)錯(cuò)。

// 報(bào)錯(cuò)
(function(){ /* code */ }())
(function(){ /* code */ }())

上面代碼的兩行之間沒(méi)有分號(hào),JavaScript 會(huì)將它們連在一起解釋?zhuān)瑢⒌诙薪忉尀榈谝恍械膮?shù)。

推而廣之,任何讓解釋器以表達(dá)式來(lái)處理函數(shù)定義的方法,都能產(chǎn)生同樣的效果,比如下面三種寫(xiě)法。

var i = function(){ return 10; }();
true && function(){ /* code */ }();
0, function(){ /* code */ }();

甚至像下面這樣寫(xiě),也是可以的。

!function () { /* code */ }();
~function () { /* code */ }();
-function () { /* code */ }();
+function () { /* code */ }();

通常情況下,只對(duì)匿名函數(shù)使用這種“立即執(zhí)行的函數(shù)表達(dá)式”。它的目的有兩個(gè):一是不必為函數(shù)命名,避免了污染全局變量;二是 IIFE 內(nèi)部形成了一個(gè)單獨(dú)的作用域,可以封裝一些外部無(wú)法讀取的私有變量。

// 寫(xiě)法一
var tmp = newData;
processData(tmp);
storeData(tmp);

// 寫(xiě)法二
(function () {
  var tmp = newData;
  processData(tmp);
  storeData(tmp);
}());

上面代碼中,寫(xiě)法二比寫(xiě)法一更好,因?yàn)橥耆苊饬宋廴救肿兞俊?/p>

eval 命令

基本用法

eval命令接受一個(gè)字符串作為參數(shù),并將這個(gè)字符串當(dāng)作語(yǔ)句執(zhí)行。

eval('var a = 1;');
a // 1

上面代碼將字符串當(dāng)作語(yǔ)句運(yùn)行,生成了變量a。

如果參數(shù)字符串無(wú)法當(dāng)作語(yǔ)句運(yùn)行,那么就會(huì)報(bào)錯(cuò)。

eval('3x') // Uncaught SyntaxError: Invalid or unexpected token

放在eval中的字符串,應(yīng)該有獨(dú)自存在的意義,不能用來(lái)與eval以外的命令配合使用。舉例來(lái)說(shuō),下面的代碼將會(huì)報(bào)錯(cuò)。

eval('return;'); // Uncaught SyntaxError: Illegal return statement

上面代碼會(huì)報(bào)錯(cuò),因?yàn)?code>return不能單獨(dú)使用,必須在函數(shù)中使用。

如果eval的參數(shù)不是字符串,那么會(huì)原樣返回。

eval(123) // 123

eval沒(méi)有自己的作用域,都在當(dāng)前作用域內(nèi)執(zhí)行,因此可能會(huì)修改當(dāng)前作用域的變量的值,造成安全問(wèn)題。

var a = 1;
eval('a = 2');

a // 2

上面代碼中,eval命令修改了外部變量a的值。由于這個(gè)原因,eval有安全風(fēng)險(xiǎn)。

為了防止這種風(fēng)險(xiǎn),JavaScript 規(guī)定,如果使用嚴(yán)格模式,eval內(nèi)部聲明的變量,不會(huì)影響到外部作用域。

(function f() {
  'use strict';
  eval('var foo = 123');
  console.log(foo);  // ReferenceError: foo is not defined
})()

上面代碼中,函數(shù)f內(nèi)部是嚴(yán)格模式,這時(shí)eval內(nèi)部聲明的foo變量,就不會(huì)影響到外部。

不過(guò),即使在嚴(yán)格模式下,eval依然可以讀寫(xiě)當(dāng)前作用域的變量。

(function f() {
  'use strict';
  var foo = 1;
  eval('foo = 2');
  console.log(foo);  // 2
})()

上面代碼中,嚴(yán)格模式下,eval內(nèi)部還是改寫(xiě)了外部變量,可見(jiàn)安全風(fēng)險(xiǎn)依然存在。

總之,eval的本質(zhì)是在當(dāng)前作用域之中,注入代碼。由于安全風(fēng)險(xiǎn)和不利于 JavaScript 引擎優(yōu)化執(zhí)行速度,一般不推薦使用。通常情況下,eval最常見(jiàn)的場(chǎng)合是解析 JSON 數(shù)據(jù)的字符串,不過(guò)正確的做法應(yīng)該是使用原生的JSON.parse方法。

eval 的別名調(diào)用

前面說(shuō)過(guò)eval不利于引擎優(yōu)化執(zhí)行速度。更麻煩的是,還有下面這種情況,引擎在靜態(tài)代碼分析的階段,根本無(wú)法分辨執(zhí)行的是eval。

var m = eval;
m('var x = 1');
x // 1

上面代碼中,變量meval的別名。靜態(tài)代碼分析階段,引擎分辨不出m('var x = 1')執(zhí)行的是eval命令。

為了保證eval的別名不影響代碼優(yōu)化,JavaScript 的標(biāo)準(zhǔn)規(guī)定,凡是使用別名執(zhí)行eval,eval內(nèi)部一律是全局作用域。

var a = 1;

function f() {
  var a = 2;
  var e = eval;
  e('console.log(a)');
}

f() // 1

上面代碼中,eval是別名調(diào)用,所以即使它是在函數(shù)中,它的作用域還是全局作用域,因此輸出的a為全局變量。這樣的話,引擎就能確認(rèn)e()不會(huì)對(duì)當(dāng)前的函數(shù)作用域產(chǎn)生影響,優(yōu)化的時(shí)候就可以把這一行排除掉。

eval的別名調(diào)用的形式五花八門(mén),只要不是直接調(diào)用,都屬于別名調(diào)用,因?yàn)橐嬷荒芊直?code>eval()這一種形式是直接調(diào)用。

eval.call(null, '...')
window.eval('...')
(1, eval)('...')
(eval, eval)('...')

上面這些形式都是eval的別名調(diào)用,作用域都是全局作用域。

參考鏈接


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)