讀《JavaScript核心技術開發解密》筆記

【前言】《JavaScript核心技術開發解密》這本書真的寫的很是好,特別適合深刻理解JavaScript的運行機制,我的推薦閱讀。如下是我在閱讀該書時根據書中章節作的筆記,目的是方便本身加深理解。若是有理解錯誤的地方,還請指出來。前端

【筆記內容】node

1、數據結構

在JS語言核心中,咱們必須瞭解三種數據結構:棧(stack)、堆(heap)、隊列(queue)。jquery

棧是一種先進後出,後進先出(LIFO)的數據結構,相似於只有一個出入口的羽毛球盒,在JS中,棧能夠用來規定代碼的執行順序,例如函數調用棧(call stack)webpack

JS的數組是提供了兩個棧操做的方法,es6

push向數組的末尾添加元素(進棧)web

pop取出數組最末尾的一個元素(出棧)面試

堆是一種樹形的結構,JS中的對象表示能夠當作是一種堆的結構,如:ajax

var root = {
    a: 10,
    b: 20,
    c: {
        d: 30,
        e: 40
    }
}
複製代碼

能夠畫出堆的圖示如:正則表達式

image-20190520130628236

那麼對應的,JS的對象通常是保存在堆內存中。算法

隊列

隊列是一種**先進先出(FIFO)**的數據結構,相似於排隊過安檢同樣,理解隊列對理解JS的事件循環頗有幫助

2、數據類型和內存空間

基礎數據類型

最新的ECMAScript標準號定義了7種數據類型,其中包括6種基本數據類型和一種引用類型

其中,基本數據類型是:Number、String、Boolean、Null、Undefined、Symbol (在ES5中沒有Symbol類型),

一種引用數據類型是Object

目前瀏覽器對Symbol類型的兼容不行,所以建議在實際開發中不使用Symbol

咱們在書寫函數的時候,聲明變量通常是這樣的:

function foo(){
    var num1 = 28;
    var num2 = 39;
    ...
}
複製代碼

那,在運行函數foo的時候,它的變量保存在哪裏?從JS的內存管理上來看,函數運行時,會建立一個執行環境,這個執行環境叫作執行上下文(EC),在執行上下文中,會建立一個變量對象(VO),即函數內聲明的基礎數據類型保存在該執行上下文的變量對象中

變量對象是保存在堆內存中的,可是因爲變量對象具備特殊功能,因此在理解時,咱們將變量對象與堆內存空間區分開來

引用數據類型

引用數據類型除了Object,數組對象、正則表達式、函數等也屬於引用數據類型。其中,引用數據類型的值是保存在堆內存空間中的對象。如:

var o = {
    a: 10,
   	b: { m: 20}
}
複製代碼

對於如上代碼,o屬於引用數據類型,等號右邊的內容屬於其值,那麼{a:10,b:{m:20}}存在堆內存空間,o存在對應的執行上下文的變量對象中,這裏的執行上下文爲全局。

咱們根據一個例子和圖示理解下:

function foo(){
    var num = 28;
    var str = 'hello';
    var obj = null;
    var b = { m: 20 };
    var c = [1,2,3];
    ...
}
複製代碼

如圖,當咱們想要訪問對象b的內容時,其實是經過一個引用(地址指針)來訪問的:

image-20190520140256862

咱們再來思考兩個問題:

var a = 20;
var b = a;
b = 30;
console.log(a);  // 此時輸出多少?
var m = {x:10, y:20};
var n = m;
n.y = 30;
console.log(m.y); // 此時輸出多少?
複製代碼

輸出的結果是20 30,若是可以理解這兩個輸出,那麼相信你對於引用和JS的內存管理是理解了。

內存空間管理

JS有自動垃圾回收機制,當一塊內存空間的數據可以被訪問時,垃圾回收器就認爲該數據暫時未使用完不算垃圾,碰到不須要再使用的內存空間數據時,會將其標記爲垃圾,並釋放該內存空間,也就是標記-清除算法。這個算法會從全局對象開始查找,相似從樹的根節點往下找,進行標記清除。

因此,通常當一個函數執行完以後,其內部的變量對象就會被回收。可是若是是全局變量的話,變量何時釋放對於回收器來講是比較難判斷的,咱們爲了性能,應該儘可能避免過多的使用全局變量或者在不使用該全局變量時手動設置變量值爲null這種方式釋放。

3、執行上下文

前面說到,JS在執行一個函數時會建立一個執行上下文。其實,在全局環境下也會有執行上下文,能夠籠統的將執行上下文分爲局部執行上下文和全局執行上下文。

一個JS程序中只能有一個全局環境,可是能夠有不少局部環境,因此可見,在一個JS程序中,一定出現多個執行上下文。JS引擎以函數調用棧的方式來處理執行上下文,其中棧底永遠都是全局上下文,棧頂則是當前正在執行的上下文,棧頂的執行上下文執行完畢後,會自動出棧

咱們經過一個實例來理解:

function a(){
    var hello = "Hello";
    var world = "world";
    function b(){
        console.log(hello);
    }
    function c(){
        console.log(world);
    }
    b();
    c();
}

a();
複製代碼

第一步,全局上下文入棧,並置於棧底:

image-20190520142921427

第二步,全局上下文入棧後,開始執行全局上下文內部代碼,直到遇到a(),a()激活了函數a,從而建立了a的執行上下文,因而a的執行上下文入棧,如圖:

image-20190520143147280

第三步,a的執行上下文執行內容,碰到了b()激活了b函數,因此b的執行上下文入棧:

image-20190520143403615

第四步,在b的執行上下文裏面,沒有能夠生成其餘執行上下文的狀況,因此這段代碼能夠順利執行完畢,b的執行上下文出棧。

image-20190520143147280

第五步,b的執行上下文出棧以後,急需執行a的後面內容,碰到了c()激活了c函數,因此c的執行上下文入棧,如圖所示:

image-20190520143859026

第六步,在c的執行上下文中,沒有其餘的生成執行上下文內容,因此當c裏面的執行代碼結束後,c的執行上下文出棧:

image-20190520144111387

第七步,a接下來的代碼也執行完畢,因此接着a的執行上下文出棧

image-20190520142921427

