本文來自於個人微信公衆號 —
閃兔網絡工做室
:前端面試整理—Javascipt問題
,轉載請保留連接 ;)javascript
本章節是前端開發者面試問題 - JS 部分的參考答案。 歡迎提出建議和指正!html
請解釋事件委託(event delegation)。前端
請簡述JavaScript
中的this
。html5
請解釋原型繼承(prototypal inheritance)的工做原理。java
說說你對 AMD 和 CommonJS 的瞭解。git
請解釋下面代碼爲何不能用做 IIFE:function foo(){ }();
,須要做出哪些修改才能使其成爲 IIFE?github
null
、undefined
和未聲明變量之間有什麼區別?如何檢查判斷這些狀態值?面試
什麼是閉包(closure),爲何使用閉包?express
請說明.forEach
循環和.map()
循環的主要區別,它們分別在什麼狀況下使用?編程
匿名函數的典型應用場景是什麼?
你如何組織本身的代碼?(使用模塊模式(module pattern)仍是經典繼承(classical inheritance)?)
宿主對象(host objects)和原生對象(native objects)的區別是什麼?
下列語句有什麼區別:function Person(){}
、var person = Person()
和var person = newPerson()
?
.call
和.apply
有什麼區別?
請說明Function.prototype.bind
的用法。
何時會用到document.write()
?
功能檢測(feature detection)、功能推斷(feature inference)和使用 UA 字符串之間有什麼區別?
請儘量詳細地解釋 Ajax。
使用Ajax的優缺點分別是什麼?
請說明 JSONP 的工做原理,它爲何不是真正的 Ajax?
你使用過 JavaScript 模板嗎?用過什麼相關的庫?
請解釋變量提高(hosting)。
請描述事件冒泡。
「attribute」 和 「property」 之間有什麼區別?
爲何擴展 JavaScript 內置對象是很差的作法?
document 中的load
事件和DOMContentLoaded
事件之間的區別是什麼?
==
和===
的區別是什麼?
請解釋關於 JavaScript 的同源策略。
請使下面的語句生效:
請說明三元表達式中「三元」這個詞表明什麼?
什麼是"use strict";
?使用它有什麼優缺點?
建立一個循環,從1迭代到100,3
的倍數時輸出 「fizz」,5
的倍數時輸出 「buzz」,同時爲3
和5
的倍數時輸出"fizzbuzz"。
爲何不要使用全局做用域?
爲何要使用load
事件?這個事件有什麼缺點嗎?你知道一些代替方案嗎,爲何使用它們?
請解釋單頁應用是什麼,如何使其對SEO友好。
你對 Promises 及其 polyfill 的掌握程度如何?
Promise
代替回調函數有什麼優缺點?
用轉譯成 JavaScript 的語言寫 JavaScript 有什麼優缺點?
你使用什麼工具和技巧調試 JavaScript 代碼?
你使用什麼語句遍歷對象的屬性和數組的元素?
請解釋可變對象和不可變對象之間的區別。
請解釋同步和異步函數之間的區別。
什麼是事件循環?調用堆棧和任務隊列之間有什麼區別?
請解釋function foo() {}
和var foo = function() {}
之間foo
的用法上的區別。
使用let
、var
和const
建立變量有什麼區別?
ES6 的類和 ES5 的構造函數有什麼區別?
你能給出一個使用箭頭函數的例子嗎,箭頭函數與其餘函數有什麼不一樣?
在構造函數中使用箭頭函數有什麼好處?
高階函數(higher-order)的定義是什麼?
請給出一個解構(destructuring)對象或數組的例子。
ES6 的模板字符串爲生成字符串提供了很大的靈活性,你能夠舉個例子嗎?
你能舉出一個柯里化函數(curry function)的例子嗎?它有哪些好處?
使用擴展運算符(spread)的好處是什麼,它與使用剩餘參數語句(rest)有什麼區別?
如何在文件之間共用代碼?
什麼狀況下會用到靜態類成員?
事件委託是將事件監聽器添加到父元素,而不是每一個子元素單獨設置事件監聽器。當觸發子元素時,事件會冒泡到父元素,監聽器就會觸發。這種技術的好處是:
內存佔用減小,由於只須要一個父元素的事件處理程序,而沒必要爲每一個後代都添加事件處理程序。
無需從已刪除的元素中解綁處理程序,也無需將處理程序綁定到新元素上。
JavaScript
中的this
。JS 中的this
是一個相對複雜的概念,不是簡單幾句能解釋清楚的。粗略地講,函數的調用方式決定了this
的值。我閱讀了網上不少關於this
的文章,Arnav Aggrawal 寫的比較清楚。this
取值符合如下規則:
在調用函數時使用new
關鍵字,函數內的this
是一個全新的對象。
若是apply
、call
或bind
方法用於調用、建立一個函數,函數內的 this 就是做爲參數傳入這些方法的對象。
當函數做爲對象裏的方法被調用時,函數內的this
是調用該函數的對象。好比當obj.method()
被調用時,函數內的this將綁定到obj
對象。
若是調用函數不符合上述規則,那麼this
的值指向全局對象(global object)。瀏覽器環境下this
的值指向window
對象,可是在嚴格模式下('use strict'
),this
的值爲undefined
。
若是符合上述多個規則,則較高的規則(1號最高,4號最低)將決定this
的值。
若是該函數是 ES2015 中的箭頭函數,將忽略上面的全部規則,this
被設置爲它被建立時的上下文。
想得到更深刻的解釋,請查看他在 Medium 上的文章。
這是一個很是常見的 JavaScript 問題。全部 JS 對象都有一個prototype
屬性,指向它的原型對象。當試圖訪問一個對象的屬性時,若是沒有在該對象上找到,它還會搜尋該對象的原型,以及該對象的原型的原型,依次層層向上搜索,直到找到一個名字匹配的屬性或到達原型鏈的末尾。這種行爲是在模擬經典的繼承,可是與其說是繼承,不如說是委託(delegation)。
它們都是實現模塊體系的方式,直到 ES2015 出現以前,JavaScript 一直沒有模塊體系。CommonJS 是同步的,而AMD(Asynchronous Module Definition)從全稱中能夠明顯看出是異步的。CommonJS 的設計是爲服務器端開發考慮的,而 AMD 支持異步加載模塊,更適合瀏覽器。
我發現 AMD 的語法很是冗長,CommonJS 更接近其餘語言 import 聲明語句的用法習慣。大多數狀況下,我認爲AMD 沒有使用的必要,由於若是把全部 JavaScript 都捆綁進一個文件中,將沒法獲得異步加載的好處。此外,CommonJS 語法上更接近 Node 編寫模塊的風格,在先後端都使用 JavaScript 開發之間進行切換時,語境的切換開銷較小。
我很高興看到 ES2015 的模塊加載方案同時支持同步和異步,咱們終於能夠只使用一種方案了。雖然它還沒有在瀏覽器和Node 中徹底推出,可是咱們可使用代碼轉換工具進行轉換。
function foo(){ }();
,須要做出哪些修改才能使其成爲 IIFE?IIFE(Immediately Invoked Function Expressions)表明當即執行函數。 JavaScript 解析器將 function foo(){ }();
解析成function foo(){ }
和();
。其中,前者是函數聲明;後者(一對括號)是試圖調用一個函數,卻沒有指定名稱,所以它會拋出Uncaught SyntaxError: Unexpected token )
的錯誤。
修改方法是:再添加一對括號,形式上有兩種:(function foo(){ })()
和(function foo(){ }())
。以上函數不會暴露到全局做用域,若是不須要在函數內部引用自身,能夠省略函數的名稱。
你可能會用到 void
操做符:void function foo(){ }();
。可是,這種作法是有問題的。表達式的值是undefined
,因此若是你的 IIFE 有返回值,不要用這種作法。例如:
const foo = void function bar() { return 'foo'; }();console.log(foo); // undefined
null
、undefined
和未聲明變量之間有什麼區別?如何檢查判斷這些狀態值?當你沒有提早使用var
、let
或const
聲明變量,就爲一個變量賦值時,該變量是未聲明變量(undeclaredvariables)。未聲明變量會脫離當前做用域,成爲全局做用域下定義的變量。在嚴格模式下,給未聲明的變量賦值,會拋出ReferenceError
錯誤。和使用全局變量同樣,使用未聲明變量也是很是很差的作法,應當儘量避免。要檢查判斷它們,須要將用到它們的代碼放在try
/catch
語句中。
function foo() { x = 1; // 在嚴格模式下,拋出 ReferenceError 錯誤}foo();console.log(x); // 1
當一個變量已經聲明,但沒有賦值時,該變量的值是undefined
。若是一個函數的執行結果被賦值給一個變量,可是這個函數卻沒有返回任何值,那麼該變量的值是undefined
。要檢查它,須要使用嚴格相等(===
);或者使用typeof
,它會返回'undefined'
字符串。請注意,不能使用非嚴格相等(==
)來檢查,由於若是變量值爲null
,使用非嚴格相等也會返回true
。
var foo;console.log(foo); // undefinedconsole.log(foo === undefined); // trueconsole.log(typeof foo === 'undefined'); // trueconsole.log(foo == null); // true. 錯誤,不要使用非嚴格相等!function bar() {}var baz = bar();console.log(baz); // undefined
null
只能被顯式賦值給變量。它表示空值
,與被顯式賦值 undefined
的意義不一樣。要檢查判斷null
值,須要使用嚴格相等運算符。請注意,和前面同樣,不能使用非嚴格相等(==
)來檢查,由於若是變量值爲undefined
,使用非嚴格相等也會返回true
。
var foo = null;console.log(foo === null); // trueconsole.log(foo == undefined); // true. 錯誤,不要使用非嚴格相等!
做爲一種我的習慣,我從不使用未聲明變量。若是定義了暫時沒有用到的變量,我會在聲明後明確地給它們賦值爲null
。
閉包是函數和聲明該函數的詞法環境的組合。詞法做用域中使用的域,是變量在代碼中聲明的位置所決定的。閉包是即便被外部函數返回,依然能夠訪問到外部(封閉)函數做用域的函數。
爲何使用閉包?
利用閉包實現數據私有化或模擬私有方法。這個方式也稱爲模塊模式(module pattern)。
部分參數函數(partial applications)柯里化(currying).
.forEach
循環和.map()
循環的主要區別,它們分別在什麼狀況下使用?爲了理解二者的區別,咱們看看它們分別是作什麼的。
forEach
遍歷數組中的元素。
爲每一個元素執行回調。
無返回值。
const a = [1, 2, 3];const doubled = a.forEach((num, index) => { // 執行與 num、index 相關的代碼});// doubled = undefined
map
遍歷數組中的元素
經過對每一個元素調用函數,將每一個元素「映射(map)」到一個新元素,從而建立一個新數組。
const a = [1, 2, 3];const doubled = a.map(num => { return num * 2; });// doubled = [2, 4, 6]
.forEach
和.map()
的主要區別在於.map()
返回一個新的數組。若是你想獲得一個結果,但不想改變原始數組,用.map()
。若是你只須要在數組上作迭代修改,用forEach
。
匿名函數能夠在IIFE中使用,來封裝局部做用域內的代碼,以便其聲明的變量不會暴露到全局做用域。
(function() { // 一些代碼。})();
匿名函數能夠做爲只用一次,不須要在其餘地方使用的回調函數。當處理函數在調用它們的程序內部被定義時,代碼具備更好地自閉性和可讀性,能夠省去尋找該處理函數的函數體位置的麻煩。
setTimeout(function() { console.log('Hello world!'); }, 1000);
匿名函數能夠用於函數式編程或 Lodash(相似於回調函數)。
const arr = [1, 2, 3];const double = arr.map(function(el) { return el * 2; });console.log(double); // [2, 4, 6]
我之前使用 Backbone 組織個人模型(model),Backbone 鼓勵採用面向對象的方法——建立 Backbone 模型,併爲其添加方法。
模塊模式仍然是很好的方式,可是如今我使用基於 React/Redux 的 Flux 體系結構,它鼓勵使用單向函數編程的方法。我用普通對象(plain object)表示個人 app 模型,編寫實用純函數去操做這些對象。使用動做(actions)和化簡器(reducers)來處理狀態,就像其餘 Redux 應用同樣。
我儘量避免使用經典繼承。若是非要這麼作,我會堅持這些原則。
原生對象是由 ECMAScript 規範定義的 JavaScript 內置對象,好比String
、Math
、RegExp
、Object
、Function
等等。
宿主對象是由運行時環境(瀏覽器或 Node)提供,好比window
、XMLHTTPRequest
等等。
function Person(){}
、var person = Person()
和varperson = new Person()
?這個問題問得很含糊。我猜這是在考察 JavaScript 中的構造函數(constructor)。從技術上講,functionPerson(){}
只是一個普通的函數聲明。使用 PascalCase 方式命名函數做爲構造函數,是一個慣例。
var person = Person()
將Person
以普通函數調用,而不是構造函數。若是該函數是用做構造函數的,那麼這種調用方式是一種常見錯誤。一般狀況下,構造函數不會返回任何東西,所以,像普通函數同樣調用構造函數,只會返回undefined
賦給用做實例的變量。
var person = new Person()
使用new
操做符,建立Person
對象的實例,該實例繼承自Person.prototype
。另一種方式是使用Object.create
,例如:Object.create(Person.prototype)`。
function Person(name) { this.name = name; }var person = Person('John');console.log(person); // undefinedconsole.log(person.name); // Uncaught TypeError: Cannot read property 'name' of undefinedvar person = new Person('John');console.log(person); // Person { name: "John" }console.log(person.name); // "john"
.call
和.apply
有什麼區別?.call
和.apply
都用於調用函數,第一個參數將用做函數內this的值。然而,.call
接受逗號分隔的參數做爲後面的參數,而.apply
接受一個參數數組做爲後面的參數。一個簡單的記憶方法是,從call
中的 C 聯想到逗號分隔(comma-separated),從apply
中的 A 聯想到數組(array)。
function add(a, b) { return a + b; }console.log(add.call(null, 1, 2)); // 3console.log(add.apply(null, [1, 2])); // 3
Function.prototype.bind
的用法。摘自MDN:
bind()
方法建立一個新的函數, 當被調用時,將其this關鍵字設置爲提供的值,在調用新函數時,在任何提供以前提供一個給定的參數序列。
根據個人經驗,將this
的值綁定到想要傳遞給其餘函數的類的方法中是很是有用的。在React組件中常常這樣作。
document.write()
?document.write()
用來將一串文本寫入由document.open()
打開的文檔流中。當頁面加載後執行document.write()
時,它將調用document.open
,會清除整個文檔(<head>
和<body>
會被移除!),並將文檔內容替換成給定的字符串參數。所以它一般被認爲是危險的而且容易被誤用。
網上有一些答案,解釋了document.write()
被用於分析代碼中,或者當你想包含只有在啓用了 JavaScript 的狀況下才能工做的樣式。它甚至在HTML5樣板代碼中用於並行加載腳本並保持執行順序!可是,我懷疑這些使用緣由是過期的,如今能夠在不使用document.write()
的狀況下實現。若是個人觀點有錯,請糾正我。
功能檢測(feature detection)
功能檢測包括肯定瀏覽器是否支持某段代碼,以及是否運行不一樣的代碼(取決於它是否執行),以便瀏覽器始終可以正常運行代碼功能,而不會在某些瀏覽器中出現崩潰和錯誤。例如:
if ('geolocation' in navigator) { // 可使用 navigator.geolocation} else { // 處理 navigator.geolocation 功能缺失}
Modernizr是處理功能檢測的優秀工具。
功能推斷(feature inference)
功能推斷與功能檢測同樣,會對功能可用性進行檢查,可是在判斷經過後,還會使用其餘功能,由於它假設其餘功能也可用,例如:
if (document.getElementsByTagName) { element = document.getElementById(id); }
很是不推薦這種方式。功能檢測更能保證萬無一失。
UA 字符串
這是一個瀏覽器報告的字符串,它容許網絡協議對等方(network protocol peers)識別請求用戶代理的應用類型、操做系統、應用供應商和應用版本。它能夠經過navigator.userAgent
訪問。 然而,這個字符串很難解析而且極可能存在欺騙性。例如,Chrome 會同時做爲 Chrome 和 Safari 進行報告。所以,要檢測 Safari,除了檢查 Safari 字符串,還要檢查是否存在 Chrome 字符串。不要使用這種方式。
Ajax(asynchronous JavaScript and XML)是使用客戶端上的許多Web技術,建立異步Web應用的一種Web開發技術。藉助Ajax,Web應用能夠異步(在後臺)向服務器發送數據和從服務器檢索數據,而不會干擾現有頁面的顯示和行爲。經過將數據交換層與表示層分離,Ajax容許網頁和擴展Web應用程序動態更改內容,而無需從新加載整個頁面。實際上,如今一般將JSON替換爲XML,由於 JavaScript 對 JSON 有原生支持優點。
XMLHttpRequest
API常常用於異步通訊。此外還有最近流行的fetch
API。
優勢
交互性更好。來自服務器的新內容能夠動態更改,無需從新加載整個頁面。
減小與服務器的鏈接,由於腳本和樣式只須要被請求一次。
狀態能夠維護在一個頁面上。JavaScript 變量和 DOM 狀態將獲得保持,由於主容器頁面未被從新加載。
基本上包括大部分 SPA 的優勢。
缺點
動態網頁很難收藏。
若是 JavaScript 已在瀏覽器中被禁用,則不起做用。
有些網絡爬蟲不執行 JavaScript,也不會看到 JavaScript 加載的內容。
基本上包括大部分 SPA 的缺點。
JSONP(帶填充的JSON)是一種一般用於繞過Web瀏覽器中的跨域限制的方法,由於 Ajax 不容許跨域請求。
JSONP 經過<script>
標籤發送跨域請求,一般使用callback
查詢參數,例如:https://example.com?callback=printData
。 而後服務器將數據包裝在一個名爲printData
的函數中並將其返回給客戶端。
// 文件加載自 https://example.com?callback=printDataprintData({ name: 'Yang Shun' });
客戶端必須在其全局範圍內具備printData
函數,而且在收到來自跨域的響應時,該函數將由客戶端執行。
JSONP 可能具備一些安全隱患。因爲 JSONP 是純 JavaScript 實現,它能夠完成 JavaScript 所能作的一切,所以須要信任 JSONP 數據的提供者。
現現在,跨來源資源共享(CORS) 是推薦的主流方式,JSONP 已被視爲一種比較 hack 的方式。
使用過。Handlebars、Underscore、Lodash、AngularJS和JSX。我不喜歡 AngularJS 中的模板,由於它在指令中大量使用了字符串,而且書寫錯誤會被忽略。JSX是個人新寵,由於它更接近 JavaScript,幾乎沒有什麼學習成本。如今,可使用 ES2015 模板字符串快速建立模板,而不需依賴第三方代碼。
const template = `
可是,請注意上述方法中可能存在的 XSS,由於內容不會被轉義,與模板庫不一樣。
變量提高(hoisting)是用於解釋代碼中變量聲明行爲的術語。使用var
關鍵字聲明或初始化的變量,會將聲明語句「提高」到當前做用域的頂部。 可是,只有聲明纔會觸發提高,賦值語句(若是有的話)將保持原樣。咱們用幾個例子來解釋一下。
// 用 var 聲明獲得提高console.log(foo); // undefinedvar foo = 1;console.log(foo); // 1// 用 let/const 聲明不會提高console.log(bar); // ReferenceError: bar is not definedlet bar = 2;console.log(bar); // 2
函數聲明會使函數體提高,但函數表達式(以聲明變量的形式書寫)只有變量聲明會被提高。
// 函數聲明console.log(foo); // [Function: foo]foo(); // 'FOOOOO'function foo() { console.log('FOOOOO'); }console.log(foo); // [Function: foo]// 函數表達式console.log(bar); // undefinedbar(); // Uncaught TypeError: bar is not a functionvar bar = function() { console.log('BARRRR'); };console.log(bar); // [Function: bar]
當一個事件在DOM元素上觸發時,若是有事件監聽器,它將嘗試處理該事件,而後事件冒泡到其父級元素,併發生一樣的事情。最後直到事件到達祖先元素。事件冒泡是實現事件委託的原理(event delegation)。
「Attribute」 是在 HTML 中定義的,而 「property」 是在 DOM 上定義的。爲了說明區別,假設咱們在 HTML 中有一個文本框:<input type="text" value="Hello">
。
const input = document.querySelector('input');console.log(input.getAttribute('value')); // Helloconsole.log(input.value); // Hello
可是在文本框中鍵入「 World!」後:
console.log(input.getAttribute('value')); // Helloconsole.log(input.value); // Hello World!
擴展 JavaScript 內置(原生)對象意味着將屬性或方法添加到其prototype
中。雖然聽起來很不錯,但事實上這樣作很危險。想象一下,你的代碼使用了一些庫,它們經過添加相同的 contains 方法來擴展Array.prototype
,若是這兩個方法的行爲不相同,那麼這些實現將會相互覆蓋,你的代碼將不能正常運行。
擴展內置對象的惟一使用場景是建立 polyfill,本質上爲老版本瀏覽器缺失的方法提供本身的實現,該方法是由JavaScript 規範定義的。
load
事件和DOMContentLoaded
事件之間的區別是什麼?當初始的 HTML 文檔被徹底加載和解析完成以後,DOMContentLoaded
事件被觸發,而無需等待樣式表、圖像和子框架的完成加載。
window
的load
事件僅在DOM和全部相關資源所有完成加載後纔會觸發。
做者:小碼哥的freestyle