this、原型和原型鏈、作用域和作用域鏈

涉及到JavaScript高級的知識,永遠都躲不過this、原型、原型鏈、作用域和作用域鏈。但是拗口的概念又經常讓人描述得不準確,在此做個記錄。

1、this

代表函數調用相關聯的對象,通常也稱之為執(zhí)行上下文

1.1、作為函數直接調用,非嚴格模式下,this指向window,嚴格模式下,this指向undefined;

// 非嚴格模式
function foo() {
    console.log(this)
}
foo()  // window

// 嚴格模式
"use strict"
function foo() {
    console.log(this)
}
foo()  // undefined

1.2、作為某對象的方法調用,this通常指向調用的對象。

let foo = {
  bar: function() { 
    console.log(this) 
  }
}

foo.bar()   // foo

1.3.、使用apply、call、bind 可以綁定this的指向(不管給函數 bind 幾次,函數中的this 永遠由第一次 bind 決定)。

let a = {}
let fn = function () { console.log(this) }
fn.bind(a)()  // a

let a = {}
let fn = function () { console.log(this) }

fn.bind().bind(a)()  // window

1.4、 在構造函數中,this指向新創(chuàng)建的對象

function Foo() {
    this.name = 'hi';
    console.log(this)
}
new Foo()  // Foo {name: "hi"}

1.5、 箭頭函數沒有單獨的this值,this在箭頭函數創(chuàng)建時確定,它與聲明所在的上下文相同。

let a = {
  b: function() { 
    console.log(this) 
  },
  c: () => {
    console.log(this)
  }
}

a.b()   // a
a.c()   // window

this判斷 下面輸出為多少?

var name1 = 1;

function test() {
    let name1 = 'kin';
    let a = {
        name1: 'jack',
        fn: () => {
      var name1 = 'black'
      console.log(this.name1)
    }
  }
    return a;
}

test().fn() // ?

答案: 輸出1
因為fn處綁定的是箭頭函數,箭頭函數并不創(chuàng)建this,它只會從自己的作用域鏈的上一層繼承this。這里它的上一層是test(),非嚴格模式下testthis值為window

  • 如果在綁定fn的時候使用了function,那么答案會是 jack
  • 如果第一行的 var 改為了 let,那么答案會是 undefind, 因為let不會掛到window

再來一題:

var num = 1;
var myObject = {
  num: 2,
  add: function() {
    this.num = 3;
    (function() {
      console.log("第1個出現的console:" + this.num);
      this.num = 4;
    })();
    console.log("第2個出現的console:" + this.num);
  },
  sub: function() {
    console.log("第3個出現的console:" + this.num);
  }
};

myObject.add();
console.log("第4個出現的console:" + myObject.num);
console.log("第5個出現的console:" + num);
var sub = myObject.sub;
sub();

下面來看正確答案:

第1個出現的console:1
第2個出現的console:3
第4個出現的console:3
第5個出現的console:4
第3個出現的console:4

1.2、作為某對象的方法調用,this通常指向調用的對象。

  • myObject.add()里,第1個出現的console在立即執(zhí)行函數里,根據上面提到過的,那么這個1.2所說,立即執(zhí)行函數在myObject.add()里,this應該指向myObject。然而,立即執(zhí)行函數中的this指向window,因為立即執(zhí)行函數是window調用的,所以,第1個出現的console的值為1。

  • 第1個出現的console執(zhí)行完以后,this.num = 4,改變的是window中的值。所以第5個出現的console的值為4。

  • var sub = myObject.sub;,此時sub的環(huán)境也是window,所以第3個出現的console的值也是4。

一個小小的吐槽 ~ 之前以為自己把 this搞懂了,一個立即執(zhí)行函數 IEFF讓我遭遇了 this滑鐵盧。

多個this規(guī)則出現時,this最終指向哪里?