最後,全局上下文在瀏覽器窗口關閉(或Node程序終止)的時候出棧。

注意:函數執行中,若是碰到return會直接終止可執行代碼的執行,所以會直接將當前上下文彈出棧。

總的執行順序如圖:

image-20190520145341482

思考下面的程序從執行上下文來看分爲幾步?

function a(){
    var hello = "hello";
    function b(){
        console.log(b);
    }
    return b;
}
var result = a();
result();
複製代碼

圖示以下:

image-20190520145859647

4、變量對象

前面咱們提到過變量對象,在JS代碼中聲明的全部變量都保存在變量對象中,其中變量對象包含以下內容:

  1. 函數的全部參數
  2. 當前上下文的全部函數聲明(經過function聲明的函數)
  3. 當前上下文的全部變量聲明(經過var聲明的變量)

建立過程

  1. 在Chrome瀏覽器(Node)中,變量對象會首先獲取函數的參數變量及值;在Firefox瀏覽器中,直接將參數對象arguments保存到變量對象中;
  2. 先依次獲取當前上下文全部的函數聲明,也就是function關鍵字聲明的函數。函數名做爲變量對象的屬性,其屬性值爲指向該函數所在的內存地址引用。若是函數名已存在,那麼屬性值會被新的引用覆蓋
  3. 依次獲取當前上下文全部的變量聲明,也就是var關鍵字聲明的變量。每找到一個變量就在變量對象中建議一個屬性,屬性值爲undefined。若是該變量名已存在,爲防止同名函數被修改成undefined,則會直接跳過該變量,原屬性值不修改

咱們根據上面的過程,思考下面這一句代碼執行的過程:

var a = 30;
複製代碼

過程以下:

第一步,上下文的建立階段會先確認變量對象,而變量對象的建立過程對於變量聲明來講是先獲取變量名並賦值爲undefined,因此第一步拆解爲:

var a = undefined;

複製代碼

上下文建立階段結束後,進入執行階段,在執行階段完成變量賦值的工做,因此第二步是:

a = 30;

複製代碼

須要注意的是,這兩步分別是在上下文的建立階段和執行階段完成的,所以var a=undefined是提早到比較早的地方去執行了,也便是變量提高(Hoisting)。因此,咱們如今要有意識,就是JS程序的執行是分爲上下文建立階段和執行階段的

思考以下代碼的執行順序:

console.log(a);  // 輸出什麼?
var a = 30;

複製代碼

在變量對象的建立過程當中,函數聲明的優先級高於變量聲明,並且同名的函數會覆蓋函數與變量,可是同名的變量並不會覆蓋函數。不過在上下文的執行階段,同名的函數會被變量從新賦值。

以下代碼中:

var a = 20;
function fn(){ console.log('函數1') };
function fn(){ console.log('函數2') };
function a(){ console.log('函數a') };


fn();
var fn = '我是變量可是我要覆蓋函數';
console.log(fn);
console.log(a);

// 輸出:
// 函數2
// 我是變量可是我要覆蓋函數
// 20

複製代碼

上面例子執行的順序能夠當作:

/** 建立階段 **/
// 函數變量先提高
function fn(){ console.log('函數1') };
function fn(){ console.log('函數2') };
function a(){ console.log('函數a') };
// 普通變量接着提高
var a = undefined; 
var fn = undefined;  // 建立階段即便同名,可是變量的值不會覆蓋函數值

/** 執行階段 **/
a = 20;
fn();
fn = '我是變量可是我要覆蓋函數';
console.log(fn);
console.log(a);

複製代碼

實例分析

function foo(){
    console.log(a);
    console.log(fn());
    
    var a = 1;
    function fn(){
        return 2;
    }
}
foo();

複製代碼

運行foo函數時,對應的上下文建立,咱們使用以下形式來表達這個過程:

/** 建立過程 **/
fooEC(foo的執行上下文) = {
    VO: {},		// 變量對象
    scopeChain: [],		// 做用域鏈
    this: {}	
}

// 這裏暫時不討論做用域與this對象

// 其中,VO含以下內容
VO = {
    arguments: {...},
    fn: <fn reference>, a: undefined } 複製代碼

建立過程當中會建立變量對象,因此如上形式所示。在函數調用棧中,若是當前上下文在棧頂,那就開始執行,此時變量對象稱爲活動對象(AO,Activation Object):

/** 執行階段 **/
VO -> AO
AO = {
    arguments: {},
    fn: <fn reference>, a: 1 } 複製代碼

因此,這段代碼的執行順序應該爲:

function foo(){
    function fn(){
    	return 2;
    }
    var a = undefined;
    console.log(a);
    console.log(fn());
    a = 1;
}
foo();

複製代碼

全局上下文的變量對象

以瀏覽器爲例,全局變量對象爲window對象。而在node中,全局變量對象是global。

windowEC = {
    VO: window,
    this: window,
    scopeChain: []
}

複製代碼

5、做用域與做用域鏈

在其餘的語言中,咱們確定也據說過做用域這個詞,做用域是用來規定變量與函數可訪問範圍的一套規則

種類

在JS中,做用域分爲全局做用域與函數做用域。

全局做用域

全局做用域中聲明的變量與函數能夠在代碼的任何地方被訪問。

如何建立全局做用域下的變量:

  1. 全局對象擁有的屬性與方法

    window.alert
    window.console
    
    複製代碼
  2. 在最外層聲明的變量與方法

    var a = 1;
    function foo(){...}
    
    複製代碼
  3. 非嚴格模式下,不使用關鍵字聲明的變量和方法【不要使用!!】

    function foo(){
        a = 1;    // a會成爲全局變量
    }
    
    複製代碼

函數做用域

函數做用域中聲明的變量與方法,只能被下層子做用域訪問,而不能被其餘不相干的做用域訪問。

例如:

function foo(){
    var a = 1;
    var b = 2;
}
foo();
function sum(){
    return a+b;
}
sum(); // 執行報錯,由於sum沒法訪問到foo做用域下的a和b

複製代碼

可是像下面這樣寫是對的:

function foo(){
    var a = 1;
    var b = 2;
    function sum(){
        return a+b;
    }
    sum();	// 能夠訪問,由於sum的做用域是foo做用域的子做用域
}
foo();

