一些經典面試題分析(上)

引用變量賦值傳遞

最新的 ECMAScript 標準定義了 7 種數據類型:javascript

  • 6 種原始類型(即值類型):java

    • Booleangit

    • Nullgithub

    • Undefined數組

    • Number瀏覽器

    • String(在許多語言中,字符串都被看做引用類型,而非原始類型,由於字符串的長度是可變的。ECMAScript 打破了這一傳統。)app

    • Symbol (ECMAScript 6 新定義)函數

  • Object(即引用類型,包括Array、function這些)this

值類型:存儲在棧(stack)中的簡單數據段,也就是說,它們的值直接存儲在變量訪問的位置。spa

引用類型:存儲在堆(heap)中的對象,也就是說,存儲在變量處的值是一個指針(point),指向存儲對象的內存處。

以下圖所示:

圖片描述

//第一題
//看完了前言 那麼就來看題目了
var obj = {n: 1}
var obj2 = obj
obj2.n = 2
console.log(obj.n) // ?   輸出 2, obj和obj2都是引用類型,他們都是在棧裏保存的都是指針,指向了堆中同一個地址,因此修改其中一個另一個也會跟着改變

function fn1(a) {
  a.n = 3
}
fn1(obj)
console.log(obj.n) // ?   輸出 3
/*
這又涉及到JavaScript中函數的傳參機制,主要有兩種狀況
1. 若是參數是值類型
    基本數據類型的變量基本上都是值類型,例如字符串,數值,布爾值等。
    值類型的傳參是值傳遞,函數內對這種類型參數的修改不會影響到原來傳入的值。
2. 若是參數是引用類型
    複合數據類型如對象,數組等都是引用類型。
    引用傳參是引用傳遞,函數內對這種類型參數的修改都會影響到原來傳入的值。
    
因此上面fn1函數調用的時候會將 實參obj賦值給形參a,a與obj都指向堆中同一個地址,此時修改a.n會影響到obj.n的值
*/

function fn2(a) {
  a = {n:4}
}
fn2(obj)
console.log(obj.n) // ?  輸出的仍是 3
/*
函數fn2調用的時候會會將 實參obj賦值給形參a,a與obj都指向堆中同一個地址,可是此時修改了a的指向,是其指向堆中另一個地址,因此最後輸出obj.n的時候不會發生任何變化
*/
// 第二題
var a = {n: 1}
var b = a
a.x = a = {n: 2}
console.log(a) // ? {n:2}
console.log(b) 
/* ? b爲 
{
  n:1,
  x:{
    n:2
  }
}
*/
分析過程以下圖
ps:其實這個分析過程是一個連續的過程,光憑一張圖有點難展現的很好,能說說明的也不是太詳細。

!圖片描述

做用域與做用域鏈

引用自《You-Dont-Know-JShttps://github.com/fishenal/Y...

​ 做用域有兩種常見的模型,一種叫作 詞法做用域 (Lexical Scope),一種叫作動態做用域 (Dynamic Scope)。其中詞法做用域更常見,被大多數語言採用,包括JavaScript。

詞法做用域簡單來講就是代碼在編寫過程當中體現出來的做用範圍. 代碼一旦寫好, 不用執行, 做用範圍就已經肯定好了. 這個就是所謂詞法做用域.

在 js 中詞法做用域規則:

  • 函數容許訪問函數外的數據.

  • 整個代碼結構中只有函數能夠限定做用域.

  • 做用域規則首先使用提高規則分析

  • 若是當前做用規則中有名字了, 就不考慮外面的名字

變量搜索原則

在代碼的運行過程當中, 若是訪問某一個變量,那麼:

  1. 首先在當前鏈上找

    • 若是有,則中止查找

    • 若是沒有, 在 n-1 級( 假定當前做用域爲第n級 )上找( 在函數內部容許訪問定義在函數外部的變量 )

  2. 如此往復, 直到 0 級鏈

    • 若是找到, 則結束尋找, 直接得到該鏈上變量的數據

    • 若是尚未 拋出異常。

// 第一題
var x = 10
function fn() {
  console.log(x)
}
function show(f) {
  var x = 20
  f()
}
show(fn) // ? 輸出10  js中的是詞法做用域。那麼從函數聲明的時候開始一級一級往父級做用域查找,父級做用域中存在x變量。
// 第二題
var fn = function () {
  console.log(fn)
}
fn()   // ? 輸出的是  function(){console.log(fn)}  當前做用域沒有fn變量,因此向父級做用域尋找。

函數的四種調用模式

  1. 函數模式

特徵:就是一個簡單的函數調用,函數名前面沒有任何的引導內容

this在函數模式中的含義: this在函數中表示全局對象,在瀏覽器中是window對象

  1. 方法模式

特徵: 方法必定是依附於一個對象, 將函數賦值給對象的一個屬性, 那麼就成爲了方法.

this在方法模式調用中的含義:表示函數所依附的這個對象

  1. 構造器調用模式

特徵:使用 new 關鍵字, 來引導構造函數.

因爲構造函數只是給 this 添加成員. 沒有作其餘事情. 而方法也能夠完成這個操做, 就 this 而言, 構造函數與方法沒有本質區別.

