【JS 口袋書】第 3 章:JavaScript 函數

阿里雲最近在作活動,低至2折,有興趣能夠看看:html

promotion.aliyun.com/ntms/yunpar…git


函數是什麼

函數是完成某個特定功能的一組語句。如沒有函數,完成任務可能須要五行、十行、甚至更多的代碼。這時咱們就能夠把完成特定功能的代碼塊放到一個函數裏,直接調用這個函數,就省重複輸入大量代碼的麻煩。github

函數能夠歸納爲:一次封裝,四處使用。數組

函數的定義

函數的定義方式一般有三種:函數聲明方式、函數表達式、 使用Function構造函數 。瀏覽器

函數聲明方式bash

語法:微信

function 函數名(參數1,參數2,...){  
    //要執行的語句  
}  
複製代碼

例:閉包

// 聲明
function sum(num1, num2) {
  return num1 + num2;
}

// 調用
sum(1, 2)  // 3
複製代碼

函數表達式app

語法:函數

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被綁定到全局對象;在嚴格模式下,thisundefined

// 非嚴格模式
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),打印全局變量value1。可是當調用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)
}
複製代碼

兩個問題:

  1. 函數 fn 裏面的變量 a, 是否是外面的變量 a
  2. 函數 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

當它被執行時你指望發生什麼? 是否發生錯誤,由於ba以後聲明或者一切正常? 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)
  • 接着引用了外部變量 myVarmyVar 屬於全局做用域的。
  • 接着調用函數 b函數b的過程跟a同樣,這裏不作分析。

下面調用堆棧的執行示意圖:

  • 建立全局上下文,全局變量和函數。
  • 每一個函數的調用,會建立一個上下文,外部環境的引用及 this。
  • 函數執行結束後會從堆棧中彈出,而且它的執行上下文被垃圾收集回收(閉包除外)。
  • 當調用堆棧爲空時,它將從事件隊列中獲取事件。

代碼部署後可能存在的BUG無法實時知道,過後爲了解決這些BUG,花了大量的時間進行log 調試,這邊順便給你們推薦一個好用的BUG監控工具Fundebug

交流(歡迎加入羣,羣工做日都會發紅包,互動討論技術)

阿里雲最近在作活動,低至2折,有興趣能夠看看:promotion.aliyun.com/ntms/yunpar…

乾貨系列文章彙總以下,以爲不錯點個Star,歡迎 加羣 互相學習。

github.com/qq449245884…

由於篇幅的限制,今天的分享只到這裏。若是你們想了解更多的內容的話,能夠去掃一掃每篇文章最下面的二維碼,而後關注我們的微信公衆號,瞭解更多的資訊和有價值的內容。

clipboard.png

每次整理文章,通常都到2點才睡覺,一週4次左右,挺苦的,還望支持,給點鼓勵

相關文章
相關標籤/搜索