JS Foo.getName筆試題解析,雜談靜態屬性與實例屬性,變量提升,this指向,new一個函數的過程


 壹 ❀ 引

Foo.getName算是一道比較老的面試題了,大致百度了一下在17年就有相關文章在介紹它,遺憾的是我在19年才遇到,比較奇妙的是現在仍有公司會使用這道題。相關解析網上是有的,這里我站在自己的理解做個記錄,也算是相關知識的一次復習,題目如下,輸出過程也直接標出來了:

function Foo() {
    getName = function () {
        console.log(1);
    };
    return this;
};
Foo.getName = function () {
    console.log(2);
};
Foo.prototype.getName = function () {
    console.log(3);
};
var getName = function () {
    console.log(4);
};
function getName() {
    console.log(5);
};

Foo.getName(); //2
getName(); //4
Foo().getName(); //1
getName(); //1
new Foo.getName(); //2
new Foo().getName(); //3
new new Foo().getName(); //3

如果大家搜這個題,那說明肯定是對於某一部分執行是有疑慮,那么現在就跟着我的思路重新理一遍,本文開始:

 貳 ❀ 分析

1.Foo.getName()

為什么輸出2,不是3?這就得說說構造函數的靜態屬性與實例屬性

我們都知道函數屬於對象,而對象擁有可以自由添加屬性的特性,函數也不例外,構造函數也是函數:

function Fn() {};
Fn.person = '聽風是風';
Fn.sayName = function () {
    console.log(this.person);
};
Fn.sayName(); // 聽風是風

比如這個例子中,我為構造函數Fn添加了靜態屬性person靜態方法sayName,我們可以通過構造函數Fn直接訪問。在JS中,我們將綁定在構造函數自身上的屬性方法稱為靜態成員,靜態成員可通過構造函數自身訪問,而實例無法訪問。

let people = new Fn();
people.sayName();// 報錯,實例無法訪問構造函數的靜態屬性/方法

那有什么屬性是實例可以訪問而構造函數自身無法訪問的呢,當然有,比如實例屬性。這里我將實例屬性細分為構造器屬性與原型屬性兩種,看下面的例子:

function Fn() {
    // 構造器屬性
    this.name = '聽風是風';
    this.age = 26;
};
// 原型屬性
Fn.prototype.sayName = function () {
    console.log(this.name);
};
let people = new Fn();
people.sayName(); // 聽風是風

在這個例子中,我們在構造函數Fn中添加了兩條構造器屬性this.namethis.age,此外還在函數外面通過原型添加了一個原型方法sayName

當我們new一個實例后,實例可以直接訪問這些構造器屬性原型屬性,所以這里將兩種屬性統稱為實例屬性實例屬性只有實例才能訪問,構造函數自身無法訪問:

Fn.sayName()// 報錯,找不到此方法

說到這大家有沒有覺得靜態屬性與實例屬性像一對歡喜冤家,靜態屬性只有構造函數自身可以使用,而實例屬性呢只有實例可以使用,兩者看似划清界限,但都由構造函數產生。

那么大家可能又有疑問了,你說實例屬性好歹可以用在繼承上,這靜態屬性取了個高大上的名字也感覺有什么大作用啊,其實是有的,比如JS的Memoization(記憶化)模式:

//Memoization模式
const myFunc = function (param) {
    //do something
    if (!myFunc.cache[param]) {
        myFunc.cache[param] = param * 100;
    };
};
//在函數上添加了一個用於儲存的對象
myFunc.cache = {};
//調用函數
myFunc(1);
//訪問存儲
myFunc.cache[1]; //100

這個例子中,我們為函數添加了一個用於存儲執行結果的對象cache,將每次調用函數的參數作為對象的key,執行結果作為value,對於執行特別復雜的操作,這樣只用執行一次之后就可以直接通過參數訪問到最終結果。

如果對於靜態屬性有興趣,想了解更多可以閱讀博主這篇文章 精讀JavaScript模式(七),命名空間模式,私有成員與靜態成員

2.getName()

為什么輸出4而不是5,這里考的是變量提升與函數聲明提升。我們知道使用var聲明變量會存在變量提升的情況,比如下面的例子中,即使在聲明前使用變量a也不會報錯

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

這是因為聲明提前會讓聲明提升到代碼的最上層,而賦值操作停留在原地,所以上面代碼等同於:

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

而函數聲明(注意是函數聲明,不是函數表達式或者構造函數創建函數)也會存在聲明提前的情況,即我們可以在函數聲明前調用函數:

fn() // 1
function fn() {
    console.log(1);
};
fn() // 1

//因為函數聲明提前,導致函數聲明也會被提到代碼頂端,所以等同於
function fn() {
    console.log(1);
};
fn() // 1
fn() // 1

那這樣就存在一個問題了,變量聲明會提升,函數聲明也會提升,誰提升的更高呢?在你不知道的JavaScript中明確指出,函數聲明會被優先提升,也就是說都是提升,但是函數比變量提升更高,所以題目中的兩個函數順序可以改寫成:

function getName() {
    console.log(5);
};

var getName;

getName = function () {
    console.log(4);
};