構造函數中發this與方法中同樣, 表示對象, 可是構造函數中的對象是剛剛建立出來的對象

ps:補充關於構造函數中return關鍵字的補充說明

    • 構造函數中不須要return, 就會默認的return this

    • 若是手動的添加return, 就至關於 return this

    • 若是手動的添加return 基本類型; 無效, 仍是保留原來 返回this

    • 若是手動添加return null; 或return undefiend, 無效

    • 若是手動添加return 對象類型; 那麼原來建立的this就會被丟掉, 返回的是 return後面的對象

    1. 上下文調用模式

    特徵:上下文(Context),就是函數調用所處的環境。上下文調用,也就是自定義設置this的含義。

    常見的就是經過callapplybind調用

    var obj = {
      fn1: function () {
        console.log(this.fn1) // ? 
        console.log(fn1) // ? 
      }
    }
    obj.fn1()
    /*
    obj.fn1()這個調用模式是方法調用模式,函數內的this指向的obj,因此 this.fn1 === obj.fn1 即打印出來的是obj.fn1的函數體
    
    console.log(fn1)  會向上父級做用域查找變量fn1所表明的值,因爲父級做用域並無這個變量,因此會報錯 
    Uncaught ReferenceError: fn1 is not defined
    */

    變量提高

    在js代碼的預解析階段,系統會將全部的變量聲明以及函數聲明提高到其所在的做用域的最頂上,這個過程就是變量提高

    變量提高的特殊狀況

    1. 函數同名
      所有提高,後面的會覆蓋前面的

    2. 函數和變量同名
      只提高函數,忽略掉變量的聲明

    3. 變量的提高是分做用域的

    4. 變量的提高是分段(script標籤)
      當前script標籤中的函數和變量聲明不會被提高到上一個標籤中,只會提高到當前標籤中

    5. 條件式函數聲明(在條件語句中聲明的函數)
      將其當作函數表達式來處理便可

    只會提高函數名,不會提高函數體!

    1. 函數形參在變量提高中表現
      函數的形參的聲明以及賦值過程優先於變量提高,而且不參與變量提高

    原型鏈

    • 原型鏈

    每個對象都有原型屬性,那麼對象的原型屬性也會有原型屬性,因此這樣就造成了一個鏈式結構,咱們稱之爲原型鏈。

    • 屬性搜索原則

    所謂的屬性搜索原則,也就是屬性的查找順序,在訪問對象的成員的時候,會遵循以下的原則:

    1. 首先在當前對象中查找,若是找到,中止查找,直接使用,若是沒有找到,繼續下一步

    2. 在該對象的原型中查找,若是找到,中止查找,直接使用,若是沒有找到,繼續下一步

    3. 在該對象的原型的原型中查找,若是找到,中止查找,直接使用,若是沒有找到,繼續下一步。

    4. 繼續往上查找,直到查找到Object.prototype尚未, 那麼是屬性就返回 undefined,是方法,就報錯xxx is not a function

    // 綜合題
    function Person() {
      getAge = function () {
        console.log(10)
      }
      return this
    }
    
    Person.getAge = function () {
      console.log(20)
    }
    
    Person.prototype.getAge = function () {
      console.log(30)
    }
    
    var getAge = function () {
      console.log(40)
    }
    
    function getAge() {
      console.log(50)
    }
    
    
    Person.getAge() // ?  20   這個很好分析,直接調用Person對象(函數也是對象)的getAge方法
    
    
    getAge() // ?  40    有人會問爲何這個輸出的是40 而不是50 ? 這題考察了函數與變量重名的問題,執行順序能夠看作是 var getAge --> function getAge  -->  getAge = XXX  ,先是變量提高而後再是賦值操做。
    
    
    Person().getAge() // ?  10  
    /*
    考點1:this指向  Person() 這種調用模式稱爲函數調用,this指向window
    考點2:變量的搜索原則,調用Person()的時候會將變量getAge從新賦予新值,可是這個getAge當前做用域沒有,因此向上父級做用域查找,父級做用域是咱們常說的全局做用域,由於Person()的返回值是this--> 即window,因此window.getAge()就會是執行在Person函數內重新賦值的getAge()函數,即輸出 10
    */
    
    getAge() // ?  10   ---->  同上
    
    
    new Person.getAge() // ? 20   
    /*
    考點1:運算優先級,這裏稍微提一嘴,成員訪問(xx.xxx)是比new的運算優先級高的,具體參照mdn  https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Operator_Precedence
    考點2:new 關鍵字會更改this指向,可是這道題目沒有關於this的問題。
    */
    
    new Person().getAge() // ? 30
    /*
    考點1:new 關鍵字通常作了如下三個事情:
            1. 建立一個空對象
            2. 調用構造函數,而且將構造函數中的this賦值爲new出來的對象
            3. 默認的返回剛纔建立好的對象
    考點2:屬性搜索原則 new出來那個Person對象自己是沒有getAge這個方法,因此去Person構造函數的原型上查找。
    */

    後話

    以上只是我我的一些很淺的見解,若有錯誤歡迎指出交流,但願能共同進步。

    相關文章
    相關標籤/搜索