JavaScript函數(1),前端開發工作

移動前端入門 2021-09-19 20:20:51 阅读数:529

javascript 前端 工作

還有其它的方式來調用函數。常見的一些情形是某些地方需要動態調用函數,或者函數的實參數量是變化的,或者調用函數的上下文需要指定為在運行時確定的特定對象。顯然,函數本身就是對象,因此這些對象也有方法。作為此中情形之一,apply()方法可以實現這些目的。

函數作用域

在函數內定義的變量不能在函數之外的任何地方訪問,因為變量僅僅在該函數的域的內部有定義。相對應的,一個函數可以訪問定義在其範圍內的任何變量和函數。換言之,定義在全局域中的函數可以訪問所有定義在全局域中的變量。在另一個函數中定義的函數也可以訪問在其父函數中定義的所有變量和父函數有權訪問的任何其他變量。

// 下面的變量定義在全局作用域(global scope)中
var num1 = 20,
num2 = 3,
name = "Chamahk";
// 本函數定義在全局作用域
function multiply() {
return num1 * num2;
}
multiply(); // 返回 60
// 嵌套函數的例子
function getScore() {
var num1 = 2,
num2 = 3;
function add() {
return name + " scored " + (num1 + num2);
}
return add();
}
getScore(); // 返回 "Chamahk scored 5"

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.

作用域和函數堆棧

遞歸