複製代碼

在ES6以前,ECMAScript沒有塊級做用域,所以使用時須要特別注意,必定是在函數環境中才能夠生成新的做用域。而ES6以後,咱們能夠經過用let來聲明變量或方法,這樣它們就能在"{"和"}"之間造成塊級做用域

模擬塊級做用域

咱們能夠經過函數來模擬塊級做用域,以下:

var arr = [1,2,3,4];

(function(){
    for (var i=0; i< arr.length; i++ ){
        console.log(i);
    }
})();

console.log(i); // 輸出undefined,由於i在函數做用域裏

複製代碼

這種函數叫作當即執行函數

寫法大體有以下幾種,建議第一種寫法:

(function(){
    ...
})();

!function(){
    ...
}();
    
// 把!改爲+或-也能夠

複製代碼

在ECMAScript開發中,咱們可能會常用當即執行函數方式來實現模塊化。

做用域鏈

function a(){
    ...
    function b(){
        ...
    }
}
a();

複製代碼

如上僞代碼中,前後建立了全局函數a和函數b的執行上下文,假設加上全局上下文,它們的變量對象是VO(global),VO(a)和VO(b),那麼b的做用域鏈則同時包含了這三個變量對象,以下:

bEC = {
    VO: {...},
    scopeChain: [VO(b), VO(a), VO(global)],  //做用域
    this: {}
}

複製代碼

做用域鏈是在代碼執行階段建立的,理解做用域鏈是學習閉包的前提,閉包裏面會有更多的對做用域鏈的應用

6、閉包

什麼是閉包

簡單來說的話,閉包就是指有權訪問另外一個函數做用域中變量的函數,其本質是在函數外部保持了內部變量的引用,。

建立一個閉包最多見的方式,就是在一個函數內部建立另外一個函數,這個函數就是閉包,以下:

function foo(){
    var a = 1;
    var b = 2;
    
    function closure(){
        return a + b;
    }
    
    return closure;
}

複製代碼

上面的代碼中,closure函數就是一個閉包(在chrome的調試裏面,用閉包的父做用域函數名錶示),閉包的做用域爲[VO(closure), VO(foo), VO(global)]

根據上面的理解,咱們來看一個例子,裏面有閉包嗎:

var name = 'window';
var obj = {
    name: 'my object',
    getName: function(){
        return function(){
            return this.name
        }
    }
}
console.log( obj.getName()() )  // 輸出: window

複製代碼

在這個例子中,雖然在getName函數裏面,用了一個內部函數,可是咱們發現最終返回的this.name輸出window,能夠看出這不是一個閉包。由於其返回的是個匿名函數,而匿名函數的執行上下文是全局上下文,所以其this對象一般指向全局對象,因此this.name輸出了window。那咱們怎麼修改可讓其返回obj的name屬性呢?

以下:

var name = 'window';
var obj = {
    name: 'my object',
    getName: function(){
        var _this = this;
        return function(){
            return _this.name
        }
    }
}
console.log( obj.getName()() )  // 輸出: my object

複製代碼

總結下,就是閉包的做用域鏈必須是包含了他的父級函數做用域,使用了父級做用域的變量對象下的變量確定就包含了父級做用域

閉包和垃圾回收機制

咱們來回顧下垃圾回收機制:當一個值再也不被引用時會被標記而後清除,當一個函數的執行上下文運行完畢後,內部全部的內容都會失去引用而被清除。

閉包的本質是保持在外部對函數的引用,因此閉包會阻止垃圾回收機制進行回收。

例如一下代碼:

function foo(){
    var n = 1;
    nAdd = function(){
        n += 1;
    }
    return function fn(){
        console.log(n);
    }
}
var result = foo();
result();      // 1
nAdd();
result();      // 2

複製代碼

由於nAdd和fn函數都訪問了foo的n變量,因此它們都與foo造成了閉包。這個時候變量n的引用被保存了下來。

因此,在使用閉包時應該警戒,濫用閉包,極可能會由於內存緣由致使程序性能過差

閉包的應用場景

回顧下,使用閉包後,任何在函數中定義的變量,均可以認爲是私有變量,由於不能在函數外部訪問這些變量。私有變量包括函數的參數、局部變量和函數定義的其餘函數。

循環、setTimeout與閉包

咱們先來看一個面試常見的例子:

for( var i=0; i<5; i++ ){
    setTimeout(function timer(){
        console.log(i);
    }, i*1000);
}

複製代碼

可能乍一看會以爲每隔1秒從0輸出到4,可是實際的運行是每隔1秒輸出一個5。

咱們來分析一下:

  1. for循環不能造成本身的做用域,因此i是全局變量,會隨着循環遞增,循環結束後爲5
  2. 在每一次循環中,setTimeout的第二個參數訪問的都是當前的i,所以第二個參數中i分別爲0,1,2,3,4
  3. 第一個參數timer訪問的是timer函數執行時的i,因爲延遲緣由,當timer開始執行時,此時i已經爲5了

若是咱們要隔秒輸出0,1,2,3,4,那就須要讓for循環造成本身的做用域,因此須要藉助閉包的特性,將每個i值用一個閉包保存起來。以下代碼:

for( var i=0; i<5; i++ ){
    (function(i){
        setTimeout(function timer(){
            console.log(i);
        }, i*1000);
    })(i);
}

複製代碼

固然,在ES6或更高版本中,能夠直接使用let關鍵字造成for的塊級做用域,這樣也是OK的:

for( let i=0; i<5; i++ ){
    setTimeout(function timer(){
        console.log(i);
    }, i*1000);
}

複製代碼

單例模式與閉包

JavaScript也有許多解決特定問題的編碼思惟(設計模式),例如咱們常聽到過的工廠模式、訂閱通知模式、裝飾模式、單例模式等。其中,單例模式是最經常使用也是最簡單的一種,咱們嘗試用閉包來實現它。

其實在JS中,對象字面量就是一個單例對象。以下:

var student = {
    name: 'zeus',
    age: 18,
    getName: function(){
        return this.name;
    },
    getAge: function(){
        return this.age;
    }
}
student.getName();
student.name;

複製代碼

可是,這種對象的變量很容易被外部修改,不符合咱們的需求,咱們指望創建本身的私有屬性和方法。以下:

var student = (function(){
    var name = 'zeus';
    var age = 18;
    
    return {  // 外部可訪問內容
        getName: function(){
            return name;
        },
        getAge: function(){
            return age;
        }
    }
})();
student.getName();
student.name;  // undefined

複製代碼

如上,第二個例子中,在當即函數執行的時候就返回student對象了,下面咱們寫一個例子,在調用時才初始化:

var student = (function(){
    var name = 'zeus';
    var age = 18;

    var instance = null; // 定義一個變量,用來保存實例
    
    function init(){
        return {
            getName: function(){
                return name;
            },
            getAge: function(){
                return age;
            }
        }
    }

    return {
        getInstance: function(){
            if ( !instance ){
                instance = init();
            }
            return instance;
        }
    }

    
})();
var student1 = student.getInstance();
var student2 = student.getInstance();
console.log( student1 === student2 );  // true

複製代碼

模塊化與閉包

提出一個問題:若是想在全部的地方都能訪問同一個變量,應該怎麼作?例如全局的動態管理。

解決方案:使用全局變量(可是時間開發中,不建議輕易使用全局變量)。

其實,模塊化的思想能夠幫助咱們解決這個問題。

模塊化開發是目前最流行,也是必需要掌握的一種開發思路。而模塊化實際上是創建在單例模式上的,所以模塊化開發和閉包息息相關。目前好比Node裏的require,ES6的import和modules等,實現方式不一樣可是核心思路是同樣的

模塊化架構通常須要實現下面三個內容:

1.每個單例就是一個模塊,在當前的一些模塊化開發中,每個文件是一個模塊

2.每一個模塊必須有獲取其餘模塊的能力

如在一些模塊化開發中,使用require或者import來獲取其餘模塊內容

3.每個模塊都應該有對外的接口,以保證與其餘模塊交互的能力

在一些模塊化開發中使用module.exports或者export default {}等將容許其餘模塊使用的接口暴露出來

咱們今天使用單例模式,來實現簡單的模塊化思想,案例實現的是每隔一秒,body的背景色就隨着一個數字的遞增在固定的三個顏色之間切換:

/** * 管理全局狀態模塊,含有私有變量並暴露兩個方法來獲取和設置其內部私有變量 */
var module_status = (function(){
    var status = {
        number: 0,
        color: null
    }

    var get = function(prop){
        return status[prop];
    }

    var set = function(prop, value){
        status[prop] = value;
    }

    return {
        get: get,
        set: set
    }
})();
/** * 負責body背景顏色改變的模塊 */
var module_color = (function(){
    // 僞裝用這種方式執行第二步引用模塊
    var state = module_status;

    var colors = ['#c31a86', 'orange', '#ccc'];

    function render(){
        var color = colors[ state.get('number') % 3];
        document.body.style.backgroundColor = color;
    }

    return {
        render: render
    }

})();
/** * 負責顯示當前number值模塊,用於參考對比 */
var module_context = (function(){
    var state = module_status;

    function render(){
        document.body.innerHTML = 'this Number is '+state.get('number');
    }

    return {
        render: render
    }

})();
/** * 主模塊,藉助上面的功能模塊實現咱們須要的功能 */
var module_main = (function(){
    var state = module_status;
    var color = module_color;
    var context = module_context;

    setInterval(function(){
        var newNumber = state.get('number') + 1;
        state.set('number', newNumber);

        color.render();
        context.render();
    }, 1000);
})();

複製代碼

本身分析整個完整的代碼以後,真的頗有幫助

7、this對象

上面六大節的內容,能夠算是JavaScript的進階,但其實應該算是JavaScript的基礎,具有這些知識的時候再來看this對象這一節,收穫會很大。在JS中,最重要的部分就是理解閉包和this!

咱們來回顧下執行上下文和變量對象那一節,咱們知道在函數被調用執行時,變量對象VO會生成,這個時候,this的指向會肯定。所以,必須牢記當前函數的this是在函數被調用執行的時候才肯定的,也就是說this對象須要當前執行上下文在函數調用棧的棧頂時,VO變成AO,同時this的指向肯定。

以下代碼:

var a = 10;
var obj = {
    a: 20
}
function fn(){
    console.log(this.a);
}
fn();  // 10
fn.call(obj); // 20

複製代碼

代碼裏面,fn函數裏的this分別指向了window(全局對象變量)與obj

全局對象的this

全局對象的變量對象是一個比較特殊的存在,在全局對象中,this指向它自己,因此比較簡單

this.a1 = 10;
var a2 = 20;
a3 = 30;

console.log(a1); //10
console.log(a2); //20
console.log(a3); //30

複製代碼

以上的用法都會構建全局變量,且在非嚴格模式語法上沒有錯誤。

函數中的this

在本節第一個例子中,咱們看到,同一個函數的this因爲調用方式不一樣致使this的指向不一樣,所以,在函數中,this最終指向誰,與調用該函數的方式有關。

在一個函數的執行上下文中,this由該函數的調用者提供,由調用函數的方式來決定其指向

以下例子:

function fn(){
    console.log(this);
}
fn();	// fn爲調用者,獨立調用,非函數的嚴格模式下指向全局對象window

複製代碼

若是調用者被某個對象擁有,那麼在調用該函數時,函數內部的this指向該對象。若是調用者函數獨立調用,那麼該函數內部this指向undefined,可是在非嚴格模式下,當this指向undefined時,它會指向全局對象。

function fn(){
 'use strict';
    console.log(this);
}
fn();   // undefined
window.fn();  // window

複製代碼

思考一下,以下這個例子返回什麼:

var a = 20;
var obj = {
    a: 30
}
function fn(){
    console.log('fn this:', this);
    function foo(){
        console.log(this.a);
    }
    foo();
}
fn.call(obj);
fn();

複製代碼

另外,對象字面量不會產生做用域,因此以下

'use strict';
var obj = {
 a: 1,
 b: this.a+1
}

複製代碼

嚴格模式下會報語法錯誤,非嚴格模式下this指向全局對象

思考下面的例子:

var a = 10;
var foo = {
 a: 20,
 getA: function(){
     return this.a;
 }
}

console.log( foo.getA() );  // 20

