函式是什麼
函式是完成某個特定功能的一組語句。如沒有函式,完成任務可能需要五行、十行、甚至更多的程式碼。這時我們就可以把完成特定功能的程式碼塊放到一個函式裡,直接呼叫這個函式,就省重複輸入大量程式碼的麻煩。
函式可以概括為:一次封裝,四處使用。
函式的定義
函式的定義方式通常有三種:函式宣告方式、函式表示式、 使用Function建構函式 。
函式宣告方式
語法:
function 函式名(引數1,引數2,...){
//要執行的語句
}
複製程式碼
例:
// 宣告
function sum(num1, num2) {
return num1 + num2;
}
// 呼叫
sum(1, 2) // 3
複製程式碼
函式表示式
語法:
var fn = function(引數1,引數2,...){
//要執行的語句
};
複製程式碼
例:
// 宣告
var sum = function(num1,num2){
return num1+num2;
};
// 呼叫
sum(1, 2) // 3
複製程式碼
使用Function建構函式
Function建構函式可以接收任意數量的引數,最後一個引數為函式體,其他的引數則列舉出新函式的引數。其語法為:
new Function("引數1","引數2",...,"引數n","函式體");
複製程式碼
例:
// 宣告
var sum = new Function("num1","num2","return num1+num2");
// 呼叫
sum(1, 2) // 3
複製程式碼
三種定義方式的區別
三種方式的區別,可以從作用域、效率以及載入順序來區分。
從作用域上來說,函式宣告式和函式表示式使用的是區域性變數,而 Function()
建構函式卻是全域性變數,如下所示:
var name = '我是全域性變數 name';
// 宣告式
function a () {
var name = '我是函式a中的name';
return name;
}
console.log(a()); // 列印: "我是函式a中的name"
// 表示式
var b = function() {
var name = '我是函式b中的name';
return name; // 列印: "我是函式b中的name"
}
console.log(b())
// Function建構函式
function c() {
var name = '我是函式c中的name';
return new Function('return name')
}
console.log(c()()) // 列印:"我是全域性變數 name",因為Function()返回的是全域性變數 name,而不是函式體內的區域性變數。
複製程式碼
從執行效率上來說,Function()
建構函式的效率要低於其它兩種方式,尤其是在迴圈體中,因為建構函式每執行一次都要重新編譯,並且生成新的函式物件。
來個例子:
var start = new Date().getTime()
for(var i = 0; i < 10000000; i++) {
var fn = new Function('a', 'b', 'return a + b')
fn(i, i+1)
}
var end = new Date().getTime();
console.log(`使用Function建構函式方式所需要的時間為:${(end - start)/1000}s`)
// 使用Function建構函式方式所需要的時間為:8.646s
start = new Date().getTime();
var fn = function(a, b) {
return a + b;
}
for(var i = 0; i < 10000000; i++) {
fn(i, i+1)
}
end = new Date().getTime();
console.log(`使用表示式的時間為:${(end - start)/1000}s`)
// 使用表示式的時間為:0.012s
複製程式碼
由此可見,在迴圈體中,使用表示式的執行效率比使用 Function()
建構函式快了很多很多。所以在 Web 開發中,為了加快網頁載入速度,提高使用者體驗,我們不建議選擇 Function ()
建構函式方式來定義函式。
最後是載入順序,function
方式(即函式宣告式)是在 JavaScript 編譯的時候就載入到作用域中,而其他兩種方式則是在程式碼執行的時候載入,如果在定義之前呼叫它,則會返回 undefined
:
console.log(typeof f) // function
console.log(typeof c) // undefined
console.log(typeof d) // undefined
function f () {
return 'JS 深入淺出'
}
var c = function () {
return 'JS 深入淺出'
}
console.log(typeof c) // function
var d = new Function('return "JS 深入淺出"')
console.log(typeof d) // function
複製程式碼
函式的引數和返回值
函式的引數-arguments
JavaScript 中的函式定義並未指定函式形參的型別,函式呼叫也未對傳入的實參值做任何型別檢查。實際上,JavaScript 函式呼叫甚至不檢查傳入形參的個數。
function sum(a) {
return a + 1;
}
console.log(sum(1)); // 2
console.log(sum('1')); // 11
console.log(add()); // NaN
console.log(add(1, 2)); // 2
複製程式碼
當實參比形參個數要多時,剩下的實參沒有辦法直接獲得,需要使用即將提到的arguments
物件。
JavaScript中的引數在內部用一個陣列表示。函式接收到的始終都是這個陣列,而不關心陣列中包含哪些引數。在函式體內可以通過arguments
物件來訪問這個引數陣列,從而獲取傳遞給函式的每一個引數。arguments
物件並不是Array的例項,它是一個類陣列物件,可以使用方括號語法訪問它的每一個元素。
function sum (x) {
console.log(arguments[0], arguments[1], arguments[2]); // 1 2 3
}
sum(1, 2, 3)
複製程式碼
arguments
物件的length
屬性顯示實參的個數,函式的length
屬性顯示形參的個數。
function sum(x, y) {
console.log(arguments.length); // 3
return x + 1;
}
sum(1, 2, 3)
console.log(sum.length) // 2
複製程式碼
函式的引數-arguments
JavaScript 中的函式定義並未指定函式形參的型別,函式呼叫也未對傳入的實參值做任何型別檢查。實際上,JavaScript 函式呼叫甚至不檢查傳入形參的個數。
function sum(a) {
return a + 1;
}
console.log(sum(1)); // 2
console.log(sum('1')); // 11
console.log(add()); // NaN
console.log(add(1, 2)); // 2
複製程式碼
函式的引數-同名引數
在非嚴格模式下,函式中可以出現同名形參,且只能訪問最後出現的該名稱的形參。
function sum(x, x, x) {
return x;
}
console.log(sum(1, 2, 3)) // 3
複製程式碼
而在嚴格模式下,出現同名形參會丟擲語法錯誤。
function sum(x, x, x) {
'use strict';
return x;
}
console.log(sum(1, 2, 3)) // SyntaxError: Duplicate parameter name not allowed in this context
複製程式碼
函式的引數-引數個數
當實參比函式宣告指定的形參個數要少,剩下的形參都將設定為undefined
值。
function sum(x, y) {
console.log(x, y);
}
sum(1); // 1 undefined
複製程式碼
函式的返回值
所有函式都有返回值,沒有return
語句時,預設返回內容為undefined
。
function sum1 (x, y) {
var total = x + y
}
console.log(sum1()) // undefined
function sum2 (x, y) {
return x + y
}
console.log(sum2(1, 2)) // 3
複製程式碼
如果函式呼叫時在前面加上了new
字首,且返回值不是一個物件,則返回this
(該新物件)。
function Book () {
this.bookName = 'JS 深入淺出'
}
var book = new Book();
console.log(book); // Book { bookName: 'JS 深入淺出' }
console.log(book.constructor); // [Function: Book]
複製程式碼
如果返回值是一個物件,則返回該物件。
function Book () {
return {bookName: JS 深入淺出}
}
var book = new Book();
console.log(book); // { bookName: 'JS 深入淺出' }
console.log(book.constructor); // [Function: Book]
複製程式碼
函式的呼叫方式
JS 一共有4種呼叫模式:函式呼叫、方法呼叫、構造器呼叫和間接呼叫。
函式呼叫
當一個函式並非一個物件的屬性時,那麼它就是被當做一個函式來呼叫的。對於普通的函式呼叫來說,函式的返回值就是呼叫表示式的值
function sum (x, y) {
return x + y;
}
var total = sum(1, 2);
console.log(total); // 3
複製程式碼
使用函式呼叫模式呼叫函式時,非嚴格模式下,this
被繫結到全域性物件;在嚴格模式下,this
是undefined
// 非嚴格模式
function whatIsThis1() {
console.log(this);
}
whatIsThis1(); // window
// 嚴格模式
function whatIsThis2() {
'use strict';
console.log(this);
}
whatIsThis2(); // undefined
複製程式碼
方法呼叫
當一個函式被儲存為物件的一個屬性時,稱為方法,當一個方法被呼叫時,this
被繫結到該物件。
function printValue(){
console.log(this.value);
}
var value=1;
var myObject = {value:2};
myObject.m = printValue;
//作為函式呼叫
printValue();
//作為方法呼叫
myObject.m();
複製程式碼
我們們注意到,當呼叫printValue
時,this
繫結的是全域性物件(window),列印全域性變數value
值1
。但是當呼叫myObject.m()
時,this
繫結的是方法m
所屬的物件Object,所以列印的值為Object.value
,即2
。
建構函式呼叫
如果函式或者方法呼叫之前帶有關鍵字new
,它就構成建構函式呼叫。
function fn(){
this.a = 1;
};
var obj = new fn();
console.log(obj.a);//1
複製程式碼
引數處理:一般情況構造器引數處理和函式呼叫模式一致。但如果建構函式沒用形參,JavaScript建構函式呼叫語法是允許省略實參列表和圓括號的。
如:下面兩行程式碼是等價的。
var o = new Object();
var o = new Object;
複製程式碼
函式的呼叫上下文為新建立的物件。
function Book(bookName){
this.bookName = bookName;
}
var bookName = 'JS 深入淺出';
var book = new Book('ES6 深入淺出');
console.log(bookName);// JS 深入淺出
console.log(book.bookName);// ES6 深入淺出
Book('新版JS 深入淺出');
console.log(bookName); // 新版JS 深入淺出
console.log(book.bookName);// ES6 深入淺出
複製程式碼
1.第一次呼叫Book()
函式是作為建構函式呼叫的,此時呼叫上下文this
被繫結到新建立的物件,即 book
。所以全域性變數bookName
值不變,而book
新增一個屬性bookName
,值為'ES6 深入淺出'
;
2.第二次呼叫Book()
函式是作為普通函式呼叫的,此時呼叫上下為this
被繫結到全域性物件,在瀏覽器中為window
。所以全域性物件的bookNam
值改變為' 新版JS 深入淺出'
,而book
的屬性值不變。
間接呼叫
JS 中函式也是物件,函式物件也可以包含方法,call()
和apply()
方法可以用來間接地呼叫函式。
這兩個方法都允許顯式指定呼叫所需的this
值,也就是說,任何函式可以作為任何物件的方法來呼叫,哪怕這個函式不是那個物件的方法。兩個方法都可以指定呼叫的實參。call()
方法使用它自有的實參列表作為函式的實參,apply()
方法則要求以陣列的形式傳入引數。
var obj = {};
function sum(x,y){
return x+y;
}
console.log(sum.call(obj,1,2));//3
console.log(sum.apply(obj,[1,2]));//3
複製程式碼
詞法(靜態)作用域與動態作用域
作用域
通常來說,一段程式程式碼中所用到的名字並不總是有效/可用的,而限定這個名字的可用性的程式碼範圍就是這個名字的作用域。
詞法作用域
詞法作用域,也叫靜態作用域,它的作用域是指在詞法分析階段就確定了,不會改變。而與詞法作用域相對的是動態作用域,函式的作用域是在函式呼叫的時候才決定的。
來個例子,如下程式碼所示:
var blobal1 = 1;
function fn1 (param1) {
var local1 = 'local1';
var local2 = 'local2';
function fn2(param2) {
var local2 = 'inner local2';
console.log(local1)
console.log(local2)
}
function fn3() {
var local2 = 'fn3 local2';
fn2(local2)
}
fn3()
}
fn1()
複製程式碼
當瀏覽器看到這樣的程式碼,不會馬上去執行,它會先生成一個抽象語法樹。上述程式碼生成的抽象語法樹大概是這樣的:
執行fn1
函式,fn1
中呼叫 fn3()
,從fn3
函式內部查詢是否有區域性變數 local1
,如果沒有,就根據抽象樹,查詢上面一層的程式碼,也就是 local1
等於 'local1'
,所以結果會列印 'local1'
。
同樣的方法查詢是否有區域性變數 local2
,發現當前作用域內有local2
變數,所以結果會列印 'inner local2
。
思考
有如下的程式碼:
var a = 1;
function fn() {
console.log(a)
}
複製程式碼
兩個問題:
- 函式
fn
裡面的變數a
, 是不是外面的變數a
。 - 函式
fn
裡面的變數a
的值, 是不是外面的變數a
的值。
對於第一個問題:
分析一個語法,就能確定函式 fn
裡面的 a
就是外面的 a
。
對於第二個問題:
函式 fn
裡面的變數 a
的值, 不一定是外面的變數 a
的值,假設我們們這樣做:
var a = 1;
function fn() {
console.log(a)
}
a = 2
fn()
複製程式碼
這時候當我們們執行 fn()
的時候,列印 a
的值為 2
。所以如果沒有看到最後,一開始我們們是不知道列印的 a
值到底是什麼。
所以詞法作用域只能確定變數所在位置,並不能確定變數的值。
呼叫棧(Call Stack)
什麼是執行上下文
執行上下文就是當前JavaScript程式碼被解析和執行是所在環境的抽象概念,JavaScript中執行任何的程式碼都是在執行上下文中執行。
執行上下文的型別,主要有兩類:
-
全域性執行上下文:這是預設的,最基礎的執行上下文。不在任何函式中的程式碼都位於全域性執行上下文中。共有兩個過程:1.建立有全域性物件,在瀏覽器中這個全域性物件就是
window
物件。2.將this
指標指向這個全域性物件。一個程式中只能存在一個執行上下文。 -
函式執行上下文:每次呼叫函式時,都會為該函式建立一個新的執行上下文。每個函式都擁有自己的執行上下文,但是隻有在函式被呼叫的時候才會被建立。一個程式中可以存在多個函式執行上下文,這些函式執行上下文按照特定的順序執行一系列步驟,後文具體討論。
呼叫棧
呼叫棧,具有LIFO
(Last in, First out 後進先出)結構,用於儲存在程式碼執行期間建立的所有執行上下文。
當JavaScript引擎首次讀取指令碼時,會建立一個全域性執行上下文並將其push
到當前執行棧中。每當發生函式呼叫時,引擎都會為該函式建立一個新的執行上下文並push
到當前執行棧的棧頂。
引擎會執行執行上下文在執行棧棧頂的函式,根據LIFO
規則,當此函式執行完成後,其對應的執行上下文將會從執行棧中pop
出,上下文控制權將轉到當前執行棧的下一個執行上下文。
看看下面的程式碼:
var myOtherVar = 10;
function a() {
console.log('myVar', myVar);
b();
}
function b() {
console.log('myOtherVar', myOtherVar);
c();
}
function c() {
console.log('Hello world!');
}
a();
var myVar = 5;
複製程式碼
有幾個點需要注意:
- 變數宣告的位置(一個在上,一個在下)
- 函式
a
呼叫下面定義的函式b
, 函式b
呼叫函式c
當它被執行時你期望發生什麼? 是否發生錯誤,因為b
在a
之後宣告或者一切正常? console.log
列印的變數又是怎麼樣?
以下是列印結果:
"myVar" undefined
"myOtherVar" 10
"Hello world!"
複製程式碼
1. 變數和函式宣告(建立階段)
第一步是在記憶體中為所有變數和函式分配空間。 但請注意,除了undefined
之外,尚未為變數分配值。 因此,myVar
在被列印時的值是undefined
,因為JS引擎從頂部開始逐行執行程式碼。
函式與變數不一樣,函式可以一次宣告和初始化,這意味著它們可以在任何地方被呼叫。
所以以上程式碼在建立階段時,看起來像這樣子:
var myOtherVar = undefined
var myVar = undefined
function a() {...}
function b() {...}
function c() {...}
複製程式碼
這些都存在於JS建立的全域性上下文中,因為它位於全域性作用域中。
在全域性上下文中,JS還新增了:
- 全域性物件(瀏覽器中是
window
物件,NodeJs 中是global
物件) this
指向全域性物件
2. 執行
接下來,JS 引擎會逐行執行程式碼。
myOtherVar = 10
在全域性上下文中,myOtherVar
被賦值為10
已經建立了所有函式,下一步是執行函式 a()
每次呼叫函式時,都會為該函式建立一個新的上下文(重複步驟1),並將其放入呼叫堆疊。
function a() {
console.log('myVar', myVar)
b()
}
複製程式碼
如下步驟:
- 建立新的函式上下文
a
函式裡面沒有宣告變數和函式- 函式內部建立了
this
並指向全域性物件(window) - 接著引用了外部變數
myVar
,myVar
屬於全域性作用域的。 - 接著呼叫
函式 b
,函式b
的過程跟a
一樣,這裡不做分析。
下面呼叫堆疊的執行示意圖:
- 建立全域性上下文,全域性變數和函式。
- 每個函式的呼叫,會建立一個上下文,外部環境的引用及 this。
- 函式執行結束後會從堆疊中彈出,並且它的執行上下文被垃圾收集回收(閉包除外)。
- 當呼叫堆疊為空時,它將從事件佇列中獲取事件。
程式碼部署後可能存在的BUG沒法實時知道,事後為了解決這些BUG,花了大量的時間進行log 除錯,這邊順便給大家推薦一個好用的BUG監控工具Fundebug。
交流(歡迎加入群,群工作日都會發紅包,互動討論技術)
乾貨系列文章彙總如下,覺得不錯點個Star,歡迎 加群 互相學習。
因為篇幅的限制,今天的分享只到這裡。如果大家想了解更多的內容的話,可以去掃一掃每篇文章最下面的二維碼,然後關注我們們的微信公眾號,瞭解更多的資訊和有價值的內容。
每次整理文章,一般都到2點才睡覺,一週4次左右,挺苦的,還望支援,給點鼓勵