當前位置: 妍妍網 > 碼農

前端面試,無數人倒在了這三座大山

2024-05-29碼農

> 程式設計師編程學習第一站:https://code-nav.cn

今天來來和大家聊聊前端面試中最難的三座大山,分別指:原型與原型鏈作用域及閉包異步和單執行緒💪

原型與原型鏈

說到 原型 ,就不得不提一下 建構函式 ,首先我們看下面一個簡單的例子:

functionDog(name,age){
this.name = name;
this.age = age;
}
let dog1 = new Dog("哈士奇",3);
let dog2 = new Dog("泰迪",2);

首先創造空的物件,再讓 this 指向這個物件,透過 this.name 進行賦值,最終返回 this ,這其實也是 new 一個物件的過程。

其實:

  • let obj = {} let obj = new Object() 的語法糖;

  • let arr = [] let arr = new Array() 的語法糖;

  • function Dog(){...} let Dog = new Fucntion() 的語法糖。

  • 那什麽是原型那?

    在 js 中,所有物件都是 Object 的例項,並繼承 Object.prototype 的內容和方法,但是有一些是隱性的。

    我們來看一下原型的規則:

    var obj = {};
    obj.attribute = "三座大山";
    var arr = [];
    arr.attribute = "三座大山";
    functionfn1 () {}
    fn1.attribute = "三座大山";

    2. 所有的 參照型別 (包括陣列,物件,函式)都有隱性原型內容( proto ), 值也是一個普通的物件。

    console.log(obj.__proto__);

    3. 所有的函式,都有一個 prototype 內容,值也是一個普通的物件。

    console.log(obj.prototype);

    4. 所有的參照型別的 proto 內容值都指向建構函式的 prototype 內容值。

    console.log(obj.__proto__ === Object.prototype); // true

    5. 當試圖獲取物件內容時,如果物件本身沒有這個內容,那就會去他的 proto (prototype)中去尋找。

    functionDog(name){
    this.name = name;
    }
    Dog.prototype.callName = function (){
    console.log(this.name,"wang wang");
    }
    let dog1 = new Dog("Three Mountain");
    dog1.printName = function (){
    console.log(this.name);
    }
    dog1.callName(); // Three Mountain wang wang
    dog1.printName(); // Three Mountain

    原型鏈: 如下圖。

    我找一個內容,首先會在 f.proto 中去找,因為內容值為一個物件,那麽就會去 f.proto.proto 去找,同理如果還沒找到,就會一直向上去尋找,直到結果為 null 為止。這個串起來的鏈即為原型鏈。

    作用域及閉包

    講到作用域,你會想到什麽?

    當然是 執行上下文

    每個函式都有自己的 excution context ,和 variable object 。這些環境用於儲存上下文中的 變量 函式聲明 參數 等。只有函式才能制造作用域。

    PS :for if else 不能創造作用域。

    console.log(a) ; // undefined
    var a = 1;
    //可理解為
    var a;
    console.log(a); // undefined
    a = 1;

    執行 console.log 時, a 只是被聲明出來,並沒有賦值;所以結果當然是 undefined

    本質上來說,在 js 裏 this 是一個指向函式執行環境的 指標

    this 永遠指向 最後呼叫 它的物件,並且在執行時才能獲取值,定義是無法確認他的值。

    var a = {
    name : "A",
    fn : function (){
    console.log (this.name)
    }
    }
    a.fn() // this === a a 呼叫了fn() 所以此時this為a
    a.fn.call ({name : "B"}) // this === {name : "B"} 使用call(),將this的值指定為{name:"B"}
    var fn1 = a.fn
    fn1() // this === window雖然指定fn1 = a.fn,但是呼叫是有window呼叫,所以this 為window


    this 有多種使用場景,下面我會主要介紹 4 個使用場景:

    1. 作為建構函式執行

    functionStudent(name,age{
    this.name = name // this === s
    this.age = age // this === s
    //return this
    }
    var s = new Student("前端開發愛好者",30)

    首先 new 欄位會建立一個空的物件,然後呼叫 apply() 函式,將 this 指向這個空物件。這樣的話,函式內部的 this 就會被空物件代替。

    1. 作為普通函式執行

    functionfn () {
    console.log (this) // this === window
    }
    fn ()

    1. 作為物件內容執行

    var obj = {
    name : "A",
    printName : function () {
    console.log (this.name) // this === obj
    }
    }
    obj.printName ()

    1. call() , apply() , bind() 三個函式可以修改 this 的指向,具體請往下看:

    var name = "小明" , age = "17"
    var obj = {
    name : "安妮",
    objAge : this.age,
    fun : function () {
    console.log ( this.name + "今年" + this.age )
    }
    }
    console.log(obj.objAge) // 17
    obj.fun() // 安妮今年undefined

    var name = "小明" , age = "17"
    var obj = {
    name : "安妮",
    objAge :this.age,
    fun : function (like,dislike{
    console.log (this.name + "今年" + this.age ,"喜歡吃" + like + "不喜歡吃" + dislike)
    }
    }
    var a = { name : "Jay"age : 23 }
    obj.fun.call(a,"蘋果","香蕉"// Jay今年23 喜歡吃蘋果不喜歡吃香蕉
    obj.fun.apply(a,["蘋果","香蕉"]) // Jay今年23 喜歡吃蘋果不喜歡吃香蕉
    obj.fun.bind(a,"蘋果","香蕉")() // Jay今年23 喜歡吃蘋果不喜歡吃香蕉

    首先 call,apply,bind 第一個參數都是 this 指向的物件, call apply 如果第一個參數指向 null undefined 時,那麽 this 會指向 windows 物件。

    call,apply 都是改變上下文中的 this,並且是立即執行的。

    bind 方法可以讓對應的函式想什麽時候呼叫就什麽時候呼叫。

    閉包

    閉包的概念很抽象,看下面的例子你就會理解什麽叫閉包了:

    functiona(){
    var n = 0;
    this.fun = function () {
    n++;
    console.log(n);
    };
    }
    var c = new a();
    c.fun(); //1
    c.fun(); //2

    閉包就是能夠讀取其他函式內部變量的函式。

    在 js 中只有函式內部的子函式才能讀取局部變量。

    所以可以簡單的理解為: 定義在內部函式的函式

    用途主要有兩個:

  • 前面提到的,讀取函式 內部 的變量。

  • 讓變量值始終 保持在記憶體中

  • 異步和單執行緒

    我們先感受下異步。

    console.log("start");
    setTimeout(function () {
    console.log("medium");
    }, 1000);
    console.log("end");

    使用異步後,打印的順序為 start->end->medium 。因為沒有阻塞。

    為什麽會產生異步呢?

    首先因為 js 為 單執行緒 ,也就是說 CPU 同一時間 只能處理 一個事務 。得按順序,一個一個處理。

    如上例所示,

  • 第一步:執行第一行打印 「start」;

  • 第二步:執行 setTimeout,將其中的函式分存起來,等待時間結束後執行;

  • 第三步:執行最後一行,打印 「end」;

  • 第四部:處於空閑狀態,檢視暫存中,是否有可執行的函式;

  • 第五步:執行分存函式。

  • 為什麽 js 引擎是單執行緒?

    js 的主要用途是與 使用者互動 ,以及操作 DOM ,這決定它只能是單執行緒。

    例: 一個執行緒要添加 DOM 節點,一個執行緒要刪減 DOM 節點,容易造成分歧

    為了更好使用多 CPU,H5 提供了 web Worker 標準,允許 js 建立多執行緒,但是子執行緒受到主執行緒控制,而且不得操作 DOM。

    任務列隊

    單執行緒就意味著,所有的任務都要 排隊 ,前一個結束,才會執行後面的任務。

    如果列隊是因為計算量大, CPU 忙不過來,倒也算了。

    但是更多的時候, CPU 是閑置的,因為 IO 裝置處理得很慢,例如 ajax 讀取網路數據。

    js 設計者便想到,主執行緒完全可以不管 IO 裝置,將其掛起,然後執行後面的任務。

    等後面的任務結束掉,在反過頭來處理 掛起 的任務。

    好,我們來梳理一下:

  • 所有的同步任務都在 主執行緒 上執行,形成一個執行棧。

  • 除了主執行緒之外,還存在一個 任務列隊 ,只要任務有了執行結果,就在任務列隊中植入一個時間。

  • 主執行緒完成所有任務,就會 讀取佇列任務 ,並將其執行。

  • 重復上面三步。

  • 只要主執行緒空了,就會讀取任務列隊,這就是 js 的 執行機制 ,也被稱為 event loop (事件迴圈)。


  • 👇🏻 點選下方閱讀原文,獲取魚皮往期編程幹貨。

    往期推薦