var test = foo.getA();
console.log( test() );	// 10,這裏爲何是10?

複製代碼

由於test在執行時,test是調用者,它是獨立調用,在非嚴格模式下,其this指向全局對象

思考以下代碼輸出什麼:

function foo(){
    console.log(this.a);
}

function active(fn){
    fn();
}

var a = 20;
var obj = {
    a: 10,
    getA: foo,
    active: active
}

active(obj.getA);
obj.active(obj.getA);

複製代碼

call/apply/bind顯式的指定this

JS內部提供了一種能夠手動設置函數內部this指向的方式,就是call/apply/bind。全部的函數均可以調用這三個方法。

看以下例子:

var a = 20;
var obj = {
    a: 30
}
function foo(num1,num2){
    console.log(this.a+num1+num2);
}

複製代碼

咱們知道,直接調用foo(10,10)的話會打印40,若是咱們想把obj裏的a打印出來,咱們像下面這樣寫:

foo.call(obj,10,10);	// 50
// 或
foo.apply(obj, [10,10]);  // 50

複製代碼

那其實call/apply表示將第一個參數做爲該函數執行的this對象指向,而後當即執行函數。

call和apply有一點區別,就是傳參的區別:

在call中,第一個參數是函數內部this的指向,後續參數則是函數執行時所需參數;

在apply中,只有兩個參數,第一個參數是函數內部this的指向,第二個參數是一個數組,數組裏面是函數執行所需參數。

bind方法用法與call方法同樣,與call惟一不一樣的是,bind不會當即執行函數,而是直接返回一個新的函數,而且新的函數內部this指向bind方法的第一個參數

8、函數與函數式編程

其實,咱們仔細回顧下會發現,前面的一到七節的內容基本上都是在圍繞函數展開的,讓咱們更加清晰的認識函數,這一節主要了解如何運用函數

函數

函數的形式有四種:函數聲明、函數表達式、匿名函數與當即執行函數。

1.函數聲明

關鍵字function,從前面的執行上下文建立過程咱們知道function關鍵字聲明的變量比var關鍵字聲明的變量有更高的優先執行順序,因此變量提高中,先提高函數變量。

function fn(){ ... }

複製代碼

2.函數表達式

指將一個函數體賦值給一個變量的過程

var fn = function(){ ... }

複製代碼

能夠理解爲:

// 建立階段
var fn = undefined;
// 執行階段
fn = function(){ ... }

複製代碼

因此使用函數表達式時,必需要考慮函數使用的前後順序:

fn();  // TypeError: fn is not a function

var fn = function(){ console.log('hello') }

複製代碼

請問,若是在函數表達式裏面有this,那這個this指向什麼?

3.匿名函數

就是指沒有名字的函數,通常會做爲參數或返回值來使用,一般不使用變量來保存它的引用。

匿名函數不必定就是閉包,匿名函數能夠做爲普通函數來理解,而閉包的造成條件,僅僅是有的時候或者匿名函數有關而已

4.當即執行函數

當即執行函數是匿名函數一個很是重要的應用場景,由於函數能夠產生做用域,因此咱們常用當即執行函數來模擬塊級做用域,並進一步在此基礎上實現模塊化的運用。

函數式編程

函數式編程其實就是將一些功能、邏輯等封裝起來以便使用,減小重複編碼量。函數式編程的內涵就是函數封裝思想。怎麼去封裝,學習前輩優秀的封裝習慣。讓本身的代碼看上去更加專業可靠是咱們學習的目的。

1.函數是一等公民

一等公民也就是說函數跟其餘的變量同樣,沒有什麼特殊的,咱們能夠像對待任何數據類型同樣對待函數。

  • 把函數賦值給一個變量

    var fn = function(){}
    
    複製代碼
  • 把函數做爲形參

    function foo(a, b, callback){
        callback(a+b);
    }
    function fn(res){
        console.log(res);
    }
    foo(2,3,fn);  // 5
    
    複製代碼
  • 函數做爲返回值

    function foo(x){
        return function(y){
            console.log(x+y);
        }
    }
    foo(2)(3);	// 5
    
    複製代碼

2.純函數

相同的輸入總會獲得相同的值,而且不會產生反作用的函數,叫作純函數。

例如咱們想封裝一個獲取數組最後一項的方法,有兩種選擇:

// 第一種
function getLast1(arr){
    return arr[arr.length];
}

// 第二種
function getLast2(arr){
    return arr.pop();
}

複製代碼

getLast1和getLast2雖然均可以知足需求,可是getLast2在使用以後會改變arr數組內容,下一次再使用的話,因爲arr最後一個值已經被取出,致使第二次使用取到的值是原來值的倒數第二個值。因此,像第二種這樣的封裝是很是糟糕的,會將原數據弄得特別混亂。在JavaScript的標準函數裏,也有許多不純的方法,咱們在使用時要多注意。

3.高階函數

能夠粗暴的理解,凡是接收一個函數做爲參數的函數,就是高階函數。可是這樣就太廣義了,高階函數實際上是一個高度封裝的過程,

咱們來嘗試封裝一個方法mapArray(array, fn),其有兩個參數,第一個參數是一個數組,第二個參數是一個函數,其中第二個參數參數有兩個參數fn(item, index)第一個item表示是數組每次遍歷的值,第二個是每次遍歷的序號。

var a = [1,2,3,4,5];

function mapArray(array, fn){
    var temp = [];
    if ( typeof fn === "function" ){
        for ( var k=0; k<array.length; k++ ){
            temp.push( fn.call(array, array[k], k) );
        }
    } else {
        console.error('TypeError' + fn + ' is not a function.');
    }
    return temp;
}

var b = mapArray(a, function(item, index){
    console.log(this.length);  // 5
    return item + 3;
});

console.log(b);  // [4,5,6,7,8]
複製代碼

mapArray函數實現了將數組裏的每一項都進行了相同的操做,而且在第二個函數裏的this指向的是第一個數組參數對象。

從這個封裝函數來看,實際上是把數組的循環給封裝了,那就是說,咱們要封裝的就是程序公用的那一部分,而具體要作什麼事情,則以一個參數的形式,來讓使用者自定義。這個被當作參數傳入的函數就是基礎函數,而咱們封裝的mapArray方法,就能夠稱之爲高階函數

