JS原型鏈,做用域鏈,上下文,閉包,this查缺補漏(大雜燴)

走在前端的大道上css

本篇是 一篇文章帶你理解原型和原型鏈一篇文章帶你徹底理解this的查漏補缺,會不斷豐富提煉總結更新。html

什麼是原型鏈

原型鏈 是針對構造函數的,好比我先建立了一個函數,而後經過一個變量new了這個函數,那麼這個被new出來的函數就會繼承建立出來的那個函數的屬性,而後若是我訪問new出來的這個函數的某個屬性,可是我並無在這個new出來的函數中定義這個變量,那麼它就會往上(向建立出它的函數中)查找,這個查找的過程就叫作原型鏈。前端

Object ==> 構造函數1 ==> 構造函數2面試

  就和css中的繼承同樣,若是自身沒有定義就會繼承父元素的樣式。segmentfault

function a(){};
a.prototype.name = "追夢子";
var b = new a();
console.log(b.name); //追夢子

什麼是做用域鏈

做用域 是針對變量的,好比咱們建立了一個函數,函數裏面又包含了一個函數,那麼如今就有三個做用域數組

全局做用域==>函數1做用域==>函數2做用域app

做用域的特色:先在本身的變量範圍中查找,若是找不到,就會沿着做用域往上找。函數

如:this

var a = 1;
function b(){
    var a = 2;
    function c(){
        var a = 3;
        console.log(a);
    }
    c();
}
b();

最後打印出來的是3,由於執行函數c()的時候它在本身的範圍內找到了變量a因此就不會越上繼續查找,若是在函數c()中沒有找到則會繼續向上找,一直會找到全局變量a,這個查找的過程就叫做用域鏈。spa

不知道你有沒有疑問,函數c爲何能夠在函數b中查找變量a,由於函數c是在函數b中建立的,也就是說函數c的做用域包括了函數b的做用域,固然也包括了全局做用域,可是函數b不能向函數c中查找變量,由於做用域只會向上查找。

上下文

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

// 由於沒有定義a因此報錯了。

var a = 52;
console.log(a); //52

// 有定義a,而且給a賦值了52因此打印a就是52。

console.log(a); //undefined
var a = 52;

雖然有定義a可是打印卻在變量a的前面,那爲何不是報錯而是打印出來的是undefined?由於在js執行代碼以前,js會先獲取到全部的變量而且把這些變量放置到js代碼的頂部。(簡稱變量聲明提早)

咱們給賦值給a的52到哪去了。雖然我前面說了js會事先獲取全部的變量而且將這些變量放置到頂部,可是 變量的賦值並不會事先執行 ,也就是說,在哪聲明的變量,這個變量的賦值就在哪執行
實際上,上面的代碼是這樣執行的:

var a;
console.log(a); //undefined
a=52;

console.log(a); 
function a(){
  this.user = "追夢子";
}
//爲何,能夠事先就打印出函數a呢?

由於 函數的賦值在函數聲明的時候 就已經賦值了,結合上面我說的變量提早,那是否是就能夠理解這句話了?

function a(){
  this.user = "追夢子";
}
console.log(a);
//正常
a(); //Uncaught TypeError: a is not a function
var a = function(){
  console.log(52);
}
//爲何如今又不行了?

由於如今的函數已經賦值給了變量a,如今 它的執行過程和變量同樣 了,咱們一般把這種函數賦值給變量的形式叫作 函數表達式


var a = function(){
  console.log(52);
}
a(); //52
//正常
if(false){
    var a = 1;
}
console.log(a); //undefined

之因此沒有報錯而是輸出了undefined是由於 變量存在預解析 的狀況,又由於 js沒有塊級做用域,因此最後代碼就成了這樣

var a;
if(false){
    a = 1;
}
console.log(a);

總結:

函數分爲:函數聲明和函數表達式。

  函數聲明

function a(){
    alert("追夢子博客");
}

  函數表達式

var a = function(){
    alert("追夢子");
}

  看似兩段相同的語句,它們的執行順序卻大相徑庭,函數聲明時的賦值行爲是在函數建立的時候進行的,而函數表達式的賦值行爲是在執行這句變量時進行的(由於它已經賦值給了變量因此我這裏把它稱爲變量)。

  不論是變量仍是函數都會存在變量聲明提早

來看看幾題有意思的js例題,加以理解

  

var a = 1;
function b(){
    console.log(a); //undefined
    var a = 5;
}
b();

爲何打印的是undefined?

  咱們先來看看它的解析過程:

var a = 1;
function b(){
    var a
    console.log(a); //undefined
    a = 5;
}
b();

咱們一塊兒來看看另一題比較有難度的js面試題:

var a = 1;      
function b() {      
    a = 120;      
    return;      
    function a() {}
}      
b();      
alert(a); //1;

  若是你看了上面一題我相信你應該有種不知所措的感受,這裏如今爲何又是1了呢?

我把執行過程的代碼寫出來我相信你就懂了。

var a = 1;      
function b() {
    var a;      
    a = 120;      
    return;      
    function a() {}
}      
b();      
alert(a);

  若是你正在js的進階階段確定更悶了,你確定會想咱們不是寫return了嗎?return後面的代碼不是不執行嗎?爲何這裏卻影響了這段代碼?

  雖然return後面的代碼不會被執行,可是在js預解析的時候(變量提高的時候)仍是會把return後面的變量提早,因此咱們這段代碼 由於變量提早因此函數裏面的變量a就成了局部變量,由於函數外部是訪問不了函數內部的變量因此就輸出了1。

  另外提兩點,函數的arguments和函數名都是直接賦值的,也就是在這個函數解析的時候就會進行賦值。

做用域的進階

 什麼是自由變量?

如我在全局中定義了一個變量a,而後我在函數中使用了這個a,這個a就能夠稱之爲自由變量,能夠這樣理解,凡是跨了本身的做用域的變量都叫 自由變量

var a = "追夢子";
function b(){
    console.log(a); //追夢子
}
b();

上面的這段代碼中的變量a就是一個自由變量,由於在函數b執行到console.log(a)的時候,發如今函數中找不到變量a,因而就往上一層中找,最後找到了全局變量a。

  做用域的進階

在我講做用域鏈的時候說過若是有一個全局變量a,以及函數中也有一個變量a,那麼只會做用函數中的那個變量a,都是有一種狀況就顯得比較複雜一些,咱們一塊兒來看看這段代碼。

var aa = 22;
function a(){
    console.log(aa);
}
function b(fn){
    var aa = 11;
    fn();
}
b(a); //22

  最後打印的不是11而是22,爲何會這樣呢?一塊兒來分析一下這段代碼。

假如咱們的代碼是這樣的

var aa = 22;
function a(){
    console.log(aa);
}

打印出的是22,我想你們應該沒有意見,可是有一點我必定要提,那就是 在建立這個函數的時候,這個函數的做用域就已經決定了,而是否是在調用的時候,這句話至管重要。

分析一下過程,首先咱們建立了一個全局變量aa

var aa = 22;

接着咱們建立了一個函數a

function a(){
    console.log(aa);
}

這時js解析這個函數的時候,就已經決定了這個函數a的做用域,既若是在函數a中找不到變量aa那就會到全局變量中找,若是找到了就返回這個aa,若是找不到就報錯。

接着咱們又建立了一個函數b

function b(fn){
    var aa = 11;
    fn();
}

在函數b中咱們定義了又從新定義了這個變量aa,雖然咱們這個時候從新定義了變量aa,可是由於函數a的做用域在建立的時候已經決定了,因此在函數b中建立的那個變量aa以及和函數a裏面的那個變量aa沒有關係了

function b(fn){
    var aa = 11;
    fn();
}
b(a);

咱們把函數a傳到了函數b中,而且當作函數b的形參,接着咱們執行了這個被傳進去的函數a,最後打印出來的就是22。

在建立這個函數的時候,這個函數的做用域就已經決定了,而是否是在調用的時候

筆者注: 看到這句話是否是似曾相識?this的指向在函數定義的時候是肯定不了的,只有函數執行的時候才能肯定this到底指向誰,實際上this的最終指向的是那個調用它的對象

一個是做用域,一個是上下文

舉個例子回顧對比一下

box.onclick = function(){
  function fn(){
    alert(this);
  }
  fn();
};

咱們本來覺得這裏面的this指向的是box,然而倒是Window。通常咱們這樣解決,將this保存下來:

box.onclick = function(){
  var _this = this;
  function fn(){
    alert(_this);
  }
  fn();
};

還有一些狀況,有時咱們想讓僞數組也可以調用數組的一些方法,這時call、apply、bind就派上用場了。

咱們先來解決第一個問題修復this指向。

box.onclick = function(){
  function fn(){
    alert(this);
  }
  fn();
};

改爲以下:

box.onclick = function(){
  function fn(){
    console.log(this);
  }
  fn.call(this);
};

很神奇吧,call的做用就是改變this的指向的,第一個傳的是一個對象,就是你要借用的那個對象。

fn.call(this);

  這裏的意思是 讓this去調用fn這個函數,這裏的this是box,這個沒有意見吧?box調用fn,這句話很是重要,咱們知道 this始終指向一個對象,恰好box就是一個對象。那麼fn裏面的this就是box。

能夠簡寫的,好比:

box.onclick = function(){
  var fn = function(){
    console.log(this); //box
  }.call(this);
};

或者這樣:

box.onclick = function(){
  (function(){
    console.log(this);
  }.call(this)); //box
};

又或者這樣:

var objName = {name:'JS2016'};
var obj = {
  name:'0 _ 0',
  sayHello:function(){
    console.log(this.name);
  }.bind(objName)
};
obj.sayHello();//JS2016

call和apply、bind可是用來改變this的指向的,但也有一些小小的差異。下面咱們來看看它們的差異在哪。

call和apply、bind可是用來改變this的指向的,但也有一些小小的差異。下面咱們來看看它們的差異在哪。

function fn(a,b,c,d){
  console.log(a,b,c,d);
}

//call
fn.call(null,1,2,3);

//apply
fn.apply(null,[1,2,3]);

//bind
var f = fn.bind(null,1,2,3);
f(4);

結果以下:

1 2 3 undefined
1 2 3 undefined
1 2 3 4

前面說過第一個參數傳的是一個你要借用的對象,但這麼咱們不須要,全部就傳了一個null,固然你也能夠傳其餘的,反正在這裏沒有用到,除了第一個參數後面的參數將做爲實際參數傳入到函數中。

  call就是挨個傳值apply傳一個數組bind也是挨個傳值,call和apply會直接執行這個函數,而bind並不會而是將綁定好的this從新返回一個新函數,何時調用由你本身決定。

var objName = {name:'JS2016'};
var obj = {
  name:'0 _ 0',
  sayHello:function(){
    console.log(this.name);
  }.bind(objName)
};
obj.sayHello();//JS2016

這裏也就是爲何我要用bind的緣由,若是 用call的話就會報錯了。本身想一想這個sayHello在obj都已經執行完了,就根本沒有sayHello這個函數了。

clipboard.png

這幾個方法使用的好的話能夠幫你解決很多問題好比:

正常狀況下Math.max只能這樣用

Math.max(10,6)

但若是你想傳一個數組的話你能夠用apply

var arr = [1,2,30,4,5];
console.log(Math.max.apply(null,arr));

clipboard.png

又或者你想讓僞數組調用數組的方法

function fn(){
  [].push.call(arguments,3);
  console.log(arguments); //[1, 2, 3]
}
fn(1,2);

再者:

var arr = ['aaabc'];
console.log(''.indexOf.call(arr,'b')); //3

參考文章:
1.什麼是做用域鏈,什麼是原型鏈,它們的區別,在js中它們具體指什麼?
2.js中的執行上下文,菜鳥入門基礎
3.JS中call、apply、bind使用指南,帶部分原理。
3.理解js中的自由變量以及做用域的進階

相關文章
相關標籤/搜索