首先,new 的方式優(yōu)先級最高,接下來是 bind 這些函數,然后是 obj.foo() 這種調用方式,最后是 foo 這種調用方式,同時,箭頭函數的 this 一旦被綁定,就不會再被任何方式所改變。

2、原型與原型鏈

2.1、原型對象

每一個JavaScript對象(null除外)都和另一個對象相關聯,“另一個”對象就是我們熟知的原型,每一個對象都是從原型繼承屬性

每一個函數都有一個prototype(原型)屬性,這個屬性指向函數的原型對象,而這個對象的用途是包含可以由特定類型的所有實例共享的屬性和方法。

  • 《JavaScript高級程序設計》(第3版) 中提到:prototype就是通過調用構造函數而創(chuàng)建的那個對象實例的原型對象。
  • 《JavaScript權威指南》(第6版) 中也提到:通過new關鍵字和構造函數調用創(chuàng)建的對象的原型就是構造函數的prototype屬性的值。
function Person(){};
Person.prototype.name = "wood";
Person.prototype.job = "engineer";
Person.prototype.sayName = function(){
    console.log(this.name);
}

var person1 = new Person();
person1.sayName();  // wood

var person2 = new Person();
person2.sayName();  // wood

console.log(person1.sayName == person2.sayName);  // true

所以通過上述例子可知:Person.prototype就是實例person1和實例person2的原型對象。


在默認情況下,所有原型對象都會自動獲得一個constructor(構造函數)屬性,這是一個指向prototype屬性所在函數的指針。

也就是說每個原型都有都有一個 constructor 屬性,指向了原型所在的函數,在上面例子來說,Person.prototype.constructor == Person

上述代碼中各個對象之間的關系

有細心的小伙伴會注意到,為什么實例person1person2之中是[[prototype]]?
—— 當調用構造函數創(chuàng)建一個新實例后,該實例的內部將包含一個指針(內部屬性),指向構造函數的原型對象。ECMA-262第5版中管這個指針叫[[prototype]]。雖然在腳本中沒有標準的方式訪問[[prototype]],但Firfox、Safari和Chrome在每個對象上都支持一個__proto__的屬性(來自-《JavaScript高級程序設計》(第三版))。
所以在有的地方會直接說,實例person1person2__proto__屬性,指向Person.prototype。

注意:我們無法直接訪問到[[prototype]]__proto__,可以通過 isPrototypeOf()方法判斷某個原型和對象實例是否存在關系,或者,也可以使用 Object.getPrototypeOf() 獲取一個對象實例 __proto__ 屬性的值。

console.log(Person.prototype.isPrototypeOf(person1)); // true
console.log(Object.getPrototypeOf(person1) == Person.prototype); // true

更簡單的原型語法
前面例子中,每添加一個屬性和方法就要敲一次Person.prototype,更常見的做法是用一個包含所有屬性和方法的對象字面量來重寫整個原型對象,如下所示:

function Person(){};

Person.prototype = {
    name : "wood",
    job: "engineer",
    sayName : function () {
        console.log(this.name);
    }
};

重寫后的代碼與原代碼最終結果相同。但是有一個例外:constructor屬性不再指向Person了。我們在前面提到過,每創(chuàng)建一個函數,就會同時創(chuàng)建它的prototype對象,這個對象也自動獲得constructor屬性。而我們這樣重寫,本質上完全重寫了默認的prototype對象,因此constructor屬性也就變成了新對象的constructor屬性,不再指向Person函數了。此時,盡管instanceof操作符還能返回正確結果,但是通過constructor已經無法確定對象的類型了,結果如下所示:

var friend = new Person();

console.log(friend instanceof Object);  // true
console.log(friend instanceof Person);  // true
console.log(friend.constructor == Person);  // false
console.log(friend.constructor == Object);  // true

從上可見,constructor屬性等于Object而不是等于Person了。如果constructor的值真的很重要,可以像下面這樣特意將它設置回適當的值:

function Person(){};