高階函數實際上是一個封裝公共邏輯的過程

4.柯里化函數

暫時不說,由於比較難,我須要仔細理清以後再寫

9、面向對象

雖然JS是面向對象的高級語言,可是它與Java等一類語言不一樣,在ES6以前是沒有class的概念的,基於原型的構建讓你們深刻理解JavaScript的面向對象有點困難。難點就是重點,因此JS的面向對象確定是須要咱們去了解的

在EcmaScript-262中,JS對象被定義爲**"無序屬性的集合,其屬性能夠包含基本值、對象或者函數"**。

對象字面量

從上面的定義中,對象是由一系列的key-value對組成,其中value爲基本數據類型或對象,數組,函數等。像這種形式的對象定義格式,叫作對象字面量,如:

var Student = {
    name: 'ZEUS',
    age: 18,
    getName: function(){
        return this.name;
    }
    parent: {}
}
複製代碼

建立對象

第一種,經過關鍵字new來建立一個對象:

var obj = new Object();		// new 後面接的是構造函數
複製代碼

第二種,使用對象字面量:

var obj = {};
複製代碼

咱們要給對象建立屬性或方法時,能夠像這樣:

// 第一種方式
var person = {};
person.name = 'zeus';
person.getName = function(){
    return this.name;
}

// 第二種方式
var person = {
    name: 'zeus',
    getName: function(){
        return this.name;
    }
}
複製代碼

訪問對象的方法或屬性,可使用.或者 ['']

構造函數與原型

在函數式編程那一節,咱們講到封裝函數就是封裝一些公共邏輯與功能。當面對具備同一類事物時,咱們也能夠藉助構造函數與原型的方式,將這類事物封裝成對象

例如:

var Student = function(name, age){
    this.name = name;
    this.age = age;
    console.log(this);
}
Student.prototype.getName = function(){
    return this.name;
}

// 實例化對象時
var zeus = new Student('zeus', 18);  // zeus實例
zeus.getName();
Student('zeus', 18);   // window
複製代碼

能夠看到,具體的某個學生的特定屬性,一般放在構造函數中;全部學生的方法和屬性,一般放在原型對象中。

上述代碼輸出內容以下圖:

image-20190531113004351

這裏提個問,構造函數是高階函數嗎?在這裏,new Student()內部的this爲何會指向實例對象呢,而Student()內部this指向window?

構造函數名約定首字母大寫,這裏必需要注意。構造函數的this與原型方法中的this指向的都是當前實例。像上面,使用了new關鍵字以後,Student()函數纔是構造函數。那new關鍵字具體作了什麼呢?咱們能夠來用一個函數模擬new關鍵字的能力:

function New(func){
    var res = {};
    if ( func.prototype !== null ){
        res.__proto__ = func.prototype; 
    }
    var ret = func.apply(res, Array.prototype.slice.call(arguments, 1) );
    // 當咱們在構造函數中明確指定了返回對象時,進行這一步
    if ( (typeof ret ==="object" || typeof ret==="function" ) && ret !== null ){
        return ret;
    }
    // 若是沒有明確指定返回對象,則默認返回res,這個res就是實例對象
    return res;
}
複製代碼

經過對New方法的封裝,能夠知道new關鍵字在建立實例時經歷了以下過程:

  1. 先建立一個新的、空的實例對象;
  2. 將實例對象的原型(__proto__),指向構造函數的原型(prototype);
  3. 將構造函數內部的this,修改成指向實例;
  4. 最後返回該實例對象

構造函數、原型、實例之間的關係

咱們可不能夠在構造函數裏面建立方法?固然是能夠的,可是這樣比較消耗更多的內存空間,由於每一次建立實例對象,都會建立一次該方法。

因此能夠看出,在構造函數裏聲明的變量與方法只屬於當前實例,所以咱們能夠將構造函數中聲明的屬性與方法看作該實例的私有屬性和方法,它們只能被當前實例訪問。而原型中的屬性與方法可以被全部的實例訪問,所以能夠將原型中聲明的屬性和方法稱爲公有屬性與方法。若是構造函數裏的私有屬性/方法與原型裏的公有屬性/方法重名,那麼會優先訪問私有屬性/方法

怎麼判斷一個對象是否擁有某一個方法/屬性

  1. 經過in運算符來判斷,不管該方法/屬性是否公有,只要存在就返回true,不然返回false
  2. 經過hasOwnProperty方法來判斷,只有該方法/屬性存在且爲私有時,才返回true,不然返回false
var Student = function(name, age){
this.name = name;
this.age = age;
this.speak = function(){
   console.log('我是'+this.name+'的私有方法')
}
}

Student.prototype.getName = function(){
console.log(this.name);
}

var Bob = new Student('Bob', 18);
Bob.speak();
Bob.getName();

console.log( 'speak' in Bob);  // true
console.log( 'getName' in Bob);  // false
console.log( Bob.hasOwnProperty('speak') );  // true
console.log( Bob.hasOwnProperty('getName') );  // false
複製代碼

若是要在原型上添加多個方法,還能夠這樣寫:

function Student(){};
Student.prototype = {
    constructor: Student,    // 必須聲明
    getName: function(){},
    getAge: function(){}
}
複製代碼

原型對象

原型對象其實也是普通對象。在JS中,幾乎全部的對象均可以是原型對象,也能夠是實例對象,還能夠是構造函數,甚至身兼數職。當一個對象身兼多職時,它就能夠被看做原型鏈中的一個節點。

當要判斷一個對象student是不是構造函數Student的實例時,可使用instanceof關鍵字,其返回一個boolean值:

student instanceof Student;    // true or false
複製代碼

咱們回到最開始的時候,當建立一個對象時,除了使用對象字面量也可使用new Object()來建立,所以Object實際上是一個構造函數,而其對應的原型Object.prototype則是原型鏈的終點。

當建立函數時,除了使用function關鍵字外,還可使用Function對象:

var add = new Function("a", "b", "return a+b");
// 等價於
var add = function(a, b){
    return a+b;
}
複製代碼

