JS作用域、作用域鏈、原型、原型鏈

一、作用域

作用域就是變量和函數(shù)可以訪問的范圍,即作用域控制著變量和函數(shù)的可見性和生命周期。

1、全局作用域

任何地方都能訪問到的對(duì)象擁有全局作用域。
1.1 不在任何函數(shù)內(nèi)定義的變量擁有全局作用域。
1.2 未定義直接賦值的變量自動(dòng)聲明為擁有全局作用域。
1.3 window對(duì)象的內(nèi)置屬性擁有全局作用域。比如window.name。

2、局部作用域

局部作用域一般只在固定的代碼片段內(nèi)可訪問到,最常見的例如函數(shù)內(nèi)部,所以在一些地方會(huì)把這種作用域稱為函數(shù)作用域。

3、塊級(jí)作用域

ES6的塊級(jí)作用域,ES5只有全局和局部作用域。ES6引入了塊級(jí)作用域,明確允許在塊級(jí)作用域中聲明函數(shù),let和const都涉及塊級(jí)作用域。塊級(jí)作用域指在if、switch語句、循環(huán)語句等語句塊中定義變量,這意味著變量不能再語句塊之外被訪問。
① var不支持塊級(jí)作用域
在if等語句塊中,定義的變量從屬于該塊所在的作用域,與函數(shù)不同,它們不會(huì)創(chuàng)建新的作用域。
② let和const
為了解決塊級(jí)作用域,ES6 引入了let 和 const關(guān)鍵字,可以聲明一個(gè)塊級(jí)作用域的變量。

4、上下文 VS 作用域

① 首先上下文和作用域是兩個(gè)不同的概念。
② 每個(gè)函數(shù)調(diào)用都有與之相關(guān)的作用域和上下文,從根本來說,作用域是基于函數(shù),而上下文是基于對(duì)象。
③ 作用域是和每次函數(shù)調(diào)用時(shí)變量的訪問有關(guān),并且每次調(diào)用都是獨(dú)立的;上下文是關(guān)鍵字this的值,是調(diào)用當(dāng)前可執(zhí)行代碼的對(duì)象的引用。
④ 執(zhí)行上下文在運(yùn)行時(shí)確定,隨時(shí)可能改變;作用域在定義時(shí)就確定,并且不會(huì)改變。

5、「this」 上下文

this的值是通過當(dāng)前執(zhí)行上下文中保存的作用域來獲取的。
① 上下文通常是取決于一個(gè)函數(shù)如何被調(diào)用,當(dāng)函數(shù)作為對(duì)象的方法被調(diào)用時(shí),this指向調(diào)用方法的對(duì)象。

var obj = {
  test: function () {
    console.log(this == object);
  }
};
object.test(); // true

② 當(dāng)調(diào)用一個(gè)函數(shù)時(shí),通過new操作符創(chuàng)建一個(gè)對(duì)象的實(shí)例,以這種方式調(diào)用時(shí),this指向新創(chuàng)建的實(shí)例對(duì)象。

function test () {
  console.log(this);
}
test(); // window
new test(); // test

③ 當(dāng)調(diào)用一個(gè)未綁定函數(shù),this默認(rèn)指向全局上下文或者瀏覽器中的window對(duì)象,然而如果函數(shù)在嚴(yán)格模式下被執(zhí)行,this默認(rèn)指向undefined。

6、執(zhí)行上下文

① 當(dāng)函數(shù)執(zhí)行時(shí),會(huì)創(chuàng)建一個(gè)稱為執(zhí)行上下文的內(nèi)部對(duì)象(可理解為作用域),一個(gè)執(zhí)行上下文定義了一個(gè)函數(shù)執(zhí)行時(shí)的環(huán)境。(也就是說,執(zhí)行上下文是由js引擎自動(dòng)創(chuàng)建的對(duì)象, 包含對(duì)應(yīng)作用域中的所有變量屬性)。
② 函數(shù)每次執(zhí)行時(shí)對(duì)應(yīng)的執(zhí)行上下文都是獨(dú)一無二的,所以多次調(diào)用一個(gè)函數(shù)會(huì)創(chuàng)建多個(gè)執(zhí)行上下文。
③ 當(dāng)JavaScript文件被瀏覽器載入后,默認(rèn)最先進(jìn)入的是一個(gè)全局的執(zhí)行上下文。當(dāng)在全局上下文中調(diào)用執(zhí)行一個(gè)函數(shù)時(shí),程序流就進(jìn)入該被調(diào)用函數(shù)內(nèi),此時(shí)引擎就會(huì)為該函數(shù)創(chuàng)建一個(gè)新的執(zhí)行上下文,并且將其壓入到執(zhí)行棧頂部(作用域鏈)。瀏覽器總是執(zhí)行位于執(zhí)行棧頂部的當(dāng)前執(zhí)行上下文,一旦執(zhí)行完畢,該執(zhí)行上下文就會(huì)從執(zhí)行棧頂部彈出,并且控制權(quán)將進(jìn)入其下的執(zhí)行上下文。這樣,執(zhí)行棧中的執(zhí)行上下文就會(huì)被依次執(zhí)行并且彈出,直到回到全局的執(zhí)行上下文。執(zhí)行棧用于存儲(chǔ)代碼執(zhí)行期間創(chuàng)建的所有上下文,具有LIFO(Last In First Out后進(jìn)先出,也就是先進(jìn)后出)的特性。

function f1() {
    f2();
    console.log(1);
};

function f2() {
    f3();
    console.log(2);
};

function f3() {
    console.log(3);
};

f1();//3 2 1

二、作用域鏈

當(dāng)聲明一個(gè)函數(shù)時(shí),局部作用域一級(jí)一級(jí)向上包起來,就是作用域鏈。

  1. 當(dāng)執(zhí)行函數(shù)時(shí),總是先從函數(shù)內(nèi)部尋找局部變量。
  2. 如果內(nèi)部找不到,則會(huì)向創(chuàng)建函數(shù)的作用域?qū)ふ?,依次向上?/li>
function first () {
  second();
  function second () {
    third();
      function third () {
        fourth();
        function fourth () {
          // do sth...
        }
      }
  }
}
first();

a. 運(yùn)行示例代碼將會(huì)導(dǎo)致嵌套的函數(shù)被從上到下執(zhí)行,一直到 fourth 函數(shù),此時(shí)作用域鏈從上到下為: fourth, third, second, first, global。
b. fourth 函數(shù)能夠訪問全局變量和任何定義在first,second和third函數(shù)中的變量(和訪問自己的變量一樣)。
c. 一旦fourth函數(shù)執(zhí)行完成,其就會(huì)從作用域鏈頂部移除,并且執(zhí)行權(quán)會(huì)返回到third函數(shù)。這個(gè)過程一直持續(xù)到所有代碼完成執(zhí)行。

在JavaScript中,在函數(shù)里面定義的變量,可以在函數(shù)里面被訪問,但是在函數(shù)外面無法被訪問。在JavaScript中使用變量,JavaScript解釋器首先會(huì)在當(dāng)前作用域中搜索是否有該變量的定義,如有,就使用這個(gè)變量;如沒有,就到父級(jí)作用域中查找該變量,由于變量提升,在實(shí)際開發(fā)的時(shí)候,推薦將變量寫在最開始的地方。

三、原型

JavaScript中說一切皆是對(duì)象,當(dāng)然這句話也不嚴(yán)謹(jǐn),比如null和undefined就不是對(duì)象,除了這倆完全可以說JavaScript中一切皆對(duì)象。而JavaScript對(duì)象有一個(gè)叫做「原型」的公共屬性,屬性名是proto,這個(gè)原型屬性是對(duì)另一個(gè)對(duì)象的引用,通過這個(gè)原型屬性我們就可以訪問另個(gè)對(duì)象的屬性和方法。

無論何時(shí),只要?jiǎng)?chuàng)建了一個(gè)函數(shù),就會(huì)根據(jù)一組特定的規(guī)則為該函數(shù)創(chuàng)建一個(gè)prototype屬性,這個(gè)屬性指向該函數(shù)的原型對(duì)象,所有原型對(duì)象都會(huì)獲得一個(gè)constructor屬性,這個(gè)屬性包含一個(gè)指向prototype屬性所在函數(shù)的指針。例如:

// 創(chuàng)建構(gòu)造函數(shù)
function Person (name, age) {
  this.name = name;
  this.page = page;
  this.sayName = function () {
    console.log(this.name);
  }
}
// 創(chuàng)建實(shí)例
const person1 = new Person('gali', 8);
const person2 = new Person('pig', 29);

person.jpg

如圖,person1和person2都是構(gòu)造函數(shù)Person()的實(shí)例對(duì)象,Person.prototype指向了Person函數(shù)的原型對(duì)象,而Person.prototype.constructor又指向Person函數(shù)。Person的每一個(gè)實(shí)例對(duì)象,都含有一個(gè)內(nèi)部屬性proto,該屬性指向Person.prototype。所以

console.log(Person.prototype.constructor === Person); // true
console.log(person1.__proto__ === Person.prototype); // true
console.log(person2.__proto__ === Person.prototype); // true

使用hasOwnProperty()方法可以檢測(cè)一個(gè)屬性是存在于實(shí)例還是存在于原型中,這個(gè)方法只有給定屬性存在于對(duì)象實(shí)例中才會(huì)返回true。

function Person(){
}

Person.prototype.name = 'zzx';
Person.prototype.age = 22;
Person.prototype.job = 'Programmer';
Person.prototype.sayName = function(){
    console.log(this.name);
}

var person1 = new Person();

console.log(person1.hasOwnProperty('name')); //false
person1.name = 'yzy';
console.log(person1.hasOwnProperty('name')); //true
delete person1.name;
console.log(person1.hasOwnProperty('name')); //false

四、原型鏈

當(dāng)訪問對(duì)象上的屬性時(shí),會(huì)首先查找該對(duì)象上是否含有該屬性,而當(dāng)這個(gè)對(duì)象上沒有這個(gè)屬性時(shí)就會(huì)訪問proto 索引看看它的原型對(duì)象上面有沒有,如果還沒有就繼續(xù)沿著proto原型向上尋找,直到找到為止。這就是原型鏈。所有對(duì)象的原型盡頭是Object.prototype,而Object.prototype的原型指向null。
我們?nèi)粘i_發(fā)中用到的絕大多數(shù)對(duì)象的proto基本不會(huì)直接指向Object.prototype,基本都是指向另一個(gè)對(duì)象,比如所有的函數(shù)的proto都指向Function.prototype,所有數(shù)組的proto都指向Array.prototype。

tim.__proto__ === Man.prototype;
tim.__proto__.__proto__ === Object.prototype;
tim.__proto__.__proto__.__proto__ === null;

var func = function () {};
func.__proto__ == Function.prototype; // true

Function.prototype.__proto__ === Object.prototype; // true

// JS中最亂倫的東西出現(xiàn)了, Function 是自己的老子
Function.__proto__ === Function.prototype; // true

Object.__proto__ === Function.prototype; // true
prototype.png
總結(jié)
  • 所有的函數(shù)都繼承自Function.prototype,F(xiàn)unction、Object 都是函數(shù),所以繼承自Function.prototype。
  • 所有的對(duì)象都直接或間接繼承自O(shè)bject.prototype,F(xiàn)unction.prototype.proto === Object.prototype函數(shù)也是對(duì)象,所以函數(shù)最終繼承自O(shè)bject.prototype。
  • Object.prototype 繼承自 null,萬劍歸宗。
  • proto和constructor屬性 是對(duì)象所獨(dú)有的;prototype屬性數(shù)函數(shù)所獨(dú)有的,因?yàn)楹瘮?shù)也是一種對(duì)象,所以函數(shù)也擁有proto和constructor屬性。
  • prototype屬性的作用就是讓該函數(shù)所實(shí)例化的對(duì)象們都可以找到公用的屬性和方法,即f1.proto === Foo.prototype
  • constructor屬性的含義就是指向該對(duì)象的構(gòu)造函數(shù),所有函數(shù)(此時(shí)看成對(duì)象了)最終的構(gòu)造函數(shù)都指向Function。

五、常考示例

5.1 題1

var F=function(){};

Object.prototype.a=function(){
     console.log('a()')
};

Function.prototype.b=function(){
     console.log('b()')
}

var f  = new F();

f.a() // a()
f.b() // f.b is not a function
F.a() // a()
F.b() // b()

如上,f 實(shí)例對(duì)象的原型指向 F的prototype屬性;F的原型指向Function的prototype屬性;Function.prototype的原型指向了Object.prototype;Object.prototype的原型指向了Null。

f.__proto__ ===  F.prototype
F.__proto__ === Function.prototype
F.prototype.__proto__ === Object.prototype
Function.prototype.__proto__ === Object.prototype

f 實(shí)例對(duì)象本身沒有a屬性,所以會(huì)在F.prototype上查找,原型上如果找到a屬性,就繼承了原型的a屬性;若原型上沒有a屬性,會(huì)繼續(xù)在原型上的原型上查找,直到為null對(duì)象,若還是沒有該屬性,則返回undefined。
綜上,構(gòu)造函數(shù)只是提供創(chuàng)建對(duì)象的模板,而不是原型對(duì)象。所以f.prototype指向的不是構(gòu)造函數(shù),而是構(gòu)造函數(shù)的原型。

六、作用域鏈與原型鏈的區(qū)別

  • 作用域鏈:當(dāng)訪問一個(gè)變量時(shí),JS解釋器首先會(huì)在當(dāng)前作用域查找標(biāo)識(shí)符,如果沒有找到就到父級(jí)作用域去找,作用域鏈頂端是全局對(duì)象window,如果window都沒有這個(gè)變量就報(bào)錯(cuò)。
  • 原型鏈:當(dāng)在對(duì)象上訪問某屬性時(shí),首先會(huì)在這個(gè)對(duì)象本身查找該屬性,如果沒有就順著原型鏈向上找,原型鏈頂端是null,如果全程都沒找到則返一個(gè)undefined,而不是報(bào)錯(cuò)。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容