Person.prototype = {
    constructor : Person,
    name : "wood",
    job: "engineer",
    sayName : function () {
        console.log(this.name);
    }
};

以上代碼特意包含了一個constructor屬性,并將它的值設置為Person,從而確保了通過該屬性能夠訪問到適當的值。
但是,重設constructor屬性后會導致它的[[Enumerable]]特性被設置為true。默認情況下,原生的constructor屬性是不可枚舉的。因此,可通過Object.defineProperty()來重設構造函數,如下代碼所示:

function Person(){};

Person.prototype = {
    name : "wood",
    job: "engineer",
    sayName : function () {
        console.log(this.name);
    }
};

Object.defineProperty(Person.prototype, "constructor", {
    enumerable: false;
    value: Person
})

2.2、原型鏈

每個構造函數都有一個原型對象,原型對象都包含一個指向構造函數的指針,而實例都包含一個指向原型對象的內部指針。

而每一個原型對象都是個普通對象,普通對象都具有原型。所有的內置構造函數以及大部分自定義的構造函數都具有一個繼承自Object.prototype的原型(所有函數的默認原型都是Object的實例)。例如(關系示意見下圖),Date.prototype的屬性繼承自Object.prototype,因此由new Date()創(chuàng)建的Date對象的屬性同時繼承自Date.prototypeObject.prototype。這一系列鏈接的原型對象就是所謂的“原型鏈”。

原型鏈關系示意圖

當以讀取模式訪問一個實例屬性時,首先會在實例中搜索該屬性。如果沒有找到該屬性,則會繼續(xù)搜索實例的原型。在通過原型鏈實現繼承的情況下,搜索過程就得以沿著原型鏈繼續(xù)向上。

可以用instanceof操作符和isPrototypeOf()方法來確定原型和實例的關系。

謹慎地定義方法
子類型有時候需要覆蓋超類型中的某個方法,或者需要添加超類型中不存在的某個方法。但不管怎樣,給原型添加方法的代碼一定要放在替換原型的語句之后。

function SuperType(){
    this.property = true;
}

SuperType.prototype.getSuperValue = function(){
    return this.property;
};

function SubType(){
    this.subproperty = false;
}

// 繼承了 SuperType
SubType.prototype = new SuperType();

// 添加新方法
SubType.prototype.getSubValue = function (){
    return this.subproperty;
};

// 重寫超類型中的方法
SubType.prototype.getSuperValue = function (){
    return false;
};

var instance = new SubType();
alert(instance.getSuperValue());   //false

還有一點需要注意的是,通過原型鏈實現繼承時,不能使用對象字面量來創(chuàng)建原型,這樣做會重寫原型鏈。如下所示:

function SuperType(){
    this.property = true;
}

SuperType.prototype.getSuperValue = function(){
    return this.property;
};

function SubType(){
    this.subproperty = false;
}

// 繼承了 SuperType
SubType.prototype = new SuperType();

// 使用字面量添加新方法,會導致上一行代碼無效
SubType.prototype = {
    getSubValue : function (){
        return this.subproperty;
    },

    someOtherMethod : function (){
        return false;
    }
};

var instance = new SubType();
alert(instance.getSuperValue());   //error!
  • 原型鏈的問題
    原型鏈最主要的問題來自包含引用類型值的原型。包含引用類型值的原型屬性會被所有實例共享;所以要在構造函數中定義屬性,而不是在原型對象中定義。
    在通過原型實現繼承時,原型實際上會變成另一個類型的實例。于是,原先的實例屬性也就變成了現在的原型屬性了。
function SuperType(){
    this.colors = ["red", "blue", "green"];
}

function SubType(){            
}

// 繼承了 SuperType
SubType.prototype = new SuperType();

var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors);    //"red,blue,green,black"

var instance2 = new SubType();
alert(instance2.colors);    //"red,blue,green,black"

上面例子中,SuperType構造函數定義了一個colors屬性,SuperType的每個實例都會有各自包含自己數組的colors屬性。當SubType通過原型鏈繼承了SuperType之后,SubType.prototype就變成了SuperType的一個實例,因此它也擁有了一個它自己的colors屬性 —— 就跟專門創(chuàng)建了一個SubType.prototype.colors屬性一樣。結果SubType的所有實例都會共享這一個colors屬性。

原型鏈的第二個問題是:在創(chuàng)建子類型的實例時,不能向超類型的構造函數中傳遞參數。實際上,應該說是沒有辦法在不影響所有對象實例的情況下,給超類型的構造函數傳遞參數。

鑒于這兩個問題,實踐中很少會單獨使用原型鏈來實現繼承。

3、作用域與作用域鏈

3.1、作用域

一個變量的作用域是程序源代碼中定義這個變量的區(qū)域。

在之前,JavaScript是沒有塊級作用域的,并且還會出現變量提升的問題,導致內層變量可能會覆蓋外層變量和用來計數的循環(huán)變量泄露為全局變量等問題,并且還出現了閉包的問題。
但是……但是!ES6 新增了let命令,let聲明的變量,只在代碼塊內有效,并且也不存在變量提升。塊級作用域的出現,實際上使得獲得廣泛應用的匿名立即執(zhí)行函數(匿名 IIFE)不再必要了。

變量的作用域有全局作用域和函數作用域,還有ES6新增的塊級作用域。

  • 全局作用域:全局變量擁有全局作用域,在任何地方都是有定義的。
// 全局作用域
var b = 10;
function a(){
  console.log('函數內的b:' + b);  // 函數內的b:10
}
a();
console.log('函數外的b:' + b);  // 函數外的b:10
  • 函數作用域:變量在聲明他們的函數體以及這個函數體嵌套的任意函數體內都是有定義的。
// 函數作用域
function a(){
  var b = 10;
  console.log(b);  // 10
}
a();
console.log(b);  // Uncaught ReferenceError: b is not defined

// 函數作用域,變量聲明提升
function a(){
  console.log(b);  // undefined
  var b = 10;
}
a();
console.log(b);  // Uncaught ReferenceError: b is not defined

// 相當于執(zhí)行了以下代碼
function a(){
  var b;
  console.log(b);  // undefined
  b = 10;
}
a();
console.log(b);  // Uncaught ReferenceError: b is not defined
  • 塊級作用域:花括號內的變量有其自己的作用域,而且變量在聲明它們的代碼段之外是不可見的。
// 塊級作用域
function a(){
  let b = 10;
  if (true) {
    let b = 20;
    console.log(b);  // 20
  }
  console.log(b);  // 10
}
a();

在以上代碼中,分別用let在不同的花括號內聲明了b,但是最終外層代碼塊不受內層代碼塊的影響。如果兩次都使用var定義變量,則兩個輸出的值都是20

3.2、作用域鏈

當代碼在一個環(huán)境中執(zhí)行時,會創(chuàng)建變量對象的一個作用域鏈。作用域鏈的用途是保證對執(zhí)行環(huán)節(jié)有權訪問的所有變量和函數的有序訪問。作用域鏈的前端,始終都是當前執(zhí)行的代碼所在環(huán)境的變量對象。如果這個環(huán)境是函數,則將其活動對象作為變量對象?;顒訉ο笤谧铋_始的時候只包含一個變量,即arguments對象(這個對象在全局環(huán)境中是不存在的)。作用域鏈中的下一個變量對象來自包含(外部)環(huán)境,而再下一個變量對象則來自下一個包含環(huán)境。這樣,一直延續(xù)到全局執(zhí)行環(huán)境;全局執(zhí)行環(huán)境的變量對象始終都是作用域鏈中的最后一個對象?!禞avaScript高級程序設計》(第3版)

var color = "blue";
function changeColor(){
  if (color === "blue"){
    color = "red";
  } else {
    color = "blue";
  }
}
changeColor();
console.log("Color is now " + color);  // Color is now red

在上面例子中,函數changeColor()的作用域包含兩個對象:它自己的變量對象(其中定義著arguments對象) 和全局環(huán)境的變量對象。最終輸出為red,可見可以在函數內部訪問變量color,就是因為可以在這個作用域鏈中找到它。
此外,在局部作用域中定義的變量可以在局部環(huán)境中與全局變量互換使用,見以下例子:

var color = "blue";

function changeColor(){
  var anotherColor = "red";
  function swapColors(){
    var tempColor = anotherColor;
    anotherColor = color;
    color = tempColor;
    // 這里可以訪問color、anotherColor 和t empColor
  }
  // 這里可以訪問color and anotherColor,但不能訪問 tempColor        
  swapColors();
}
changeColor();
// 這里可以訪問color,但不能訪問anotherColor 和 tempColor
console.log("Color is now " + color);  // Color is now red

以上代碼共涉及3個執(zhí)行環(huán)境:全局環(huán)境、changeColor()的局部環(huán)境和swapColors()的局部環(huán)境。全局環(huán)境中有一個變量color和一個函數changeColor()。changeColor()的局部環(huán)境中有一個名為anotherColor的變量和一個名為swapColors()的函數,但它也可以訪問全局環(huán)境中的變量color。swapColors()的局部環(huán)境中有一個變量tempColor,該變量只能在這個環(huán)境中訪問到。無論全局環(huán)境還是changeColor()的局部環(huán)境都無權訪問tempColor。然而,在swapColors()內部則可以訪問其他兩個環(huán)境中的所有變量,因為那兩個環(huán)境是它的父執(zhí)行環(huán)境。

作用域鏈執(zhí)行環(huán)境

上圖中的矩形表示特定的執(zhí)行環(huán)境。其中,內部環(huán)境可以通過作用域鏈訪問所有的外部環(huán)境,但外部環(huán)境不能訪問內部環(huán)境中的任何變量和函數。這些環(huán)境之間的聯系是線性、有次序的。每個環(huán)境都可以向上搜索作用域鏈,以查詢變量和函數名;但任何環(huán)境都不能通過向下搜索作用域鏈而進入另一個執(zhí)行環(huán)境。對于這個例子中的swapColors()而言,其作用域鏈包括3個對象:swapColors()的變量對象、changeColor()的變量對象和全局變量對象。swapColors()的局部環(huán)境開始時會現在自己的變量對象中搜索變量名和函數名,如果搜索不到則再搜索上一級作用域鏈。changeColor()的作用域鏈中指包含兩個對象:它自己的變量對象和全局變量對象。也就是說,它不能訪問swapColors()的環(huán)境。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 第3章 基本概念 3.1 語法 3.2 關鍵字和保留字 3.3 變量 3.4 數據類型 5種簡單數據類型:Unde...
    RickCole閱讀 5,543評論 0 21
  • ??面向對象(Object-Oriented,OO)的語言有一個標志,那就是它們都有類的概念,而通過類可以創(chuàng)建任意...
    霜天曉閱讀 2,265評論 0 6
  • 什么是原型語言 只有對象,沒有類;對象繼承對象,而不是類繼承類。 “原型對象”是核心概念。原型對象是新對象的模板,...
    zhoulujun閱讀 2,471評論 0 12
  • 由于說謊是迫于某種壓力而進行的行為,因此安慰反應在說謊的時候尤其常見且表現得格外明顯。如果對話的情境存在某種壓力,...
    三刀流之空閱讀 518評論 0 2
  • 1、感恩天使們的守護,讓我一覺睡到6點半!謝謝!謝謝!謝謝! 2、感恩公公做好早飯和午飯,腰痛還給我們做這些!感恩...
    向陽花開_bd33閱讀 152評論 0 0

友情鏈接更多精彩內容