在這裏,add方法是一個實例對象,它對應的構造函數是Function,它的原型是Function.prototype,也就是add.__proto__ === Function.prototype。這裏比較特殊的是,Function同時是Function.prototype的構造函數與實例(由於Function也是一個函數啦!);而與此同時,由於Function是繼承自Object的,因此Function.prototype仍是Object.prototype的實例,它們的原型鏈能夠用下圖表示:

add函數相關的原型鏈

對原型鏈上的方法與屬性的訪問,與做用域鏈類似,也是一個單向的查找過程,雖然add與Object原型沒有直接關係,可是它們在同一條原型鏈上,所以add也可使用Object的toString方法等(好比hasOwnProperty方法)。

實例方法,原型方法,靜態方法

看以下代碼便可瞭解:

function Foo(){
    this.bar = function(){     // 實例(私有)方法
        return 'bar in Foo';    
    }
}

Foo.bar = function(){		// 靜態方法,不須要實例化,直接能夠用函數名調用
    return 'bar in static';	
}

Foo.prototype.bar = function(){		// 原型方法
    return 'bar in prototype';
}
複製代碼

繼承

由於封裝一個對象是由構造函數與原型共同組成的,因此繼承也被分爲兩部分,一部分是構造函數繼承另外一部分是原型繼承。

以下代碼:

var Person = function(name, age){
    this.name = name;
    this.age = age;
}

Person.prototype.say = function(){
    console.log('您好');
}

var Student = function(name, age, grade){
    Person.call(this, name, age);  // 在這裏是構造繼承
    this.grade = grade;
}
// 下面這兩句是原型繼承
Student.prototype = new Person();
Student.prototype.constructor = Student;  // 這句必定不能少
Student.prototype.speak = function(){
    console.log(`我叫${this.name},我今年${this.age}歲了,我語文考了${this.grade}分`);
}

var kevin = new Student('kevin', 18, 90);
kevin.speak();
kevin.say();
複製代碼

這段代碼屬於組合繼承,是比較經常使用的一種繼承方式,不過他有個不足就是,不管什麼狀況下都會調用兩次父級構造函數。

以下是優化以後的代碼:

function inheritPrototype(child, parent){
    var obj = Object(parent.prototype);
    obj.prototype = child;
    child.prototype = obj;
}

var Person = function(name, age){
    this.name = name;
    this.age = age;
}
Person.prototype.say = function(){
    console.log('您好');
}

var Student = function(name, age, grade){
    Person.call(this, name, age);
    this.grade = grade;
}
inheritPrototype(Student, Person);
Student.prototype.speak = function(){
    console.log(`我叫${this.name},我今年${this.age}歲了,我語文考了${this.grade}分`);
}

var kevin = new Student('kevin', 18, 90);
kevin.speak();
kevin.say();
複製代碼

這一段是寄生組合式繼承,是開發人員認爲的引用類型最理想的繼承方式。

10、ES6基礎

ES6是ECMAScript6的簡稱,也被稱爲ECMAScript2015。是目前兼容性比較樂觀且比較新的ECMAScript標準,雖然增長了前端的學習成本,可是與ES5相比,它提供了不少新的特性,並且如今前端基本上都在轉ES6了,因此ES6也是學習前端的必備基礎。不過目前,並非全部的瀏覽器都支持ES6新特性,可是在開發中,咱們能夠藉助babel提供的編譯工具,將ES6轉化爲ES5,這也極大的推進了前端團隊對ES6的接受。對於大多數經常使用的ES6新特性,目前最新版的Chrome都已所有支持。不過對於部分知識,例如模塊化modules,則須要經過構建工具纔可以使用,例如使用webpack和babel的VueJS。

新的變量聲明方式let/const

在ES6中,咱們可使用let來聲明變量,其中,let會產生變量的塊級做用域,而且let在變量提高的時候不會給變量賦值undefined,因此這樣使用會直接報錯:

console.log(a);  // 不會輸出undefined,會直接報ReferenceError
let a = 10;
複製代碼

因此,若是你決定用ES6的變量聲明來寫了,就所有用let吧,不要let和var混用。

const是用來聲明一個常量的,該常量的引用地址不可改變。

這裏須要注意的是let和const變量的值,都是一個引用,若是對let的變量進行賦值操做,是新建了該值以後將其引用從新賦給變量。

例如:

const a = [];
a.push(1);    // 不會報錯
const b = 1;
b = 2;   // 報錯Uncaught TypeError: Assignment to constant variable.
複製代碼

箭頭函數

ES6的箭頭函數是一個用起來比較溫馨的方式,咱們用例子來看一下:

// ES5中聲明函數
var fn = function(a, b){
    return a+b;
}
// ES6箭頭函數
var fn = (a, b) => a+b;  // 當函數直接return時,能夠省略{}
複製代碼

須要注意的是,箭頭函數只能替換函數表達式,使用function關鍵字聲明的函數不能使用箭頭函數替換,以下形式不能用箭頭函數替換:

function fn(a,b){
    return a+b;
}
// 不能夠替換成下面形式
fn(a, b)=> a+b;
複製代碼

咱們一看到函數就應該去想如下它內部的this在調用時指向誰,從前面知識咱們知道,函數內部的this指向,與它的調用者有關,或者使用call/apply/bind也能夠修改函數內部的this指向。

咱們來回顧一下,請思考下面的輸出內容:

var name = 'Tom';
var getName = function(){
    console.log(this.name);   
}
var person = {
    name: 'Alex',
    getName: function(){
        console.log(this.name);
    }
}
var other = {
    name: 'Jone'
}
getName();    // ?
person.getName();   // ?
getName.call(other);    // ?
複製代碼

上面分別輸出了Tom,Alex,Jone,第一個getName()獨立調用,其this指向undefined並自動轉向window。那假如所有換成箭頭函數呢?咱們看一下輸出結果:

var name = 'Tom';
var getName = () => {
    console.log(this.name);   
}
var person = {
    name: 'Alex',
    getName: () => {
        console.log(this.name);
    }
}
var other = {
    name: 'Jone'
}
getName();          //Tom
person.getName();   //Tom
getName.call(other);//Tom
複製代碼

運行發現,三次都輸出了Tom,這也是須要你們注意的地方。箭頭函數中的this,就是聲明函數時所處的上下文中的this,他不會被其餘方式所改變

因此有些場景能夠用箭頭函數來解決:

document.name = 'doc';
var obj = {
    name: 'zeus',
    do: function(){
        document.onclick = function(){
            console.log(this.name);   // 由於是document調用了該函數,因此點擊頁面輸出doc
        }
    }
}
obj.do();
複製代碼

若是咱們要在頁面被點擊後輸出zeus,可能最經常使用的就是在document.onclick外面使用_this/that暫存this的值,以下:

document.name = 'doc';
var obj = {
    name: 'zeus',
    do: function(){
        var _this = this;
        document.onclick = function(){
            console.log(_this.name);   // 使用了_this中間變量,輸出zeus
        }
    }
}
obj.do();
複製代碼

其實,能夠用箭頭函數的特性來作:

document.name = 'doc';
var obj = {
    name: 'zeus',
    do: function(){
        document.onclick = () => {
            console.log(this.name);   // 箭頭函數的this指向當前上下文
        }
    }
}
obj.do();
複製代碼

模板字符串

模板字符串是解決通常的字符串拼接麻煩的問題產生的,它使用反引號`包裹字符串,使用${}包裹變量名,以下代碼:

// ES5
var a = 'hello';
var b = 'zeus';
var c = 10;
var s = a + ' '+ b + ' ' + (c+10);  // hello zeus 20
// ES6
var str = `${a} ${b} ${c+10}`;   // hello zeus 20
複製代碼

解析結構

解析結構能夠很方便的從數組或對象獲取值,例如對於以下的對象字面量:

let zeus = {
    name: 'zeus',
    age: 20,
    job: 'Front-end Engineer'
}
複製代碼

若是要取值,咱們常常會使用點運算符進行取值,例如zeus.namezeus['age'],當使用解析結構時,能夠這樣作:

const {name, age, job} = zeus;
console.log(name);
複製代碼

固然const表示得到到的值聲明爲常量,也可使用let或var。咱們還能夠給屬性變量指定默認值:

const {name = 'kevin', age = 20, job = 'student'} = zeus;
// 若是zeus對象對應屬性沒有值,則使用前面指定的默認值
複製代碼

或者給屬性變量從新命名:

const {name: username, age, job} = zeus;
// 後面使用的話就必須使用username
複製代碼

數組也可使用解析結構,以下:

let arr = [1,2,3,4];
const [a,b,c,d] = arr;
console.log(a);  // 1
console.log(c);  // 3
複製代碼

數組的解析結構的屬性變量名能夠隨意命名,可是是按順序來一一對應的,而對象解析結構中的屬性變量必須跟變量屬性命名一致。對象屬性的解析結構也能夠進行嵌套,例如:

let kevin = {
    name: 'kevin',
    age: 20,
    job: 'Student',
    school: {
    	name: 'smu',
    	addr: '成都'
	}
};
const {school: {name}} = kevin;
console.log(name);  // smu
複製代碼

展開運算符

在ES6中,使用...做爲展開運算符,它能夠展開數組/對象。例如:

const arr1 = [1,2,3];
const arr2 = [...arr1, 4,5,6];   // [1,2,3,4,5,6]
let person_kevin = {
    name: 'kevin',
    age: 20,
    job: 'Student'
};
let student_kevin = {
    ...person_kevin,
    school: {
        name: 'smu',
        addr: '成都'
    }
};
複製代碼

展開運算符能夠用在函數形參裏面,可是只能做爲函數的最後一個參數

Promise

異步與同步

同步是指發送一個請求,須要等待直到請求結果返回以後,再繼續下一步操做。異步在發送請求後,不會等待而是直接繼續下一步操做。

咱們來實現一個異步方法:

function fn(){
    return new Promise((resolve, rejsct) =>{
        setTimeout(function(){
            resolve('執行fn內容');
        },1000);
    });
}
複製代碼

可使用async/await來模擬同步效果:

var foo1 = async function(){
    let t = await fn();
    console.log(t);
    console.log('接着執行的內容');
}
foo1();
// 等待1秒後,輸出:
// 執行fn內容
// 接着執行的內容
複製代碼

若是採用異步操做的話,以下:

var foo2 = function(){
    fn().then(res=>{
        console.log(res);
    });
    console.log('接着執行的內容');
}
foo2();
// 先輸出 接着執行的內容
// 等待1秒後
// 輸出 執行fn的內容
複製代碼

簡單用法

咱們應該有使用過jquery的$.ajax()方法,該方法獲取後端的值是在參數屬性success函數的參數中獲取的,假如咱們在第一次ajax請求後,要進行第二次ajax請求而且這一次請求的參數是第一次success獲的值,若是還有第三次呢,那就必須這樣寫:

$.ajax({
    url: '',
    data: {...},
    success: function(res){
        $.ajax({
            data: {res.data},
            success: function(res){
                $.ajax(...)
            }
        })
    }
})
複製代碼

這樣就造成了常說的「回調地獄」,不過在ES6中,Promise語法能夠解決這樣的問題。·Promise能夠認爲是一種任務分發器,將任務分配到Promise隊列,執行代碼,而後等待代碼執行完畢並處理執行結果。簡單的用法以下:

var post = function(url, data) {
    return new Promise(function(resolve, reject) {
        $.ajax({
            url: url,
            data: data,
            type: 'POST',
            success: function(res){
                resolve(res);
            },
            error: function(err) {
                reject(err);
            }
        });
    });
}
post('http://127.0.0.1:8080/order', {id:1}).then(function(res){
    // 這裏返回成功的內容
}, function(err){
    // 這裏是報錯信息
});
複製代碼

上面的代碼封裝了jquery的ajax請求,將POST的請求進行了封裝,post(..)函數內部返回了一個Promise對象,Promise對象有一個then方法,then方法的第一個參數是resolve回調函數表示成功的操做,第二個參數是reject回調函數表示失敗或異常的操做。其實,Promise還有一個catch方法也能夠獲取reject回調函數,如post也能夠這樣使用:

post('http://127.0.0.1:8080/order', {id:1}).then(function(res){
    // 這裏返回成功的內容
}).catch(function(err){
    // 這裏是報錯信息
});
複製代碼

事件循環機制

後面再單獨分享

對象與class

參考阮一峯class介紹

模塊化

後面再單獨分享

相關文章
相關標籤/搜索