(七)數(shù)據(jù)類型之函數(shù)基礎(chǔ)

1 概述



函數(shù)是一段可以反復(fù)調(diào)用的代碼塊隅俘。函數(shù)還能接受輸入的參數(shù)我磁,不同的參數(shù)會(huì)返回不同的值煎饼。

1.1函數(shù)的聲明

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ù)怎囚,還可以采用變量賦值的寫法。

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

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

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

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á)式本身咽袜,其他地方都不可用丸卷。
這種寫法的用處有兩個(gè),一是可以在函數(shù)體內(nèi)部調(diào)用自身询刹,二是方便除錯(cuò)(除錯(cuò)工具顯示函數(shù)調(diào)用棧時(shí)谜嫉,將顯示函數(shù)名萎坷,而不再顯示這里是一個(gè)匿名函數(shù))。因此沐兰,下面的形式聲明函數(shù)也非常常見哆档。

var f = function f() {};

需要注意的是,函數(shù)的表達(dá)式需要在語句的結(jié)尾加上分號(hào)僧鲁,表示語句結(jié)束虐呻。而函數(shù)的聲明在結(jié)尾的大括號(hào)后面不用加分號(hào)∧海總的來說斟叼,這兩種聲明函數(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é)果完全一樣耳璧。
總的來說成箫,這種聲明函數(shù)的方式非常不直觀,幾乎無人使用旨枯。

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

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

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

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

面代碼中攀隔,后一次的函數(shù)聲明覆蓋了前面一次皂贩。而且,由于函數(shù)名的提升昆汹,前一次聲明在任何時(shí)候都是無效的先紫,這一點(diǎn)要特別注意。

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

說明:調(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語句,表示返回沟饥。JavaScript 引擎遇到return語句添怔,就直接返回return后面的那個(gè)表達(dá)式的值,后面即使還有語句贤旷,也不會(huì)得到執(zhí)行广料。也就是說,return語句所帶的那個(gè)表達(dá)式幼驶,就是函數(shù)的返回值艾杏。return語句不是必需的,如果沒有的話盅藻,該函數(shù)就不返回任何值购桑,或者說返回undefined

函數(shù)可以調(diào)用自身氏淑,這就是遞歸(recursion)勃蜘。下面就是通過遞歸,計(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守问。

1.4 第一等公民

JavaScript 語言將函數(shù)看作一種值匀归,與其它值(數(shù)值、字符串耗帕、布爾值等等)地位相同穆端。凡是可以使用值的地方,就能使用函數(shù)仿便。比如体啰,可以把函數(shù)賦值給變量和對(duì)象的屬性,也可以當(dāng)作參數(shù)傳入其他函數(shù)嗽仪,或者作為函數(shù)的結(jié)果返回荒勇。函數(shù)只是一個(gè)可以執(zhí)行的值,此外并無特殊之處闻坚。
由于函數(shù)與其他數(shù)據(jù)類型地位平等沽翔,所以在 JavaScript 語言中又稱函數(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

1.5 函數(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)聲明了堤舒。但是,如果采用賦值語句定義函數(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只是被聲明了,還沒有被賦值驾锰,等于undefined卸留,所以會(huì)報(bào)錯(cuò)。因此椭豫,如果同時(shí)采用function命令和賦值語句聲明同一個(gè)函數(shù)耻瑟,最后總是采用賦值語句的定義。

var f = function () {
  console.log('錯(cuò)的是世界');
}

function f() {
  console.log('這一切都是命運(yùn)石之門的選擇');
}

f() // 錯(cuò)的是世界

1.6 不能在條件語句中聲明函數(shù)

根據(jù) ES5 的規(guī)范赏酥,不得在非函數(shù)的代碼塊中聲明函數(shù)喳整,最常見的情況就是iftry語句。

if (foo) {
  function x() {}
}

try {
  function x() {}
} catch(e) {
  console.log(e);
}

上面代碼分別在if代碼塊和try代碼塊中聲明了兩個(gè)函數(shù)裸扶,按照語言規(guī)范框都,這是不合法的。但是呵晨,實(shí)際情況是各家瀏覽器往往并不報(bào)錯(cuò)魏保,能夠運(yùn)行。

但是由于存在函數(shù)名的提升摸屠,所以在條件語句中聲明函數(shù)谓罗,可能是無效的,這是非常容易出錯(cuò)的地方季二。

if (false) {
  function f() {}
}

f() // 不報(bào)錯(cuò)

上面代碼的原始意圖是不聲明函數(shù)f檩咱,但是由于f的提升,導(dǎo)致if語句無效,所以上面的代碼不會(huì)報(bào)錯(cuò)税手。要達(dá)到在條件語句中定義函數(shù)的目的蜂筹,只有使用函數(shù)表達(dá)式。

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

f() // undefined

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


2.1 name 屬性

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

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

如果是通過變量賦值定義的函數(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)部通過name屬性,就可以知道傳入的參數(shù)是什么函數(shù)梳杏。

2.2 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)减响。

2.3 toString()

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

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

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

函數(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è)
//   多行注釋"

3 函數(shù)作用域


3.1 定義

作用域(scope)指的是變量存在的范圍颂鸿。在 ES5 的規(guī)范中促绵,Javascript 只有兩種作用域:

一種是全局作用域,變量在整個(gè)程序中一直存在,所有地方都可以讀劝芮纭浓冒;
另一種是函數(shù)作用域,變量只在函數(shù)內(nèi)部存在尖坤。

函數(shù)外部聲明的變量就是全局變量(global variable)稳懒,它可以在函數(shù)內(nèi)部讀取。

var v = 1;

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

f()
// 1

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

在函數(shù)內(nèi)部定義的變量,外部無法讀取纯路,稱為“局部變量”(local variable)或油。

function f(){
  var v = 1;
}

v // ReferenceError: v is not defined

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

函數(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命令來說,局部變量只能在函數(shù)內(nèi)部聲明寇损,在其他區(qū)塊中聲明凸郑,一律都是全局變量

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

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

3.2 函數(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;
  };
}

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

函數(shù)本身也是一個(gè)值墩衙,也有自己的作用域务嫡。它的作用域與變量一樣甲抖,就是其聲明時(shí)所在的作用域,與其運(yùn)行時(shí)所在的作用域無關(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档押,卻沒考慮到函數(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ù)barbar的作用域綁定foo哨毁。當(dāng)我們?cè)?strong>foo外部取出bar執(zhí)行時(shí)枫甲,變量x指向的是foo內(nèi)部的x,而不是foo外部的x扼褪。正是這種機(jī)制想幻,構(gòu)成了下文要講解的“閉包”現(xiàn)象。

4 參數(shù)


4.1 概述

函數(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é)果叛买。

4.2 參數(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í)無論提供多少個(gè)參數(shù)(或者不提供參數(shù))刻伊,JavaScript 都不會(huì)報(bào)錯(cuò)。省略的參數(shù)的值就變?yōu)?strong>undefined椒功。需要注意的是捶箱,函數(shù)的length屬性與實(shí)際傳入的參數(shù)個(gè)數(shù)無關(guān),只反映函數(shù)預(yù)期傳入的參數(shù)個(gè)數(shù)动漾。

但是丁屎,沒有辦法只省略靠前的參數(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í)際開發(fā)不建議這樣做會(huì)造成錯(cuò)誤呀页,設(shè)定好的參數(shù)個(gè)數(shù)和傳入值的個(gè)數(shù)要一致妈拌。

4.3 傳遞方式

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

var p = 2;

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

p // 2

上面代碼中雪位,變量p是一個(gè)原始類型的值竭钝,傳入函數(shù)f的方式是傳值傳遞。因此雹洗,在函數(shù)內(nèi)部香罐,p的值是原始值的拷貝,無論怎么修改时肿,都不會(huì)影響到原始值庇茫。

但是,如果函數(shù)參數(shù)是復(fù)合類型的值(數(shù)組螃成、對(duì)象旦签、其他函數(shù))查坪,傳遞方式是傳址傳遞(pass by reference)。
!!!也就是說宁炫,傳入函數(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)槔U问絽?shù)(o)的值實(shí)際是參數(shù)obj的地址界拦,重新對(duì)o賦值導(dǎo)致o指向另一個(gè)地址,保存在原地址上的值當(dāng)然不受影響梗劫。

4.4 同名參數(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沒有值或被省略,也是以其為準(zhǔn)丙躏。

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

f(1) // undefined

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

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

f(1) // 1

4.5 arguments 對(duì)象

(1)定義
由于 JavaScript 允許函數(shù)有不定數(shù)目的參數(shù)谈秫,所以需要一種機(jī)制扒寄,可以在函數(shù)體內(nèi)部讀取所有參數(shù)。這就是arguments對(duì)象的由來拟烫。
arguments對(duì)象包含了函數(shù)運(yùn)行時(shí)的所有參數(shù)该编,arguments[0]就是第一個(gè)參數(shù),arguments[1]就是第二個(gè)參數(shù)构灸,以此類推上渴。
注意,這個(gè)對(duì)象只有在函數(shù)體內(nèi)部喜颁,才可以使用稠氮。

ar 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ì)象是一個(gè)只讀對(duì)象,修改它是無效的纠永,但不會(huì)報(bào)錯(cuò)鬓长。

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

f(1, 1) // 2

上面代碼中,函數(shù)體內(nèi)是嚴(yán)格模式尝江,這時(shí)修改arguments對(duì)象就是無效的涉波。

通過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ù)組專有的方法(比如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

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

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


5.1 閉包

閉包(closure)是 Javascript 語言的一個(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ù)外部無法讀取函數(shù)內(nèi)部聲明的變量弃衍。

function f1() {
  var n = 999;
}

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

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

如果出于種種原因岸裙,需要得到函數(shù)內(nèi)的局部變量。正常情況下速缆,這是辦不到的降允,只有通過變通方法才能實(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都是可見的侠草。但是反過來就不行辱挥,f2內(nèi)部的局部變量,對(duì)f1就是不可見的边涕。這就是 JavaScript 語言特有的”鏈?zhǔn)阶饔糜?/strong>”結(jié)構(gòu)(chain scope)

鏈?zhǔn)阶饔糜?/strong> :子對(duì)象會(huì)一級(jí)一級(jí)地向上尋找所有父對(duì)象的變量晤碘。所以,父對(duì)象的所有變量功蜓,對(duì)子對(duì)象都是可見的园爷,反之則不成立。

既然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 語言中,只有函數(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ù)外部連接起來的一座橋梁

閉包的最大用處有兩個(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)部變量。通過閉包棒妨,start的狀態(tài)被保留了踪古,每一次調(diào)用都是在上一次調(diào)用的基礎(chǔ)上進(jìn)行計(jì)算。從中可以看到券腔,閉包inc使得函數(shù)createIncrementor的內(nèi)部環(huán)境伏穆,一直存在。所以纷纫,閉包可以看作是函數(shù)內(nèi)部作用域的一個(gè)接口枕扫。

為什么會(huì)這樣呢?原因就在于inc始終在內(nèi)存中辱魁,而inc的存在依賴于createIncrementor烟瞧,因此也始終在內(nèi)存中,不會(huì)在調(diào)用結(jié)束后染簇,被垃圾回收機(jī)制回收参滴。

閉包的另一個(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(22);
p1.getAge() // 22

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

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

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

在 Javascript 中,圓括號(hào)()是一種運(yùn)算符击胜,跟在函數(shù)名之后亏狰,表示調(diào)用該函數(shù)。比如偶摔,print()就表示調(diào)用print函數(shù)暇唾。

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

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

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

// 語句
function f() {}

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

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

解決方法就是不要讓function出現(xiàn)在行首琢感,讓引擎將其理解成一個(gè)表達(dá)式丢间。最簡(jiǎn)單的處理,就是將其放在一個(gè)圓括號(hào)里面驹针。

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

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

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

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

上面代碼的兩行之間沒有分號(hào),JavaScript 會(huì)將它們連在一起解釋嚎货,將第二行解釋為第一行的參數(shù)橘霎。

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

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

甚至像下面這樣寫洗显,也是可以的外潜。

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

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

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

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

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

6 eval 命令 (新手少用)



eval命令的作用是嵌灰,將字符串當(dāng)作語句執(zhí)行。

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

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

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

eval('return;');

eval沒有自己的作用域豌鹤,都在當(dāng)前作用域內(nèi)執(zhí)行,因此可能會(huì)修改當(dāng)前作用域的變量的值枝缔,在網(wǎng)站上使用這樣的方法容易遭受XSS跨站腳本攻擊布疙。

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ì)影響到外部舔痪。

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

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

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

此外余黎,eval的命令字符串不會(huì)得到 JavaScript 引擎的優(yōu)化重窟,運(yùn)行速度較慢。這也是一個(gè)不應(yīng)該使用它的理由惧财。

通常情況下巡扇,eval最常見的場(chǎng)合是解析 JSON 數(shù)據(jù)字符串,
不過正確的做法應(yīng)該是使用瀏覽器提供的JSON.parse方法垮衷。

JavaScript 引擎內(nèi)部厅翔,eval實(shí)際上是一個(gè)引用,默認(rèn)調(diào)用一個(gè)內(nèi)部方法搀突。這使得eval的使用分成兩種情況刀闷,一種是像上面這樣的調(diào)用eval(expression),這叫做“直接使用”仰迁,這種情況下eval的作用域就是當(dāng)前作用域甸昏。除此之外的調(diào)用方法,都叫“間接調(diào)用”徐许,此時(shí)eval的作用域總是全局作用域施蜜。

var a = 1;

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

f() // 1

上面代碼中,eval是間接調(diào)用绊寻,所以即使它是在函數(shù)中,它的作用域還是全局作用域,因此輸出的a為全局變量澄步。

eval的間接調(diào)用的形式五花八門冰蘑,只要不是直接調(diào)用,都屬于間接調(diào)用村缸。

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

上面這些形式都是eval的間接調(diào)用祠肥,因此它們的作用域都是全局作用域。

eval作用類似的還有Function構(gòu)造函數(shù)梯皿。利用它生成一個(gè)函數(shù)仇箱,然后調(diào)用該函數(shù),也能將字符串當(dāng)作命令執(zhí)行东羹。

var jsonp = 'foo({"id": 42})';

var f = new Function( 'foo', jsonp );
// 相當(dāng)于定義了如下函數(shù)
// function f(foo) {
//   foo({"id":42});
// }

f(function (json) {
  console.log( json.id ); // 42
})

上面代碼中剂桥,jsonp是一個(gè)字符串,Function構(gòu)造函數(shù)將這個(gè)字符串属提,變成了函數(shù)體权逗。調(diào)用該函數(shù)的時(shí)候,jsonp就會(huì)執(zhí)行冤议。這種寫法的實(shí)質(zhì)是將代碼放到函數(shù)作用域執(zhí)行斟薇,避免對(duì)全局作用域造成影響。

不過恕酸,new Function()的寫法也可以讀寫全局作用域堪滨,所以也是應(yīng)該避免使用它。
對(duì)于eval()方法蕊温,為了安全性等原因袱箱,在實(shí)際開發(fā)中盡量少用。

本文大量參考借鑒了阮一峰老師的博客《JavaScript 標(biāo)準(zhǔn)參考教程》寿弱,他的文章使我受益匪淺犯眠,在此表示由衷的感謝!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末症革,一起剝皮案震驚了整個(gè)濱河市筐咧,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌噪矛,老刑警劉巖量蕊,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異艇挨,居然都是意外死亡残炮,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門缩滨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來势就,“玉大人泉瞻,你說我怎么就攤上這事“耄” “怎么了袖牙?”我有些...
    開封第一講書人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)舅锄。 經(jīng)常有香客問我鞭达,道長(zhǎng),這世上最難降的妖魔是什么皇忿? 我笑而不...
    開封第一講書人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任畴蹭,我火速辦了婚禮,結(jié)果婚禮上鳍烁,老公的妹妹穿的比我還像新娘叨襟。我一直安慰自己,他們只是感情好老翘,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開白布芹啥。 她就那樣靜靜地躺著,像睡著了一般铺峭。 火紅的嫁衣襯著肌膚如雪墓怀。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,144評(píng)論 1 285
  • 那天卫键,我揣著相機(jī)與錄音傀履,去河邊找鬼。 笑死莉炉,一個(gè)胖子當(dāng)著我的面吹牛钓账,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播絮宁,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼梆暮,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了绍昂?” 一聲冷哼從身側(cè)響起啦粹,我...
    開封第一講書人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎窘游,沒想到半個(gè)月后唠椭,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡忍饰,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年贪嫂,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片艾蓝。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡力崇,死狀恐怖斗塘,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情亮靴,我是刑警寧澤逛拱,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站台猴,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏俱两。R本人自食惡果不足惜饱狂,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望宪彩。 院中可真熱鬧休讳,春花似錦、人聲如沸尿孔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)活合。三九已至雏婶,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間白指,已是汗流浹背留晚。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留告嘲,地道東北人错维。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像橄唬,于是被迫代替她去往敵國(guó)和親赋焕。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 第2章 基本語法 2.1 概述 基本句法和變量 語句 JavaScript程序的執(zhí)行單位為行(line)仰楚,也就是一...
    悟名先生閱讀 4,118評(píng)論 0 13
  • 函數(shù)和對(duì)象 1隆判、函數(shù) 1.1 函數(shù)概述 函數(shù)對(duì)于任何一門語言來說都是核心的概念。通過函數(shù)可以封裝任意多條語句缸血,而且...
    道無虛閱讀 4,527評(píng)論 0 5
  • 函數(shù)是一段可以反復(fù)調(diào)用的代碼塊蜜氨。函數(shù)還能接受輸入的參數(shù),不同的參數(shù)會(huì)返回不同的值捎泻。 概述 函數(shù)的聲明 JavaSc...
    許先生__閱讀 444評(píng)論 0 1
  • Lua 5.1 參考手冊(cè) by Roberto Ierusalimschy, Luiz Henrique de F...
    蘇黎九歌閱讀 13,744評(píng)論 0 38
  • 女孩子總是會(huì)有些愛面子飒炎,或者說是靦腆。和約會(huì)對(duì)象見了一次面后笆豁,互留了電話郎汪,男方若沒有打電話來總會(huì)覺得錯(cuò)在對(duì)方赤赊,猜...
    eileen寶貝閱讀 310評(píng)論 0 2