前端面試比較好的回答

腹黑的可樂發表於2022-12-23

手寫題:實現柯里化

預先設定一些引數

柯里化是什麼:是指這樣一個函式,它接收函式 A,並且能返回一個新的函式,這個新的函式能夠處理函式 A 的剩餘引數

function createCurry(func, args) {
  var argity = func.length;
  var args = args || [];

  return function () {
    var _args = [].slice.apply(arguments);
    args.push(..._args);

    if (args.length < argity) {
      return createCurry.call(this, func, args);
    }

    return func.apply(this, args);
  }
}

程式碼輸出結果

function Dog() {
  this.name = 'puppy'
}
Dog.prototype.bark = () => {
  console.log('woof!woof!')
}
const dog = new Dog()
console.log(Dog.prototype.constructor === Dog && dog.constructor === Dog && dog instanceof Dog)

輸出結果:true

解析: 因為constructor是prototype上的屬性,所以dog.constructor實際上就是指向Dog.prototype.constructor;constructor屬性指向建構函式。instanceof而實際檢測的是型別是否在例項的原型鏈上。

constructor是prototype上的屬性,這一點很容易被忽略掉。constructor和instanceof 的作用是不同的,感性地來說,constructor的限制比較嚴格,它只能嚴格對比物件的建構函式是不是指定的值;而instanceof比較鬆散,只要檢測的型別在原型鏈上,就會返回true。

寫程式碼:實現函式能夠深度克隆基本型別

淺克隆:

function shallowClone(obj) {
  let cloneObj = {};

  for (let i in obj) {
    cloneObj[i] = obj[i];
  }

  return cloneObj;
}

深克隆:

  • 考慮基礎型別
  • 引用型別

    • RegExp、Date、函式 不是 JSON 安全的
    • 會丟失 constructor,所有的建構函式都指向 Object
    • 破解迴圈引用
function deepCopy(obj) {
  if (typeof obj === 'object') {
    var result = obj.constructor === Array ? [] : {};

    for (var i in obj) {
      result[i] = typeof obj[i] === 'object' ? deepCopy(obj[i]) : obj[i];
    }
  } else {
    var result = obj;
  }

  return result;
}

說一下原型鏈和原型鏈的繼承吧

  • 所有普通的 [[Prototype]] 鏈最終都會指向內建的 Object.prototype,其包含了 JavaScript 中許多通用的功能
  • 為什麼能建立 “類”,藉助一種特殊的屬性:所有的函式預設都會擁有一個名為 prototype 的共有且不可列舉的屬性,它會指向另外一個物件,這個物件通常被稱為函式的原型
function Person(name) {
  this.name = name;
}

Person.prototype.constructor = Person
  • 在發生 new 建構函式呼叫時,會將建立的新物件的 [[Prototype]] 連結到 Person.prototype 指向的物件,這個機制就被稱為原型鏈繼承
  • 方法定義在原型上,屬性定義在建構函式上
  • 首先要說一下 JS 原型和例項的關係:每個建構函式 (constructor)都有一個原型物件(prototype),這個原型物件包含一個指向此建構函式的指標屬性,透過 new 進行建構函式呼叫生成的例項,此例項包含一個指向原型物件的指標,也就是透過 [[Prototype]] 連結到了這個原型物件
  • 然後說一下 JS 中屬性的查詢:當我們試圖引用例項物件的某個屬性時,是按照這樣的方式去查詢的,首先查詢例項物件上是否有這個屬性,如果沒有找到,就去構造這個例項物件的建構函式的 prototype 所指向的物件上去查詢,如果還找不到,就從這個 prototype 物件所指向的建構函式的 prototype 原型物件上去查詢
  • 什麼是原型鏈:這樣逐級查詢形似一個鏈條,且透過 [[Prototype]] 屬性連結,所以被稱為原型鏈
  • 什麼是原型鏈繼承,類比類的繼承:當有兩個建構函式 A 和 B,將一個建構函式 A 的原型物件的,透過其 [[Prototype]] 屬性連結到另外一個 B 建構函式的原型物件時,這個過程被稱之為原型繼承。

標準答案更正確的解釋

什麼是原型鏈?

當物件查詢一個屬性的時候,如果沒有在自身找到,那麼就會查詢自身的原型,如果原型還沒有找到,那麼會繼續查詢原型的原型,直到找到 Object.prototype 的原型時,此時原型為 null,查詢停止。
這種透過 透過原型連結的逐級向上的查詢鏈被稱為原型鏈

什麼是原型繼承?

一個物件可以使用另外一個物件的屬性或者方法,就稱之為繼承。具體是透過將這個物件的原型設定為另外一個物件,這樣根據原型鏈的規則,如果查詢一個物件屬性且在自身不存在時,就會查詢另外一個物件,相當於一個物件可以使用另外一個物件的屬性和方法了。

程式碼輸出問題

function Parent() {
    this.a = 1;
    this.b = [1, 2, this.a];
    this.c = { demo: 5 };
    this.show = function () {
        console.log(this.a , this.b , this.c.demo );
    }
}

function Child() {
    this.a = 2;
    this.change = function () {
        this.b.push(this.a);
        this.a = this.b.length;
        this.c.demo = this.a++;
    }
}

Child.prototype = new Parent();
var parent = new Parent();
var child1 = new Child();
var child2 = new Child();
child1.a = 11;
child2.a = 12;
parent.show();
child1.show();
child2.show();
child1.change();
child2.change();
parent.show();
child1.show();
child2.show();

輸出結果:

parent.show(); // 1  [1,2,1] 5

child1.show(); // 11 [1,2,1] 5
child2.show(); // 12 [1,2,1] 5

parent.show(); // 1 [1,2,1] 5

child1.show(); // 5 [1,2,1,11,12] 5

child2.show(); // 6 [1,2,1,11,12] 5

這道題目值得神帝,他涉及到的知識點很多,例如this的指向、原型、原型鏈、類的繼承、資料型別等。

解析:

  1. parent.show(),可以直接獲得所需的值,沒啥好說的;
  2. child1.show(),Child的建構函式原本是指向Child的,題目顯式將Child類的原型物件指向了Parent類的一個例項,需要注意Child.prototype指向的是Parent的例項parent,而不是指向Parent這個類。
  3. child2.show(),這個也沒啥好說的;
  4. parent.show(),parent是一個Parent類的例項,Child.prorotype指向的是Parent類的另一個例項,兩者在堆記憶體中互不影響,所以上述操作不影響parent例項,所以輸出結果不變;
  5. child1.show(),child1執行了change()方法後,發生了怎樣的變化呢?
  6. this.b.push(this.a),由於this的動態指向特性,this.b會指向Child.prototype上的b陣列,this.a會指向child1a屬性,所以Child.prototype.b變成了[1,2,1,11];
  7. this.a = this.b.length,這條語句中this.athis.b的指向與上一句一致,故結果為child1.a變為4;
  8. this.c.demo = this.a++,由於child1自身屬性並沒有c這個屬性,所以此處的this.c會指向Child.prototype.cthis.a值為4,為原始型別,故賦值操作時會直接賦值,Child.prototype.c.demo的結果為4,而this.a隨後自增為5(4 + 1 = 5)。
  9. child2執行了change()方法, 而child2child1均是Child類的例項,所以他們的原型鏈指向同一個原型物件Child.prototype,也就是同一個parent例項,所以child2.change()中所有影響到原型物件的語句都會影響child1的最終輸出結果。
  10. this.b.push(this.a),由於this的動態指向特性,this.b會指向Child.prototype上的b陣列,this.a會指向child2a屬性,所以Child.prototype.b變成了[1,2,1,11,12];
  11. this.a = this.b.length,這條語句中this.athis.b的指向與上一句一致,故結果為child2.a變為5;
  12. this.c.demo = this.a++,由於child2自身屬性並沒有c這個屬性,所以此處的this.c會指向Child.prototype.c,故執行結果為Child.prototype.c.demo的值變為child2.a的值5,而child2.a最終自增為6(5 + 1 = 6)。

程式碼輸出結果

function a(xx){
  this.x = xx;
  return this
};
var x = a(5);
var y = a(6);

console.log(x.x)  // undefined
console.log(y.x)  // 6

輸出結果: undefined 6

解析:

  1. 最關鍵的就是var x = a(5),函式a是在全域性作用域呼叫,所以函式內部的this指向window物件。所以 this.x = 5 就相當於:window.x = 5。之後 return this,也就是說 var x = a(5) 中的x變數的值是window,這裡的x將函式內部的x的值覆蓋了。然後執行console.log(x.x), 也就是console.log(window.x),而window物件中沒有x屬性,所以會輸出undefined。
  2. 當指向y.x時,會給全域性變數中的x賦值為6,所以會列印出6。

參考 前端進階面試題詳細解答

程式碼輸出結果

var length = 10;
function fn() {
    console.log(this.length);
}

var obj = {
  length: 5,
  method: function(fn) {
    fn();
    arguments[0]();
  }
};

obj.method(fn, 1);

輸出結果: 10 2

解析:

  1. 第一次執行fn(),this指向window物件,輸出10。
  2. 第二次執行arguments[0],相當於arguments呼叫方法,this指向arguments,而這裡傳了兩個引數,故輸出arguments長度為2。

程式碼輸出結果

var friendName = 'World';
(function() {
  if (typeof friendName === 'undefined') {
    var friendName = 'Jack';
    console.log('Goodbye ' + friendName);
  } else {
    console.log('Hello ' + friendName);
  }
})();

輸出結果:Goodbye Jack

我們知道,在 JavaScript中, Function 和 var 都會被提升(變數提升),所以上面的程式碼就相當於:

var name = 'World!';
(function () {
    var name;
    if (typeof name === 'undefined') {
        name = 'Jack';
        console.log('Goodbye ' + name);
    } else {
        console.log('Hello ' + name);
    }
})();

這樣,答案就一目瞭然了。

程式碼輸出結果

Promise.resolve().then(() => {
    console.log('1');
    throw 'Error';
}).then(() => {
    console.log('2');
}).catch(() => {
    console.log('3');
    throw 'Error';
}).then(() => {
    console.log('4');
}).catch(() => {
    console.log('5');
}).then(() => {
    console.log('6');
});

執行結果如下:

1 
3 
5 
6

在這道題目中,我們需要知道,無論是thne還是catch中,只要throw 丟擲了錯誤,就會被catch捕獲,如果沒有throw出錯誤,就被繼續執行後面的then。

程式碼輸出結果

function runAsync (x) {
    const p = new Promise(r => setTimeout(() => r(x, console.log(x)), 1000))
    return p
}

Promise.all([runAsync(1), runAsync(2), runAsync(3)]).then(res => console.log(res))

輸出結果如下:

1
2
3
[1, 2, 3]

首先,定義了一個Promise,來非同步執行函式runAsync,該函式傳入一個值x,然後間隔一秒後列印出這個x。

之後再使用Promise.all來執行這個函式,執行的時候,看到一秒之後輸出了1,2,3,同時輸出了陣列[1, 2, 3],三個函式是同步執行的,並且在一個回撥函式中返回了所有的結果。並且結果和函式的執行順序是一致的。

程式碼輸出結果

function Foo(){
    Foo.a = function(){
        console.log(1);
    }
    this.a = function(){
        console.log(2)
    }
}

Foo.prototype.a = function(){
    console.log(3);
}

Foo.a = function(){
    console.log(4);
}

Foo.a();
let obj = new Foo();
obj.a();
Foo.a();

輸出結果:4 2 1

解析:

  1. Foo.a() 這個是呼叫 Foo 函式的靜態方法 a,雖然 Foo 中有優先順序更高的屬性方法 a,但 Foo 此時沒有被呼叫,所以此時輸出 Foo 的靜態方法 a 的結果:4
  2. let obj = new Foo(); 使用了 new 方法呼叫了函式,返回了函式例項物件,此時 Foo 函式內部的屬性方法初始化,原型鏈建立。
  3. obj.a() ; 呼叫 obj 例項上的方法 a,該例項上目前有兩個 a 方法:一個是內部屬性方法,另一個是原型上的方法。當這兩者都存在時,首先查詢 ownProperty ,如果沒有才去原型鏈上找,所以呼叫例項上的 a 輸出:2
  4. Foo.a() ; 根據第2步可知 Foo 函式內部的屬性方法已初始化,覆蓋了同名的靜態方法,所以輸出:1

程式碼輸出結果

function foo(something){
    this.a = something
}

var obj1 = {}

var bar = foo.bind(obj1);
bar(2);
console.log(obj1.a); // 2

var baz = new bar(3);
console.log(obj1.a); // 2
console.log(baz.a); // 3

輸出結果: 2 2 3

這道題目和上面題目差不多,主要都是考察this繫結的優先順序。記住以下結論即可:this繫結的優先順序:new繫結 > 顯式繫結 > 隱式繫結 > 預設繫結。

程式碼輸出結果

function fn1(){
  console.log('fn1')
}
var fn2

fn1()
fn2()

fn2 = function() {
  console.log('fn2')
}

fn2()

輸出結果:

fn1
Uncaught TypeError: fn2 is not a function
fn2

這裡也是在考察變數提升,關鍵在於第一個fn2(),這時fn2仍是一個undefined的變數,所以會報錯fn2不是一個函式。

JS 隱式轉換,顯示轉換

一般非基礎型別進行轉換時會先呼叫 valueOf,如果 valueOf 無法返回基本型別值,就會呼叫 toString

字串和數字

  • "+" 運算子,如果有一個為字串,那麼都轉化到字串然後執行字串拼接
  • "-" 運算子,轉換為數字,相減 (-a, a * 1 a/1) 都能進行隱式強制型別轉換
[] + {} 和 {} + []

布林值到數字

  • 1 + true = 2
  • 1 + false = 1

轉換為布林值

  • for 中第二個
  • while
  • if
  • 三元表示式
  • || (邏輯或) && (邏輯與)左邊的運算元

符號

  • 不能被轉換為數字
  • 能被轉換為布林值(都是 true)
  • 可以被轉換成字串 "Symbol(cool)"

寬鬆相等和嚴格相等

寬鬆相等允許進行強制型別轉換,而嚴格相等不允許

字串與數字

轉換為數字然後比較

其他型別與布林型別

  • 先把布林型別轉換為數字,然後繼續進行比較

物件與非物件

  • 執行物件的 ToPrimitive(物件)然後繼續進行比較

假值列表

  • undefined
  • null
  • false
  • +0, -0, NaN
  • ""

程式碼輸出結果

var myObject = {
    foo: "bar",
    func: function() {
        var self = this;
        console.log(this.foo);  
        console.log(self.foo);  
        (function() {
            console.log(this.foo);  
            console.log(self.foo);  
        }());
    }
};
myObject.func();

輸出結果:bar bar undefined bar

解析:

  1. 首先func是由myObject呼叫的,this指向myObject。又因為var self = this;所以self指向myObject。
  2. 這個立即執行匿名函式表示式是由window呼叫的,this指向window 。立即執行匿名函式的作用域處於myObject.func的作用域中,在這個作用域找不到self變數,沿著作用域鏈向上查詢self變數,找到了指向 myObject物件的self。

說下對 JS 的瞭解吧

是基於原型的動態語言,主要獨特特性有 this、原型和原型鏈。

JS 嚴格意義上來說分為:語言標準部分(ECMAScript)+ 宿主環境部分

語言標準部分

2015 年釋出 ES6,引入諸多新特性使得能夠編寫大型專案變成可能,標準自 2015 之後以年號代號,每年一更

宿主環境部分

  • 在瀏覽器宿主環境包括 DOM + BOM 等
  • 在 Node,宿主環境包括一些檔案、資料庫、網路、與作業系統的互動等

程式碼輸出結果

var a = 10
var obj = {
  a: 20,
  say: () => {
    console.log(this.a)
  }
}
obj.say() 

var anotherObj = { a: 30 } 
obj.say.apply(anotherObj) 

輸出結果:10 10

我麼知道,箭頭函式時不繫結this的,它的this來自原其父級所處的上下文,所以首先會列印全域性中的 a 的值10。後面雖然讓say方法指向了另外一個物件,但是仍不能改變箭頭函式的特性,它的this仍然是指向全域性的,所以依舊會輸出10。

但是,如果是普通函式,那麼就會有完全不一樣的結果:

var a = 10  
var obj = {  
  a: 20,  
  say(){
    console.log(this.a)  
  }  
}  
obj.say()   
var anotherObj={a:30}   
obj.say.apply(anotherObj)

輸出結果:20 30

這時,say方法中的this就會指向他所在的物件,輸出其中的a的值。

說一下你對盒模型的理解?

CSS3中的盒模型有以下兩種:標準盒模型、IE盒模型
盒模型都是由四個部分組成的,分別是margin、border、padding和content
標準盒模型和IE盒模型的區別在於設定width和height時, 所對應的範圍不同
1、標準盒模型的width和height屬性的範圍只包含了content
2、IE盒模型的width和height屬性的範圍包含了border、padding和content
可以透過修改元素的box-sizing屬性來改變元素的盒模型;
1、box-sizing:content-box表示標準盒模型(預設值)
2、box-sizing:border-box表示IE盒模型(怪異盒模型)

程式碼輸出結果

var obj = { 
  name : 'cuggz', 
  fun : function(){ 
    console.log(this.name); 
  } 
} 
obj.fun()     // cuggz
new obj.fun() // undefined

使用new建構函式時,其this指向的是全域性環境window。

事件迴圈機制 (Event Loop)

事件迴圈機制從整體上告訴了我們 JavaScript 程式碼的執行順序 Event Loop即事件迴圈,是指瀏覽器或Node的一種解決javaScript單執行緒執行時不會阻塞的一種機制,也就是我們經常使用非同步的原理。

先執行 Script 指令碼,然後清空微任務佇列,然後開始下一輪事件迴圈,繼續先執行宏任務,再清空微任務佇列,如此往復。

  • 宏任務:Script/setTimeout/setInterval/setImmediate/ I/O / UI Rendering
  • 微任務:process.nextTick()/Promise

上訴的 setTimeout 和 setInterval 等都是任務源,真正進入任務佇列的是他們分發的任務。

優先順序

  • setTimeout = setInterval 一個佇列
  • setTimeout > setImmediate
  • process.nextTick > Promise
for (const macroTask of macroTaskQueue) {  
  handleMacroTask();    
  for (const microTask of microTaskQueue) {    
      handleMicroTask(microTask);  
  }
}

相關文章