一個函數可以指向並調用自身。有三種方法可以達到這個目的:

  1. 函數名
  2. `arguments.callee
  3. 作用域下的一個指向該函數的變量名

例如,思考一下如下的函數定義:

var foo = function bar() {
// statements go here
};

  • 1.
  • 2.
  • 3.
  • 4.

在這個函數體內,以下的語句是等價的:

  1. bar()
  2. arguments.callee()?(譯者注:ES5禁止在嚴格模式下使用此屬性)
  3. foo()

調用自身的函數我們稱之為遞歸函數。在某種意義上說,遞歸近似於循環。兩者都重複執行相同的代碼,並且兩者都需要一個終止條件(避免無限循環或者無限遞歸)。例如以下的循環:

var x = 0;
while (x < 10) { // "x < 10" 是循環條件
// do stuff
x++;
}

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

可以被轉化成一個遞歸函數和對其的調用:

function loop(x) {
if (x >= 10) // "x >= 10" 是退出條件(等同於 "!(x < 10)")
return;
// 做些什麼
loop(x + 1); // 遞歸調用
}
loop(0);

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

不過,有些算法並不能簡單的用迭代來實現。例如,獲取樹結構中所有的節點時,使用遞歸實現要容易得多:

function walkTree(node) {
if (node == null) //
return;
// do something with node
for (var i = 0; i < node.childNodes.length; i++) {
walkTree(node.childNodes[i]);
}
}

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

loop函數相比,這裏每個遞歸調用都產生了更多的遞歸。

將遞歸算法轉換為非遞歸算法是可能的,不過邏輯上通常會更加複雜,而且需要使用堆棧。事實上,遞歸函數就使用了堆棧:函數堆棧。

這種類似堆棧的行為可以在下例中看到:

function foo(i) {
if (i < 0)
return;
console.log('begin:' + i);
foo(i - 1);
console.log('end:' + i);
}
foo(3);
// 輸出:
// begin:3
// begin:2
// begin:1
// begin:0
// end:0
// end:1
// end:2
// end:3

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.

嵌套函數和閉包

你可以在一個函數裏面嵌套另外一個函數。嵌套(內部)函數對其容器(外部)函數是私有的。它自身也形成了一個閉包。一個閉包是一個可以自己擁有獨立的環境與變量的錶達式(通常是函數)。

既然嵌套函數是一個閉包,就意味著一個嵌套函數可以”繼承“容器函數的參數和變量。換句話說,內部函數包含外部函數的作用域。

可以總結如下:

  • 內部函數只可以在外部函數中訪問。
  • 內部函數形成了一個閉包:它可以訪問外部函數的參數和變量,但是外部函數卻不能使用它的參數和變量。

下面的例子展示了嵌套函數:

function addSquares(a, b) {
function square(x) {
return x * x;
}
return square(a) + square(b);
}
a = addSquares(2, 3); // returns 13
b = addSquares(3, 4); // returns 25
c = addSquares(4, 5); // returns 41

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

由於內部函數形成了閉包,因此你可以調用外部函數並為外部函數和內部函數指定參數:

function outside(x) {
function inside(y) {
return x + y;
}
return inside;
}
fn_inside = outside(3); // 可以這樣想:給一個函數,使它的值加3
result = fn_inside(5); // returns 8
result1 = outside(3)(5); // returns 8

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

保存變量

注意到上例中?inside?被返回時?x?是怎麼被保留下來的。一個閉包必須保存它可見作用域中所有參數和變量。因為每一次調用傳入的參數都可能不同,每一次對外部函數的調用實際上重新創建了一遍這個閉包。只有當返回的?inside?沒有再被引用時,內存才會被釋放。

這與在其他對象中存儲引用沒什麼不同,但是通常不太明顯,因為並不能直接設置引用,也不能檢查它們。

多層嵌套函數

函數可以被多層嵌套。例如,函數A可以包含函數B,函數B可以再包含函數C。B和C都形成了閉包,所以B可以訪問A,C可以訪問B和A。因此,閉包可以包含多個作用域;他們遞歸式的包含了所有包含它的函數作用域。這個稱之為作用域鏈。(稍後會詳細解釋)

思考一下下面的例子:

function A(x) {
function B(y) {
function C(z) {
console.log(x + y + z);
}
C(3);
}
B(2);
}
A(1); // logs 6 (1 + 2 + 3)

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

在這個例子裏面,C可以訪問B的y和A的x。這是因為:

  1. B形成了一個包含A的閉包,B可以訪問A的參數和變量
  2. C形成了一個包含B的閉包
  3. B包含A,所以C也包含A,C可以訪問B和A的參數和變量。換言之,C用這個順序鏈接了B和A的作用域

反過來卻不是這樣。A不能訪問C,因為A看不到B中的參數和變量,C是B中的一個變量,所以C是B私有的。

命名沖突

當同一個閉包作用域下兩個參數或者變量同名時,就會產生命名沖突。更近的作用域有更高的優先權,所以最近的優先級最高,最遠的優先級最低。這就是作用域鏈。鏈的第一個元素就是最裏面的作用域,最後一個元素便是最外層的作用域。

看以下的例子:

function outside() {
var x = 5;
function inside(x) {
return x * 2;
}
return inside;
}
outside()(10); // returns 20 instead of 10

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

命名沖突發生在return x上,inside的參數xoutside變量x發生了沖突。這裏的作用鏈域是{inside,?outside, 全局對象}。因此insidex具有最高優先權,返回了20(insidex)而不是10(outsidex)。

閉包

閉包是 JavaScript 中最强大的特性之一。JavaScript 允許函數嵌套,並且內部函數可以訪問定義在外部函數中的所有變量和函數,以及外部函數能訪問的所有變量和函數。

但是,外部函數卻不能够訪問定義在內部函數中的變量和函數。這給內部函數的變量提供了一定的安全性。

此外,由於內部函數可以訪問外部函數的作用域,因此當內部函數生存周期大於外部函數時,外部函數中定義的變量和函數的生存周期將比內部函數執行時間長。當內部函數以某一種方式被任何一個外部函數作用域訪問時,一個閉包就產生了。

var pet = function(name) { //外部函數定義了一個變量"name"
var getName = function() {
//內部函數可以訪問 外部函數定義的"name"
return name;
}
//返回這個內部函數,從而將其暴露在外部函數作用域
return getName;
};
myPet = pet("Vivie");
myPet(); // 返回結果 "Vivie"

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

實際上可能會比上面的代碼複雜的多。在下面這種情形中,返回了一個包含可以操作外部函數的內部變量方法的對象。

var createPet = function(name) {
var sex;
return {
setName: function(newName) {
name = newName;
},
getName: function() {
return name;
},
getSex: function() {
return sex;
},
setSex: function(newSex) {
if(typeof newSex == "string"
&& (newSex.toLowerCase() == "male" || newSex.toLowerCase() == "female")) {
sex = newSex;
}
}
}
}
var pet = createPet("Vivie");
pet.getName(); // Vivie
pet.setName("Oliver");
pet.setSex("male");
pet.getSex(); // male
pet.getName(); // Oliver

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.

在上面的代碼中,外部函數的name變量對內嵌函數來說是可取得的,而除了通過內嵌函數本身,沒有其它任何方法可以取得內嵌的變量。內嵌函數的內嵌變量就像內嵌函數的保險櫃。它們會為內嵌函數保留“穩定”——而又安全——的數據參與運行。而這些內嵌函數甚至不會被分配給一個變量,或者不必一定要有名字。

var getCode = (function(){
var secureCode = "0]Eal(eh&2"; // A code we do not want outsiders to be able to modify...
return function () {
return secureCode;
};
})();
getCode(); // Returns the secret code

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

盡管有上述優點,使用閉包時仍然要小心避免一些陷阱。如果一個閉包的函數定義了一個和外部函數的某個變量名稱相同的變量,那麼這個閉包將無法引用外部函數的這個變量。

var createPet = function(name) { // Outer function defines a variable called "name"
return {
setName: function(name) { // Enclosed function also defines a variable called "name"
name = name; // ??? How do we access the "name" defined by the outer function ???
}
}
}

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

使用 arguments 對象

函數的實際參數會被保存在一個類似數組的arguments對象中。在函數內,你可以按如下方式找出傳入的參數:

arguments[i]

  • 1.
  • 2.

其中i是參數的序數編號(譯注:數組索引),以0開始。所以第一個傳來的參數會是arguments[0]。參數的數量由arguments.length錶示。

使用arguments對象,你可以處理比聲明的更多的參數來調用函數。這在你事先不知道會需要將多少參數傳遞給函數時十分有用。你可以用arguments.length來獲得實際傳遞給函數的參數的數量,然後用arguments對象來取得每個參數。

例如,設想有一個用來連接字符串的函數。唯一事先確定的參數是在連接後的字符串中用來分隔各個連接部分的字符(譯注:比如例子裏的分號“;”)。該函數定義如下:

function myConcat(separator) {
var result = ''; // 把值初始化成一個字符串,這樣就可以用來保存字符串了!!
var i;
// iterate through arguments
for (i = 1; i < arguments.length; i++) {
result += arguments[i] + separator;
}
return result;
}

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

你可以給這個函數傳遞任意數量的參數,它會將各個參數連接成一個字符串“列錶”:

// returns "red, orange, blue, "
myConcat(", ", "red", "orange", "blue");
// returns "elephant; giraffe; lion; cheetah; "
myConcat("; ", "elephant", "giraffe", "lion", "cheetah");
### 最後
總的來說,面試官要是考察思路就會從你實際做過的項目入手,考察你實際編碼能力,就會讓你在電腦敲代碼,看你用什麼編輯器、插件、編碼習慣等。所以我們在回答面試官問題時,有一個清晰的邏輯思路,清楚知道自己在和面試官說項目說技術時的話就好了
![JavaScript函數(1),前端開發工作_Web](https://s3.51cto.com/images/20210919/1632053410943637.jpg)
![JavaScript函數(1),前端開發工作_Web_02](https://s4.51cto.com/images/20210919/1632053411238526.jpg)
**[CodeChina開源項目:【大廠前端面試題解析+核心總結學習筆記+真實項目實戰+最新講解視頻】](https://ali1024.coding.net/public/P7/Web/git)**
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
版权声明:本文为[移動前端入門]所创,转载请带上原文链接,感谢。 https://gsmany.com/2021/09/20210919202050640g.html