這樣就解釋了為什么輸出4而不是5了。想更詳細了解變量提升,函數提升規則,可以閱讀博主這篇文章 【JS點滴】聲明提前,變量聲明提前,函數聲明提前,聲明提前的先后順序

3.Foo().getName()

這里考了全局變量與window的關系以及this指向的問題。

我們知道使用var聲明的全局變量等同於給window添加屬性,以及函數聲明的函數也會成為window屬性

var a = 1;
// window上可以找到這條屬性
window.a; //1

function acfun() {
    console.log(1);
};
// window上可以找到這個方法
window.acfun(); //1

了解了這一點后,我們再來看函數執行過程,第一步執行Foo(),在分析第二個執行時我們知道了getName是全局變量,所以在函數Foo內也能直接訪問,於是getName被修改成了輸出1的函數,之后返回了一個this。

由於Foo().getName()等同於window.Foo().getName(),所以this指向window,這里返回的this其實就是window。

現在執行第二步window.getName(),前面已經說了全局變量等同於給window添加屬性,而且全局變量getName的值在執行Foo()時被修改,所以這里輸出1。

4.getName()

這里輸出1已經毫無懸念,上一分析中,getName的值在Foo執行時被修改了,所以再調用getName一樣等同於window.getName(),同樣是輸出1。

5.new Foo.getName()

在分析一中我們已經知道了Foo.getName是Foo的靜態方法,這里的getName雖然是Foo的靜態方法,但是既沒有繼承Foo的原型,自身內部也沒提供任何構造器屬性(this.name這樣的),所以new這個靜態方法只能得到一個空屬性的實例。

因此這里new的過程就相當於單純把Foo.getName執行了一遍輸出2,然后返回了一個空的實例,我們可以嘗試打印這個執行結果,一個啥都沒繼承的實例:

6.new Foo().getName()

這里考了new基本概念,首先這個調用分為兩步,第一步new Foo()得到一個實例,第二步調用實例的getName方法。

我們知道new一個構造函數的過程大致為,以構造函數原型創建一個對象(繼承原型鏈),調用構造函數並將this指向這個新建的對象,好讓對象繼承構造函數中的構造器屬性,如果構造函數沒有手動返回一個對象,則返回這個新建的對象。

所以在執行new Foo()時,先以Foo原型創建了一個對象,由於Foo.prototype上事先設置了一個getName方法(輸出3的那個),所以這個對象可通過原型訪問到這個方法,其次由於Foo內部也沒提供什么構造器屬性,最終返回了一個this(這個this指向實例),因此這里的this還是等同於我們前面概念提到的以Foo原型創建的對象,可以嘗試輸出這個實例,除了原型上有一個getName方法就沒有其它任何屬性,因此這里輸出3。

我們可以將Foo函數改寫成下面這樣,其它不變,猜猜new Foo().getName()輸出什么:

function Foo() {
    getName = function () {
        console.log(1);
    };
    return {
        getName: function () {
            console.log(6);
        }
    };
};

如果你對於new一個函數過程以及new函數返回值規則不太了解,我在上面的分析應該是會讀的不太理解。如果你存在疑問,可以閱讀博主這兩篇文章:

精讀JavaScript模式(三),new一個構造函數究竟發生了什么?  這篇文章直接看第四、五節的知識。

js new一個對象的過程,實現一個簡單的new方法 這篇文章關於new的過程介紹更為精確。

7.new new Foo().getName()

老實說這個執行給出來真的就是滿滿的惡意,先不說new不new什么的,怎么執行都把人難住,第一眼也是看的我很懵,我們知道new一個函數都是new fn(),函數帶括號的。所以這里其實可以拆分成這樣:

var a = new Foo();
new a.getName();

那這樣就好說了,第一步執行上面已經有分析過了,由於構造函數Foo自身啥構造器屬性都沒有,只有原型上有一個輸出3的原型方法,所以實例a是一個原型上有輸出3的函數getName,除此之外的光桿司令。

那么第二步,由於原型上的getName方法也沒提供構造器屬性,自身原型上也沒屬性,所以第二步也算是單純執行a.getName()輸出3,然后得到了一個什么自定義屬性都沒有實例。

我們可以嘗試輸出這兩步得到的實例:

 叄 ❀ 總

那么到這里這道面試題就分析完了,通過本文,我們知道了構造函數靜態屬性與實例屬性的概念,其中靜態屬性只有構造函數自身可以訪問,實例無權訪問;實例屬性由構造器屬性與原型屬性組成,實例可以繼承訪問,而構造函數卻無權訪問。

其次,我們知道了變量提升與函數聲明提升,而且函數聲明提升比變量提升更高。

還有呢,通過var聲明的全局變量或者函數聲明的函數,都等同於給window添加屬性,我們可以通過window訪問這些屬性,這也是為什么說調用一個Foo()等同於window.Foo()的原因。

最后,我們簡單了解了new一個構造函數的過程,原來new中間發生了這么多有趣的事情。

一道看似普通的面試題,居然涵蓋了不少知識點,我想這也是為何19年還有公司願意使用它的原因吧,不過看過本文的你應該無所畏懼了。

那么到這里本文結束,如有看不懂的地方歡迎留言,我會第一時間回復。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM