動畫:《大前端吊打面試官系列》之原生 JavaScript 精華篇

面試官:說說 JavaScript 中的基本類型有哪些?以及各個數據類型是如何存儲的?

JavaScript 的數據類型包括 原始類型 和 引用類型(對象類型) 。

原始類型包括以下 6 個:

<code>String
Number
Boolean
null
undefined
Symbol
/<code>

引用類型統稱為 Object 類型,如果細分的話,分為以下 5 個:

<code>Object
Array
Date
RegExp
Function
/<code>

1、數據類型的存儲形式

棧(Stack)和堆(Heap),是兩種基本的數據結構。Stack 在內存中自動分配內存空間的;Heap 在內存中動態分配內存空間的,不一定會自動釋放。一般我們在項目中將對象類型手動置為 null 原因,減少無用內存消耗。

原始類型(存在棧內存中)和對象類型(存在堆內存中)分別在內存中的存在形式如下圖示:

動畫:《大前端吊打面試官系列》之原生 JavaScript 精華篇

原始類型是按值形式存放在 中的數據段,內存空間可以自由分配,同時可以 按值直接訪問

<code>var a = 10;
var b = a;
b = 30;
console.log(a); // 10值
console.log(b); // 30值/<code>

過程圖示:

動畫:《大前端吊打面試官系列》之原生 JavaScript 精華篇

引用類型是存放在 內存中,每個對象在堆內存中有一個引用地址,就像是每個房間都有一個房間號一樣。引用類型在棧中保存的就是這個對象在堆內存的引用地址,我們所說的“房間號”。通過“房間號”可以快速查找到保存在堆內存的對象。

<code>var obj1 = new Object();
var obj2 = obj1;
obj2.name = "小鹿";
console.log(obj1.name); // 小鹿/<code>

過程圖示:

動畫:《大前端吊打面試官系列》之原生 JavaScript 精華篇

2、Null

面試官:為什麼 typeof null 等於 Object?

不同的對象在底層原理的存儲是用二進制表示的,在 javaScript 中,如果二進制的前三位都為 0 的話,系統會判定為是 Object 類型。 null 的存儲二進制是 000 ,也是前三位,所以系統判定 null 為 Object 類型。

擴展:

這個 bug 個第一版的 javaScript 留下來的。俺也進行擴展一下其他的幾個類型標誌位:

3、數據類型的判斷

面試官:typeof 與 instanceof 有什麼區別?

typeof 是一元運算符,同樣返回一個字符串類型。一般用來判斷一個變量是否為空或者是什麼類型。

除了 null 類型以及 Object 類型不能準確判斷外,其他數據類型都可能返回正確的類型。

<code>typeof undefined // 'undefined'
typeof '10' // 'String'
typeof 10 // 'Number'
typeof false // 'Boolean'
typeof Symbol() // 'Symbol'
typeof Function // ‘function'
typeof null\t\t // ‘Object’
typeof [] // 'Object'
typeof {} // 'Object'/<code>

既然 typeof 對對象類型都返回 Object 類型情況的侷限性,我們可以使用 instanceof 來進行判斷 某個對象是不是另一個對象的實例 。返回值的是一個布爾類型。

<code>var a = [];
console.log(a instanceof Array) // true/<code>

instanceof 運算符用來測試一個對象在其原型鏈中是否存在一個構造函數的 prototype 屬性,如果對原型鏈不怎能瞭解,後邊俺會具體的寫到,這裡大體記一下就 OK。

我們再測一下 ES6 中的 class 語法糖是什麼類型。

<code>class A{}
console.log(A instanceof Function) // true/<code>

注意:原型鏈中的 prototype 隨時可以被改動的,改變後的值可能不存在於 object 的原型鏈上, instanceof 返回的值可能就返回 false 。

4、類型轉換

類型轉換通常在面試筆試中出現的比較多,對於類型轉換的一些細節應聘者也是很容易忽略的,所以俺整理的儘量系統一些。 javaScript 是一種弱類型語言,變量不受類型限制,所以在特定情況下我們需要對類型進行轉換。

「類型轉換」分為 顯式類型轉換 和 隱式類型轉換 。每種轉換又分為 原始類型轉換 和 對象類型轉換 。

顯式類型轉換

顯式類型轉換就是我們所說強制類型轉換。

筆試題:其他數據類型轉字符串類型!

對於原始類型來說,轉字符串類型會默認調用 toString() 方法。

數據類型String類型數字轉化為數字對應的字符串true轉化為字符串 "true"null轉化為字符串 "null"undefined轉化為字符串 “undefined”Object轉化為 "[object Object]"

<code>String(123);      // "123"
String(true); // "true"
String(null); // "null"
String(undefined);// "undefined"
String([1,2,3]) // "1,2,3"
String({});\t\t // "[object Object]"/<code>

筆試題:其他數據類型轉布爾類型!

除了特殊的幾個值 ‘’ 、 undefined 、 NAN 、 null 、 false 、 0 轉化為 Boolean 為 false 之外,其他類型值都轉化為 true 。

<code>Boolean('')         // false
Boolean(undefined) // false
Boolean(null) // false
Boolean(NaN) // false
Boolean(false) // false
Boolean(0) // false
Boolean({})\t\t // true
Boolean([])\t\t // true/<code>

筆試題:轉化為數字類型!

數據類型數字類型字符串1) 數字轉化為對應的數字
2) 其他轉化為 NaN布爾類型1) true 轉化為 1
2) false 轉化為 0null0undefinedNaN數組1) 數組為空轉化為 0;
2) 數組只有一個元素轉化為對應元素;
3) 其他轉化為NaN空字符串0

<code>Number(10);        // 10 
Number('10'); // 10
Number(null); // 0
Number(''); // 0
Number(true); // 1
Number(false); // 0
Number([]); // 0
Number([1,2]); // NaN
Number('10a'); // NaN
Number(undefined); // NaN/<code>

筆試題:對象類型轉原始類型!

對象類型在轉原始類型的時候,會調用內置的 valueOf() 和 toString() 方法,這兩個方法是可以進行重寫的。

轉化原始類型分為兩種情況:轉化為 字符串類型 或 其他原始類型 。

<code>toString()
valueOf()
toString()
/<code>

5、四則運算

隱士類型轉化是不需要認為的強制類型轉化, javaScript 自動將類型轉化為需要的類型,所以稱之為隱式類型轉換。

加法運算

加法運算符是在運行時決定,到底是執行相加,還是執行連接。運算數的不同,導致了不同的語法行為,這種現象稱為“重載”。

如果雙方都不是字符串,則將轉化為**數字**或**字符串**。

  • Boolean + Boolean 會轉化為數字相加。
  • Boolean + Number 布爾類型轉化為數字相加。
  • Object + Number 對象類型調用 valueOf ,如果不是 String、Boolean 或者 Number類型,則繼續調用 toString() 轉化為字符串。
<code>true + true  // 2
1 + true // 2
[1] + 3 // '13'/<code>

字符串和字符串以及字符串和非字符串相加都會進行 連接

<code>1 + 'b'     // ‘1b’
false + 'b' // ‘falseb’/<code>

其他運算

其他算術運算符(比如減法、除法和乘法)都不會發生重載。它們的規則是:所有運算子一律轉為數值,再進行相應的數學運算。

<code>1 * '2'  // 2
1 * [] // 0/<code>

6、邏輯運算符

邏輯運算符包括兩種情況,分別為 條件判斷 和 賦值操作 。

條件判斷

<code>&&
||
/<code>
<code>true && true   // true
true && false // false
true || true // true
true || false // true/<code>

賦值操作

  • A && B

首先看 A 的真假, A 為假,返回 A 的值, A 為真返回 B 的值。(不管 B 是啥)

<code>console.log(0 && 1) // 0
console.log(1 && 2) // 2/<code>
  • A || B

首先看 A 的真假, A 為真返回的是 A 的值, A 為假返回的是 B 的值(不管 B 是啥)

<code>console.log(0 || 1) // 1
console.log(1 || 2) // 1/<code>

7、比較運算符

比較運算符在邏輯語句中使用,以判定變量或值是否相等。

面試官:== 和 === 的區別?

對於 === 來說,是嚴格意義上的相等,會比較兩個操作符的類型和值。

  • 如果 X 和 Y 的類型不同,返回 false ;
  • 如果 X 和 Y 的類型相同,則根據下方表格進一步判斷

條件例子返回值undefined === undefinedundefined === undefinedtruenull === nullnull === nulltrueString === String
(當字符串順序和字符完全相等的時候返回 true,否則返回 false)‘a’ === 'a'
'a' === 'aa'true
falseBoolean === Booleantrue === true
true === falsetrue
falseSymbol === Symbol相同的 Symbol 返回 true,
不相同的 Symbol 返回 falseNumber === Number
① 其中一個為 NaN,返回 false
② X 和 Y 值相等,返回 true
③ 0 和 -0,返回 true
④ 其他返回 falseNaN ==== NaN
NaN === 1
3 === 3
+0 === -0false
false
true
true

而對於 == 來說,是非嚴格意義上的相等,先判斷兩個操作符的類型是否相等,如果類型不同,則先進行類型轉換,然後再判斷值是否相等。

  • 如果 X 和 Y 的類型相同,返回 X == Y 的比較結果;
  • 如果 X 和 Y 的類型不同,根據下方表格進一步判斷;

條件例子返回值null == undefinednull == undefinedtrueString == Number,String 轉 Number'2' == 2trueBoolean == Number,Boolean 轉 Numbertrue == 1trueObject == String,Number,Symbol,將 Object 轉化為原始類型再比較值大小[1] == 1
[1] == '1'true
true其他返回 falsefalse

this

面試官:什麼是 this 指針?以及各種情況下的 this 指向問題。

this 就是一個對象。不同情況下 this 指向的不同,有以下幾種情況,(希望各位親自測試一下,這樣會更容易弄懂):

  • 對象調用, this 指向該對象(前邊誰調用 this 就指向誰)。
<code>var obj = {
name:'小鹿',
age: '21',
print: function(){
console.log(this)
console.log(this.name + ':' + this.age)
}
}

// 通過對象的方式調用函數

obj.print(); // this 指向 obj/<code>
  • 直接調用的函數, this 指向的是全局 window 對象。
<code>function print(){
\tconsole.log(this);
}
// 全局調用函數
print(); // this 指向 window/<code>
  • 通過 new 的方式, this 永遠指向新創建的對象。
<code>function Person(name, age){
this.name = name;
this.age = age;
console.log(this);
}

var xiaolu = new Person('小鹿',22); // this = > xaiolu/<code>
  • 箭頭函數中的 this 。

由於箭頭函數沒有單獨的 this 值。箭頭函數的 this 與聲明所在的上下文相同。也就是說調用箭頭函數的時候,不會隱士的調用 this 參數,而是從定義時的函數繼承上下文。

<code>const obj = {
a:()=>{
console.log(this);
}
}
// 對象調用箭頭函數
/<code>

面試官:如何改變 this 的指向?

我們可以通過調用函數的 call、apply、bind 來改變 this 的指向。

<code>var obj = {
name:'小鹿',
age:'22',
adress:'小鹿動畫學編程'
}

function print(){
console.log(this); // 打印 this 的指向
console.log(arguments); // 打印傳遞的參數
}

// 通過 call 改變 this 指向
print.call(obj,1,2,3);

// 通過 apply 改變 this 指向
print.apply(obj,[1,2,3]);

// 通過 bind 改變 this 的指向
let fn = print.bind(obj,1,2,3);
fn();/<code>

對於基本的使用想必各位小夥伴都能掌握,俺就不多廢話,再說一說這三者的共同點和不同點。

共同點:

  • 三者都能改變 this 指向,且第一個傳遞的參數都是 this 指向的對象。
  • 三者都採用的後續傳參的形式。

不同點:

  • call 的傳參是單個傳遞的(試了下數組,也是可以的),而 apply 後續傳遞的參數是 數組形式(傳單個值會報錯) ,而 bind 沒有規定,傳遞值和數組都可以。
  • call 和 apply 函數的執行是直接執行的,而 bind 函數會返回一個函數,然後我們想要調用的時候才會執行。

擴展:如果我們使用上邊的方法改變箭頭函數的 this 指針,會發生什麼情況呢?能否進行改變呢?

由於箭頭函數沒有自己的 this 指針,通過 call() 或 apply() 方法調用一個函數時,只能傳遞參數(不能綁定 this ),他們的第一個參數會被忽略。

new

對於 new 關鍵字,我們第一想到的就是在面向對象中 new 一個實例對象,但是在 JS 中的 new 和 Java 中的 new 的機制不一樣。

一般 Java 中,聲明一個 構造函數 ,通過 new 類名() 來創建一個實例,而這個 構造函數是一種特殊的函數。但是在 JS 中,只要 new 一個函數,就可以 new 一個對象,函數和構造函數沒有任何的區別。

面試官:new 內部發生了什麼過程?可不可以手寫實現一個 new 操作符?

new 的過程包括以下四個階段:

  • 創建一個新對象。
  • 這個新對象的 __proto__ 屬性指向原函數的 prototype 屬性。(即繼承原函數的原型)
  • 將這個新對象綁定到 此函數的 this 上 。
  • 返回新對象,如果這個函數沒有返回其他對象。
<code>// new 生成對象的過程
// 1、生成新對象
// 2、鏈接到原型
// 3、綁定 this
// 4、返回新對象
// 參數:
// 1、Con: 接收一個構造函數
// 2、args:傳入構造函數的參數
function create(Con, ...args){
// 創建空對象
let obj = {};
// 設置空對象的原型(鏈接對象的原型)
obj._proto_ = Con.prototype;
// 綁定 this 並執行構造函數(為對象設置屬性)
let result = Con.apply(obj,args)
// 如果 result 沒有其他選擇的對象,就返回 obj 對象

return result instanceof Object ? result : obj;
}
// 構造函數
function Test(name, age) {
this.name = name
this.age = age
}
Test.prototype.sayName = function () {
console.log(this.name)
}

// 實現一個 new 操作符
const a = create(Test,'小鹿','23')
console.log(a.age)/<code>

面試官:有幾種創建對象的方式,字面量相對於 new 創建對象有哪些優勢?

最常用的創建對象的兩種方式:

  • **new 構造函數 **
  • 字面量

其他創建對象的方式:

  • Object.create()

字面量創建對象的優勢所在:

  • 代碼量更少,更易讀
  • 對象字面量運行速度更快,它們可以在解析的時候被優化。他不會像 new 一個對象一樣,解析器需要順著作用域鏈從當前作用域開始查找,如果在當前作用域找到了名為 Object() 的函數就執行,如果沒找到,就繼續順著作用域鏈往上照,直到找到全局 Object() 構造函數為止。
  • Object() 構造函數可以接收參數,通過這個參數可以把對象實例的創建過程委託給另一個內置構造函數,並返回另外一個對象實例,而這往往不是你想要的。 對於 Object.create()方式創建對象:
<code>Object.create(proto, [propertiesObject]);/<code>
<code>proto:
propertiesObject:
/<code>

一般用於繼承:

<code>var People = function (name){
this.name = name;
};

People.prototype.sayName = function (){
console.log(this.name);
}

function Person(name, age){
this.age = age;
People.call(this, name); // 使用call,實現了People屬性的繼承
};

// 使用Object.create()方法,實現People原型方法的繼承,並且修改了constructor指向
Person.prototype = Object.create(People.prototype, {
constructor: {
configurable: true,
enumerable: true,
value: Person,
writable: true
}
});

Person.prototype.sayAge = function (){
console.log(this.age);
}

var p1 = new Person('person1', 25);

p1.sayName(); //'person1'
p1.sayAge(); //25/<code>

面試官:new/字面量 與 Object.create(null) 創建對象的區別?

  • new 和 字面量創建的對象的原型指向 Object.prototype ,會繼承 Object 的屬性和方法。
  • 而通過 Object.create(null) 創建的對象,其原型指向 null , null 作為原型鏈的頂端,沒有也不會繼承任何屬性和方法。

閉包

閉包面試中的重點,但是對於很多初學者來說都是懵懵的,所以俺就從最基礎的作用域講起,大佬請繞過。

面試官:什麼是作用域?什麼是作用域鏈?

規定 變量和函數 的可使用範圍叫做作用域。只看定義,挺抽象的,舉個例子

<code>function fn1() {
let a = 1;
}

function fn2() {
let b = 2;
}/<code>

聲明兩個函數,分別創建量兩個私有的作用域(可以理解為兩個封閉容器), fn2 是不能直接訪問私有作用域 fn1 的變量 a 的。同樣的,在 fn1 中不能訪問到 fn2 中的 b 變量的。一個函數就是一個作用域。

動畫:《大前端吊打面試官系列》之原生 JavaScript 精華篇

每個函數都會有一個作用域,查找變量或函數時,由局部作用域到全局作用域依次查找, 這些作用域的集合就稱為作用域鏈。 如果還不是很好理解,俺再舉個例子​:

<code>let a = 1
function fn() {
function fn1() {
function fn2() {
let c = 3;
console.log(a);
}
// 執行 fn2
fn2();

}
// 執行 fn1
fn1();
}
// 執行函數
fn();/<code>

雖然上邊看起來嵌套有點複雜,我們前邊說過,一個函數就是一個私有作用域,根據定義,在 fn2 作用域中打印 a ,首先在自己所在作用域搜索,如果沒有就向上級作用域搜索,直到搜索到全局作用域, a = 1 ,找到了打印出值。整個搜索的過程,就是基於作用域鏈搜索的。

動畫:《大前端吊打面試官系列》之原生 JavaScript 精華篇

面試官:什麼是閉包?閉包的作用?閉包的應用?

很多應聘者喜歡這樣回答,“函數里套一個函數”,但是面試官更喜歡下面的回答,因為可以繼續為你挖坑。

函數執行,形成一個私有的作用域,保護裡邊的私有變量不受外界的干擾,除了 保護 私有變量外,還可以 保存 一些內容,這樣的模式叫做 閉包 。

閉包的作用有兩個, 保護和保存。

保護的應用

  • 團隊開發時,每個開發者把自己的代碼放在一個私有的作用域中,防止相互之間的變量命名衝突;把需要提供給別人的方法,通過 return 或 window.xxx 的方式暴露在全局下。
  • jQuery 的源碼中也是利用了這種保護機制。
  • 封裝私有變量。

保存的應用

  • 選項卡閉包的解決方案。

面試官:循環綁定事件引發的索引什麼問題?怎麼解決這種問題?

<code>// 事件綁定引發的索引問題
var btnBox = document.getElementById('btnBox'),
inputs = btnBox.getElementsByTagName('input')
var len = inputs.length;
for(var i = 0; i < 1en; i++){
inputs[i].onclick = function () {
alert(i)
}
}/<code>

閉包剩餘的部分,俺在之前的文章已經總結過,俺就不復制過來了,直接傳送過去~動畫:什麼是閉包?

原型和原型鏈

面試官:什麼是原型?什麼是原型鏈?如何理解?

原型:每個 JS 對象都有 __proto__ 屬性,這個屬性指向了原型。跟俺去看看,

動畫:《大前端吊打面試官系列》之原生 JavaScript 精華篇

再來一個,

動畫:《大前端吊打面試官系列》之原生 JavaScript 精華篇

我們可以看到,只要是對象類型,都會有這個 __proto__ 屬性,這個屬性指向的也是一個原型對象,原型對象也是對象呀,肯定也會存在一個 __proto__ 屬性。那麼就形成了原型鏈,定義如下:

原型鏈:原型鏈就是多個對象通過 __proto__ 的方式連接了起來。

原型和原型鏈是怎麼來的呢?如果理清原型鏈中的關係呢?

對於原型和原型鏈的前世今生,由於篇幅過大,俺的傳送門~

圖解:告訴面試官什麼是 JS 原型和原型鏈?

PS:下面的看不懂,一定去看文章哦!

再往深處看,他們之間存在複雜的關係,但是這些所謂的負責關係俺已經總結好了,小二上菜

動畫:《大前端吊打面試官系列》之原生 JavaScript 精華篇

這張圖看起來真複雜,但是通過下邊總結的,再來分析這張圖,試試看。

  • 所有的實例的 _proto_ 都指向該構造函數的原型對象( prototype )。
  • 所有的函數(包括構造函數)是 Function() 的實例,所以所有函數的 _proto_ 的都指向 Function() 的原型對象。
  • 所有的原型對象(包括 Function 的原型對象)都是 Object 的實例,所以 _proto_ 都指向 Object (構造函數)的原型對象。而 Object 構造函數的 _proto_ 指向 null 。
  • Function 構造函數本身就是 Function 的實例,所以 _proto_ 指向 Function 的原型對象。

面試官:instanceOf 的原理是什麼?

之前留了一個小問題,總結了上述的原型和原型鏈之後, instanceof 的原理很容易理解。

instanceof 的原理是通過判斷該對象的原型鏈中是否可以找到該構造類型的 prototype 類型。

<code>function Foo(){}
var f1 = new Foo();
console.log(f1 instanceof Foo);// true/<code>
動畫:《大前端吊打面試官系列》之原生 JavaScript 精華篇

繼承

面試官:說一說 JS 中的繼承方式有哪些?以及各個繼承方式的優缺點。

經典繼承(構造函數)

<code>/ 詳細解析
//1、當用調用 call 方法時,this 帶邊 son 。
//2、此時 Father 構造函數中的 this 指向 son。
//3、也就是說 son 有了 colors 的屬性。
//4、每 new 一個 son ,都會產生不同的對象,每個對象的屬性都是相互獨立的。
function Father(){
\tthis.colors = ["red","blue","green"];
}

function Son(){
// this 是通過 new 操作內部的新對象 {} ,
// 此時 Father 中的 this 就是為 Son 中的新對象{}
// 新對象就有了新的屬性,並返回得到 new 的新對象實例
// 繼承了Father,且向父類型傳遞參數
\tFather.call(this);
}

let s = new Son();
console.log(s.color)/<code>

① 基本思想:在子類的構造函數的內部調用父類的構造函數。

② 優點:

  • 保證了原型鏈中引用類型的獨立,不被所有實例共享。
  • 子類創建的時候可以向父類進行傳參。

③ 缺點:

  • 繼承的方法都在構造函數中定義,構造函數不能夠複用了(因為構造函數中存在子類的特殊屬性,所以構造函數中複用的屬性不能複用了)。
  • 父類中定義的方法對於子類型而言是不可見的(子類所有的屬性都定義在父類的構造函數當中)。

組合繼承

<code>function Father(name){
\tthis.name = name;
\tthis.colors = ["red","blue","green"];
}

// 方法定義在原型對象上(共享)
Father.prototype.sayName = function(){
\talert(this.name);
};

function Son(name,age){
// 子類繼承父類的屬性
\tFather.call(this,name); //繼承實例屬性,第一次調用 Father()
// 每個實例都有自己的屬性

\tthis.age = age;
}

// 子類和父類共享的方法(實現了父類屬性和方法的複用)
Son.prototype = new Father(); //繼承父類方法,第二次調用 Father()

// 子類實例對象共享的方法
Son.prototype.sayAge = function(){
\talert(this.age);
}

var instance1 = new Son("louis",5);
instance1.colors.push("black");
console.log(instance1.colors);//"red,blue,green,black"
instance1.sayName();//louis
instance1.sayAge();//5

var instance1 = new Son("zhai",10);
console.log(instance1.colors);//"red,blue,green"
instance1.sayName();//zhai
instance1.sayAge();//10/<code>

① 基本思想:

  • 使用 原型鏈 實現對「原型對象屬性和方法」的繼承。
  • 通過借用 構造函數 來實現對「實例屬性」的繼承。

② 優點:

  • 在原型對象上定義的方法實現了函數的複用。
  • 每個實例都有屬於自己的屬性。

③ 缺點:

  • 組合繼承調用了兩次父類的構造函數,造成了不必要的消耗。

原型繼承

<code>function object(o){
\tfunction F(){}
\tF.prototype = o;
// 每次返回的 new 是不同的
\treturn new F();
}

var person = {
\tfriends : ["Van","Louis","Nick"]
};

// 實例 1
var anotherPerson = object(person);
anotherPerson.friends.push("Rob");

// 實例 2
var yetAnotherPerson = object(person);
yetAnotherPerson.friends.push("Style");

// 都添加至原型對象的屬性(所共享)
alert(person.friends); // "Van,Louis,Nick,Rob,Style"/<code>

**① 基本思想:**創建臨時性的構造函數(無任何屬性),將傳入的對象作為該構造函數的原型對象,然後返回這個新構造函數的實例。

② 淺拷貝:

object 所產生的對象是不相同的,但是原型對象都是 person 對象,所改變存在原型對象的屬性所有生成的實例所共享,不僅被 Person 所擁有,而且被子類生成的實例所共享。

③ **object.create():**在 ECMAScript5 中,通過新增 object.create() 方法規範化了上面的原型式繼承.。

  • 參數一:新對象的原型的對象。
  • 參數二:先對象定義額外的屬性(可選)。

寄生式繼承

<code>function createAnother(original){
\tvar clone = object(original); // 通過調用object函數創建一個新對象
\tclone.sayHi = function(){ // 以某種方式來增強這個對象
\t\talert("hi");
\t};
\treturn clone; //返回這個對象
}/<code>
  • 基本思想:不必為了指定子類型的原型而調用超類型的構造函數(避免第二次調用的構造函數)。
  • 優點:寄生組合式繼承就是為了解決組合繼承中兩次調用構造函數的開銷。

垃圾回收機制

說到 Javascript 的垃圾回收機制,我們要從內存洩漏一步步說起。

面試官:什麼是內存洩漏?為什麼會導致內存洩漏?

不再用到的內存,沒有及時釋放,就叫做內存洩漏。

內存洩漏是指我們已經無法再通過js代碼來引用到某個對象,但垃圾回收器卻認為這個對象還在被引用,因此在回收的時候不會釋放它。導致了分配的這塊內存永遠也無法被釋放出來。如果這樣的情況越來越多,會導致內存不夠用而系統崩潰。

面試官:怎麼解決內存洩漏?說一說 JS 垃圾回收機制的運行機制的原理?

很多編程語言需要手動釋放內存,但是很多開發者喜歡系統提供自動內存管理,減輕程序員的負擔,這被稱為 "垃圾回收機制" 。

之所以會有垃圾回收機制,是因為 js 中的字符串、對象、數組等只有確定固定大小時,才會動態分配內存,只要像這樣動態地分配了內存,最終都要釋放這些內存以便他們能夠被再用,否則, JavaScript 的解釋器將會消耗完系統中所有可用的內存,造成系統崩潰

JavaScript 與其他語言不同,它具有自動垃圾收集機制,執行環境會負責管理代碼執行過程中使用的內存。

兩種垃圾回收策略

找出那些不再繼續使用的變量,然後釋放其內存。垃圾回收器會按照固定的時間間隔,週期性的執行該垃圾回收操作。

共有兩種策略:

  • 標記清除法
  • 引用計數法

標記清除法

垃圾回收器會在運行的時候,會給存儲在內存中的所有變量都加上標記,然後它會去掉環境中變量以及被環境中的變量引用的變量的標記。剩下的就視為即將要刪除的變量,原因是在環境中無法訪問到這些變量了。最後垃圾回收器完成內存清除操作。

它的實現原理就是通過判斷一個變量是否在執行環境中被引用,來進行標記刪除。

引用計數法

引用計數的垃圾收集策略不常用,引用計數的最基本含義就是跟蹤記錄每個值被引用的次數。

當聲明變量並將一個引用類型的值賦值給該變量時,則這個值的引用次數加 1,同一值被賦予另一個變量,該值的引用計數加 1 。當引用該值的變量被另一個值所取代,則引用計數減 1,當計數為 0 的時候,說明無法在訪問這個值了,所有系統將會收回該值所佔用的內存空間。

存在的缺陷:

兩個對象的相互循環引用,在函數執行完成的時候,兩個對象相互的引用計數並未歸 0 ,而是依然佔據內存,無法回收,當該函數執行多次時,內存佔用就會變多,導致大量的內存得不到回收。

最常見的就是在 IE BOM 和 DOM 中,使用的對象並不是 js 對象,所以垃圾回收是基於計數策略的。但是在 IE9 已經將 BOM 和 DOM 真正的轉化為了 js 對象,所以循環引用的問題得到解決。

如何管理內存

雖然說是 js 的內存都是自動管理的,但是對於 js 還是存在一些問題的,最主要的一個問題就是 分配給 Web 瀏覽器的可用內存數量通常比分配給桌面應用程序的少 。

為了能夠讓頁面獲得最好的性能,必須確保 js 變量佔用最少的內存,最好的方式就是將不用的變量引用釋放掉,也叫做 解除引用

  • 對於局部變量來說,函數執行完成離開環境變量,變量將自動解除。
  • 對於全局變量我們需要進行手動解除。(注意:解除引用並不意味被收回,而是將變量真正的脫離執行環境,下一次垃圾回收將其收回)
<code>var a = 20;  // 在堆內存中給數值變量分配空間
alert(a + 100); // 使用內存
var a = null; // 使用完畢之後,釋放內存空間/<code>

補充:因為通過上邊的垃圾回收機制的標記清除法的原理得知,只有與環境變量失去引用的變量才會被標記回收,所用上述例子通過將對象的引用設置為 null ,此變量也就失去了引用,等待被垃圾回收器回收。

深拷貝和淺拷貝

面試官:什麼是深拷貝?什麼是淺拷貝?

上邊在 JavaScript 基本類型中我們說到,數據類型分為 基本類型和引用類型 。對基本類型的拷貝就是對值複製進行一次拷貝,而對於引用類型來說,拷貝的不是值,而是 值的地址 ,最終兩個變量的地址指向的是同一個值。還是以前的例子:

<code>var a = 10;
var b = a;
b = 30;
console.log(a); // 10值
console.log(b); // 30值

var obj1 = new Object();
var obj2 = obj1;
obj2.name = "小鹿";
console.log(obj1.name); // 小鹿/<code>

要想將 obj1 和 obj2 的關係斷開,也就是不讓他指向同一個地址。根據不同層次的拷貝,分為深拷貝和淺拷貝。

  • 淺拷貝: 只進行一層關係的拷貝。
  • 深拷貝: 進行無限層次的拷貝。

面試官:淺拷貝和深拷貝分別如何實現的?有哪幾種實現方式?

  • 自己實現一個淺拷貝:
<code>// 實現淺克隆
function shallowClone(o){
const obj = {};
for(let i in o){
obj[i] = o[i]
}
return obj;
}/<code>
  • 擴展運算符實現:
<code>let a = {c: 1}
let b = {...a}
a.c = 2
console.log(b.c) // 1/<code>
  • Object.assign() 實現
<code>let a = {c: 1}
let b = Object.assign({}, a)
a.c = 2
console.log(b.c) // 1/<code>

對於深拷貝來說,在淺拷貝的基礎上加上遞歸,我們改動上邊自己實現的淺拷貝代碼:

<code>var a1 = {b: {c: {d: 1}};
function clone(source) {
var target = {};
for(var i in source) {
if (source.hasOwnProperty(i)) {
if (typeof source[i] === 'object') {
target[i] = clone(source[i]); // 遞歸
} else {
target[i] = source[i];
}

}
}
return target;
}/<code>

如果功底稍微紮實的小夥伴可以看出上邊深拷貝存在的問題:

  • 參數沒有做檢驗;
  • 判斷對象不夠嚴謹;
  • 沒有考慮到數組,以及 ES6 的 set, map, weakset, weakmap 兼容性。
  • 最嚴重的問題就是遞歸容易爆棧(遞歸層次很深的時候)。
  • 循環引用問題提。
<code>var a = {};
a.a = a;
clone(a); // 會造成一個死循環/<code>

兩種解決循環引用問題的辦法:

  • 暴力破解
  • 循環檢測

還有一個最簡單的實現深拷貝的方式,那就是利用 JSON.parse(JSON.stringify(object)) ,但是也存在一定的侷限性。

<code>function cloneJSON(source) {
return JSON.parse(JSON.stringify(source));
}/<code>

對於這種方法來說,內部的原理實現也是使用的遞歸,遞歸到一定深度,也會出現爆棧問題。但是對於循環引用的問題不會出現,內部的解決方案正是用到了循環檢測。對於詳細的實現一個深拷貝,具體參考文章:[ 深拷貝的終極探索 ]( segmentfault.com/a/119000001… )

異步編程

由於 JavaScript 是單線程的,單線程就意味著阻塞問題,當一個任務執行完成之後才能執行下一個任務。這樣就會導致出現頁面卡死的狀態,頁面無響應,影響用戶的體驗,所以不得不出現了同步和異步的解決方案。

面試官:JS 為什麼是單線程?又帶來了哪些問題呢?

JS 單線程的特點就是同一時刻只能執行一個任。這是由一些與用戶的互動以及操作 DOM 等相關的操作決定了 JS 要使用單線程,否則使用多線程會帶來複雜的同步問題。如果執行同步問題的話,多線程需要加鎖,執行任務造成非常的繁瑣。

雖然 HTML5 標準規定,允許 JavaScript 腳本創建多個線程,但是子線程完全受主線程控制,且不得操作 DOM 。

上述開頭我們也說到了,單線程帶來的問題就是會導致阻塞問題,為了解決這個問題,就不得不涉及 JS 的兩種任務,分別為同步任務和異步任務。

面試官:JS 如何實現異步編程?

最早的解決方案是使用回調函數,回調函數不是直接調用,而是在特定的事件或條件發生時另一方調用的,用於對該事件或條件進行響應。比如 Ajax 回調:

<code>// jQuery 中的 ajax
$.ajax({
type : "post",
url : 'test.json',
dataType : 'json',
success : function(res) {
// 響應成功回調
},
fail: function(err){
// 響應失敗回調
}
}); /<code>

但是如果某個請求存在依賴性,如下:

<code>$.ajax({
type:"post",
success: function(res){//成功回調
//再次異步請求
$.ajax({

type:"post",
url:"...?id=res.id,
success:function(res){
$.ajax({
type:"post",
url:"...?id=res.id,
success:function(){
\t\t\t\t\t\t// 往復循環
}
})
}
})
}
})/<code>

就會形成不斷的循環嵌套,我們稱之為回調地獄。我們可以看出回調地獄有以下缺點:

<code>try catch
return
/<code>

以上有兩個地方俺需要再進一步詳細說明一下:

  • 為什麼不能捕獲異常?

其實這跟 js 的運行機制相關,異步任務執行完成會加入任務隊列,當執行棧中沒有可執行任務了,主線程取出任務隊列中的異步任務併入棧執行,當異步任務執行的時候,捕獲異常的函數已經在執行棧內退出了,所以異常無法被捕獲。

  • 為什麼不能return?

return 只能終止回調的函數的執行,而不能終止外部代碼的執行。

面試官:如何解決回調地獄問題呢?

既然回調函數存在回調地獄問題,那我們如何解決呢?ES6 給我們提供了三種解決方案,分別是 Generator、Promise、async/await(ES7)。

由於這部分涉及到 ES6 部分的知識,這一期是有關 JS 的,所以會在下一期進行延伸,這裡不多涉及。

【留下一個傳送門~】

面試官:說說異步代碼的執行順序?Event Loop 的運行機制是如何的運行的?

上邊我們說到 JS 是單線程且使用同步和異步任務解決 JS 的阻塞問題,那麼異步代碼的執行順序以及 EventLoop 是如何運作的呢?

在深入事件循環機制之前,需要弄懂一下幾個概念:

  • 執行上下文 ( Execution context )
  • 執行棧 ( Execution stack )
  • 微任務 ( micro-task )
  • 宏任務 ( macro-task )

執行上下文

執行上下文是一個抽象的概念,可以理解為是代碼執行的一個環境。JS 的執行上下文分為三種, 全局執行上下文、函數(局部)執行上下文、Eval 執行上下文 。

  • 全局執行上下文 —— 全局執行上下文指的是全局 this 指向的 window ,可以是外部加載的 JS 文件或者本地 <scripe> 標籤中的代碼。/<scripe>
  • 函數執行上下文 —— 函數上下文也稱為局部上下文,每個函數被調用的時候,都會創建一個新的局部上下文。
  • Eval 執行上下文 —— 這個不經常用,所以不多討論。

執行棧

執行棧,就是我們數據結構中的“棧”,它具有“先進後出”的特點,正是因為這種特點,在我們代碼進行執行的時候,遇到一個執行上下文就將其依次壓入執行棧中。

當代碼執行的時候,先執行位於棧頂的執行上下文中的代碼,當棧頂的執行上下文代碼執行完畢就會出棧,繼續執行下一個位於棧頂的執行上下文。

<code>function foo() {
console.log('a');
bar();
console.log('b');
}

function bar() {
console.log('c');
}

foo();/<code>
  • 初始化狀態,執行棧任務為空。
  • foo 函數執行,foo 進入執行棧,輸出 a,碰到函數 bar。
  • 然後 bar 再進入執行棧,開始執行 bar 函數,輸出 c。
  • bar 函數執行完出棧,繼續執行執行棧頂端的函數 foo,最後輸出 c。
  • foo 出棧,所有執行棧內任務執行完畢。
動畫:《大前端吊打面試官系列》之原生 JavaScript 精華篇

宏任務

對於宏任務一般包括:

<code>script
setTimeout
setInterval
setImmediate
I/O
/<code>

微任務

對於微任務一般包括:

<code>Promise
process.nextTick
MutationObserver
/<code>

注意:nextTick 隊列會比 Promie 隊列先執行。

運行機制

以上概念弄明白之後,再來看循環機制是如何運行的呢?以下涉及到的任務執行順序都是靠函數調用棧來實現的。

1)首先,事件循環機制的是從


分享到:


相關文章: