深刻淺出的前端面試

1、請描述var,let和const的區別

letconstES6新增的命令,用於聲明變量,這兩個命令跟ES5var有許多不一樣,而且letconst也有一些細微的不一樣javascript

varlet/const的區別css

  • 塊級做用域
  • 不存在變量提高
  • 暫時性死區
  • 不可重複聲明
  • let、const聲明的全局變量不會掛在頂層對象下面

const命令兩個注意點:html

  • const 聲明以後必須立刻賦值,不然會報錯
  • const 簡單類型一旦聲明就不能再更改,複雜類型(數組、對象等)指針指向的地址不能更改,內部數據能夠更改

爲何須要塊級做用域?

ES5只有全局做用域和函數做用域,沒有塊級做用域。前端

這帶來不少不合理的場景:vue

  • 內層變量可能覆蓋外層變量
  • 用來計數的循環變量泄露爲全局變量
var tmp = new Date();
function f() {
  console.log(tmp); // 想打印外層的時間做用域
  if (false) {
    var tmp = 'hello world'; // 這裏聲明的做用域爲整個函數
  }
}
f(); // undefined

var s = 'hello';
for (var i = 0; i < s.length; i++) {
  console.log(s[i]); // i應該爲這次for循環使用的變量
}
console.log(i); // 5 全局範圍均可以讀到

複製代碼

塊級做用域

  1. 做用域
function f1() {
  let n = 5;
  if (true) {
    let n = 10;
    console.log(n); // 10 內層的n
  }
  console.log(n); // 5 當前層的n
}
複製代碼
  1. 塊級做用域任意嵌套
{{{{
  {let insane = 'Hello World'}
  console.log(insane); // 報錯 讀不到子做用域的變量
}}}};

複製代碼
  1. 塊級做用域真正使代碼分割成塊了
{
let a = ...;
...
}
{
let a = ...;
...
}

複製代碼

以上形式,能夠用於測試一些想法,不用擔憂變量重名,也不用擔憂外界干擾java

塊級做用域聲明函數:node

在塊級做用域聲明函數,由於瀏覽器的要兼容老代碼,會產生一些問題!react

在塊級做用域聲明函數,最好使用匿名函數的形式。css3

if(true){
  let a = function () {}; // 做用域爲塊級 令聲明的函數做用域範圍更清晰
}

複製代碼

ES6 的塊級做用域容許聲明函數的規則,只在使用大括號的狀況下成立,若是沒有使用大括號,就會報錯。web

// 報錯
'use strict';
if (true)
  function f() {} // 咱們須要給if加個{}

複製代碼

不存在變量提高

變量提高的現象:在同一做用域下,變量能夠在聲明以前使用,值爲undefined

ES5 時使用var聲明變量,常常會出現變量提高的現象。

// var 的狀況
console.log(foo); // 輸出undefined
var foo = 2;

// let 的狀況
console.log(bar); // 報錯ReferenceError
let bar = 2;

複製代碼

暫時性死區:

只要一進入當前做用域,所要使用的變量就已經存在了,可是不可獲取,只有等到聲明變量的那一行代碼出現,才能夠獲取和使用該變量

var tmp = 123; // 聲明
if (true) {
  tmp = 'abc'; // 報錯 由於本區域有tmp聲明變量
  let tmp; // 綁定if這個塊級的做用域 不能出現tmp變量
}
複製代碼

暫時性死區和不能變量提高的意義在於:

在測試時出現這種狀況:var a= '聲明';const a '不報錯',這種狀況是由於babel在轉化的時候,作了一些處理,在瀏覽器的控制檯中測試,就成功報錯

letconst不容許在相同做用域內,重複聲明同一個變量

function func(arg) {
  let arg; // 報錯
}

function func(arg) {
  {
    let arg; // 不報錯
  }
}

複製代碼

let、const聲明的全局變量不會掛在頂層對象下面

  1. 瀏覽器環境頂層對象是: window
  2. node環境頂層對象是: global
  3. var聲明的全局變量會掛在頂層對象下面,而let、const不會掛在頂層對象下面。以下面這個栗子
var a = 1;
// 若是在 Node環境,能夠寫成 global.a
// 或者採用通用方法,寫成 this.a
window.a // 1

let b = 1;
window.b // undefined
複製代碼

const命令

  1. 一旦聲明,必須立刻賦值
let p; var p1; // 不報錯
const p3 = '立刻賦值'
const p3; // 報錯 沒有賦值
複製代碼
  1. const一旦聲明值就不能改變

簡單類型:不能改動

const p = '不能改變';
p = '報錯'
複製代碼

複雜類型:變量指針不能變

考慮以下狀況:

const p = ['不能改動']
const p2 = {
  name: 'OBKoro1'
}
p[0] = '不報錯'
p2.name = '不報錯'
p = ['報錯']
p2 = {
  name: '報錯'
}
複製代碼

const所說的一旦聲明值就不能改變,實際上指的是:變量指向的那個內存地址所保存的數據不得改動

  • 簡單類型(number、string、boolean):內存地址就是值,即常量(一變就報錯)
  • 複雜類型(對象、數組等):地址保存的是一個指針,const只能保證指針是固定的(老是指向同一個地址),它內部的值是能夠改變的(不要覺得const就安全了!)
  • 因此只要不從新賦值整個數組/對象, 由於保存的是一個指針,因此對數組使用的pushshiftsplice等方法也是容許的,你就是把值一個一個全都刪光了都不會報錯。

複雜類型還有函數,正則等,這點也要注意一下。

2、CSS動畫和JS動畫的區別

CSS動畫

優勢:

  1. 瀏覽器能夠對動畫進行優化
    • 瀏覽器使用與 requestAnimationFrame 相似的機制,requestAnimationFrame比起setTimeout,setInterval設置動畫的優點主要是:(1)requestAnimationFrame會把每一幀中的全部DOM操做集中起來,在一次重繪或迴流中就完成,而且重繪或迴流的時間間隔牢牢跟隨瀏覽器的刷新頻率,通常來講,這個頻率爲每秒60幀。(2)在隱藏或不可見的元素中requestAnimationFrame不會進行重繪或迴流,這固然就意味着更少的的cpu,gpu和內存使用量。
    • 強制使用硬件加速 (經過 GPU 來提升動畫性能)
  2. 代碼相對簡單,性能調優方向固定
  3. 對於幀速表現很差的低版本瀏覽器,CSS3能夠作到天然降級,而JS則須要撰寫額外代碼

缺點:

  1. 運行過程控制較弱,沒法附加事件綁定回調函數。CSS動畫只能暫停,不能在動畫中尋找一個特定的時間點,不能在半路反轉動畫,不能變換時間尺度,不能在特定的位置添加回調函數或是綁定回放事件,無進度報告
  2. 代碼冗長。想用 CSS 實現稍微複雜一點動畫,最後CSS代碼都會變得很是笨重。

JS動畫

優勢:

  1. JavaScript動畫控制能力很強, 能夠在動畫播放過程當中對動畫進行控制:開始、暫停、回放、終止、取消都是能夠作到的。
  2. 動畫效果比css3動畫豐富,有些動畫效果,好比曲線運動,衝擊閃爍,視差滾動效果,只有JavaScript動畫才能完成
  3. CSS3有兼容性問題,而JS大多時候沒有兼容性問題

缺點:

  1. JavaScript在瀏覽器的主線程中運行,而主線程中還有其它須要運行的JavaScript腳本、樣式計算、佈局、繪製任務等,對其干擾致使線程可能出現阻塞,從而形成丟幀的狀況。
  2. 代碼的複雜度高於CSS動畫

總結:若是動畫只是簡單的狀態切換,不須要中間過程控制,在這種狀況下,css動畫是優選方案。它可讓你將動畫邏輯放在樣式文件裏面,而不會讓你的頁面充斥 Javascript 庫。然而若是你在設計很複雜的富客戶端界面或者在開發一個有着複雜UI狀態的 APP。那麼你應該使用js動畫,這樣你的動畫能夠保持高效,而且你的工做流也更可控。因此,在實現一些小的交互動效的時候,就多考慮考慮CSS動畫。對於一些複雜控制的動畫,使用javascript比較可靠。

3、請描述下cookies,sessionStorage,loaclStorage的區別

共同點:都是保存在瀏覽器端,且同源的

區別:

  1. cookie數據始終在同源的http請求中攜帶,即cookie在瀏覽器和服務器間來回傳遞
  2. 而sessionStorage和localStorage不會自動把數據發給服務器,僅在本地保存
  3. cookie數據不能超過4k(適合保存小數據),sessionStorage和localStorage容量較大,能夠達到5M或更大,數據有效期不一樣
  4. sessionStorage:僅在當前瀏覽器窗口關閉前有效
  5. localStorage始終有效,窗口或瀏覽器關閉也一直保存,需手動清除
  6. cookie只在設置的cookie過時時間以前一直有效,即便窗口或瀏覽器關閉
  7. sessionStorage不在不一樣的瀏覽器窗口中共享;localStorage 在全部同源窗口中都是共享的
  8. cookie也是在全部同源窗口中都是共享的

4、請描述下JSONP的原理

什麼是跨域?

跨域概念解釋:當前發起請求的域與該請求指向的資源所在的域不同。這裏的域指的是這樣的一個概念:咱們認爲若協議 + 域名 + 端口號均相同,那麼就是同域。 以下表

JSONP原理

利用script標籤沒有跨域限制的漏洞,網頁能夠獲得從其餘來源動態產生的 JSON 數據。JSONP請求必定須要對方的服務器作支持才能夠

JSONP和AJAX對比

JSONP和AJAX相同,都是客戶端向服務器端發送請求,從服務器端獲取數據的方式。但AJAX屬於同源策略,JSONP屬於非同源策略(跨域請求)

JSONP優缺點

優勢:

  1. 不受同源策略的限制
  2. 兼容性更好
  3. 支持老版本瀏覽器

缺點:只支持get請求具備侷限性,,不安全可能會遭受XSS攻擊

JSONP的實現

// index.html
function jsonp({ url, params, callback }) {
  return new Promise((resolve, reject) => {
    let script = document.createElement('script')
    window[callback] = function(data) {
      resolve(data)
      document.body.removeChild(script)
    }
    params = { ...params, callback } // wd=b&callback=show
    let arrs = []
    for (let key in params) {
      arrs.push(`${key}=${params[key]}`)
    }
    script.src = `${url}?${arrs.join('&')}`
    document.body.appendChild(script)
  })
}
jsonp({
  url: 'http://localhost:3000/say',
  params: { wd: 'Iloveyou' },
  callback: 'show'
}).then(data => {
  console.log(data)
})
複製代碼

上面這段代碼至關於向http://localhost:3000/say?wd=Iloveyou&callback=show這個地址請求數據,而後後臺返回show('我不愛你'),最後會運行show()這個函數,打印出'我不愛你'

// server.js
let express = require('express')
let app = express()
app.get('/say', function(req, res) {
  let { wd, callback } = req.query
  console.log(wd) // Iloveyou
  console.log(callback) // show
  res.end(`${callback}('我不愛你')`)
})
app.listen(3000)
複製代碼

5、如何進行網站性能優化

互聯網有一項著名的8秒原則。用戶在訪問Web網頁時,若是時間超過8秒就會感到不耐煩,若是加載須要太長時間,他們就會放棄訪問。

一、資源壓縮與合併

主要包括這些方面:html壓縮、css 壓縮、js的壓縮和混亂和文件合併。

資源壓縮能夠從文件中去掉多餘的字符,好比回車、空格。你在編輯器中寫代碼的時候,會使用縮進和註釋,這些方法無疑會讓你的代碼簡潔並且易讀,但它們也會在文檔中添加多餘的字節。

二、非核心代碼異步加載異步加載的方式

一、異步加載的方式 異步加載的三種方式——asyncdefer、動態腳本建立

① async方式

  • async屬性是HTML5新增屬性,須要Chrome、FireFox、IE9+瀏覽器支持
  • async屬性規定一旦腳本可用,則會異步執行
  • async屬性僅適用於外部腳本
  • 若是是多個腳本,該方法不能保證腳本按順序執行
<script type="text/javascript" src="xxx.js" async="async"></script>
複製代碼

② defer方式

  • 兼容全部瀏覽器
  • defer屬性規定是否對腳本執行進行延遲,直到頁面加載爲止
  • 若是是多個腳本,該方法能夠確保全部設置了defer屬性的腳本按順序執行
  • 若是腳本不會改變文檔的內容,可將defer屬性加入到script標籤中,以便加快處理文檔的速度
<script type="text/javascript" src="xxx.js" defer></script>
複製代碼

③動態建立script標籤

在還沒定義defer和async前,異步加載的方式是動態建立script,經過window.onload方法確保頁面加載完畢再將script標籤插入到DOM中,具體代碼以下:

function addScriptTag(src){  
    var script = document.createElement('script');  
    script.setAttribute("type","text/javascript");  
    script.src = src;  
    document.body.appendChild(script);  
}  
window.onload = function(){  
    addScriptTag("js/index.js");  
} 
複製代碼

二、異步加載的區別

  • defer是在HTML解析完以後纔會執行,若是是多個,按照加載的順序依次執行
  • async是在加載完以後當即執行,若是是多個,執行順序和加載順序無關

其中藍色線表明網絡讀取,紅色線表明執行時間,這倆都是針對腳本的;綠色線表明 HTML 解析。

三、使用CDN

經過將靜態資源(例如javascript,css,圖片等等)緩存到離用戶很近的相同網絡運營商的CDN節點上,不但能提高用戶的訪問速度,還能節省服務器的帶寬消耗,下降負載。

四、預解析DNS

資源預加載是另外一個性能優化技術,咱們可使用該技術來預先告知瀏覽器某些資源可能在未來會被使用到。

經過 DNS 預解析來告訴瀏覽器將來咱們可能從某個特定的 URL 獲取資源,當瀏覽器真正使用到該域中的某個資源時就能夠儘快地完成 DNS 解析。例如,咱們未來可從 example.com 獲取圖片或音頻資源,那麼能夠在文檔頂部的 標籤中加入如下內容:

<link rel="dns-prefetch" href="//example.com">
複製代碼

當咱們從該 URL 請求一個資源時,就再也不須要等待 DNS 的解析過程。該技術對使用第三方資源特別有用。經過簡單的一行代碼就能夠告知那些兼容的瀏覽器進行 DNS 預解析,這意味着當瀏覽器真正請求該域中的某個資源時,DNS 的解析就已經完成了,從而節省了寶貴的時間。 另外須要注意的是,瀏覽器會對a標籤的href自動啓用DNS Prefetching,因此a標籤裏包含的域名不須要在head中手動設置link。可是在HTTPS下不起做用,須要meta來強制開啓功能。這個限制的緣由是防止竊聽者根據DNS Prefetching推斷顯示在HTTPS頁面中超連接的主機名。下面這句話做用是強制打開a標籤域名解析

<meta http-equiv="x-dns-prefetch-control" content="on">
複製代碼

五、減小內存泄漏

內存泄漏的常見場景

  1. 緩存

文章前言部分就有說到,JS 開發者喜歡用對象的鍵值對來緩存函數的計算結果,可是緩存中存儲的鍵越多,長期存活的對象也就越多,這將致使垃圾回收在進行掃描和整理時,對這些對象作無用功。

  1. 做用域未釋放(閉包)
var leakArray = [];
exports.leak = function () {
    leakArray.push("leak" + Math.random());
}
複製代碼

以上代碼,模塊在編譯執行後造成的做用域由於模塊緩存的緣由,不被釋放,每次調用 leak 方法,都會致使局部變量 leakArray 不停增長且不被釋放。

閉包能夠維持函數內部變量駐留內存,使其得不到釋放。

  1. 不必的全局變量

聲明過多的全局變量,會致使變量常駐內存,要直到進程結束纔可以釋放內存。

  1. 無效的 DOM 引用
//dom still exist
function click(){
    // 可是 button 變量的引用仍然在內存當中。
    const button = document.getElementById('button');
    button.click();
}
複製代碼
  1. 定時器未清除
// vue 的 mounted 或 react 的 componentDidMount
componentDidMount() {
    setInterval(function () {
        // ...do something
    }, 1000)
}
複製代碼

vue 或 react 的頁面生命週期初始化時,定義了定時器,可是在離開頁面後,未清除定時器,就會致使內存泄漏。

  1. 事件監聽爲清空
componentDidMount() {
    window.addEventListener("scroll", function () {
        // do something...
    });
}
複製代碼

在頁面生命週期初始化時,綁定了事件監聽器,但在離開頁面後,未清除事件監聽器,一樣也會致使內存泄漏。

內存泄漏優化

  1. 解除引用

確保佔用最少的內存可讓頁面得到更好的性能。而優化內存佔用的最佳方式,就是爲執行中的代碼只保存必要的數據。一旦數據再也不有用,最好經過將其值設置爲 null 來釋放其引用——這個作法叫作解除引用(dereferencing)

function createPerson(name){
    var localPerson = new Object();
    localPerson.name = name;
    return localPerson;
}

var globalPerson = createPerson("Nicholas");

// 手動解除 globalPerson 的引用
globalPerson = null;
複製代碼

解除一個值的引用並不意味着自動回收該值所佔用的內存。解除引用的真正做用是讓值脫離執行環境,以便垃圾收集器下次運行時將其回收。

  1. 提供手動清空變量的方法
var leakArray = [];
exports.clear = function () {
    leakArray = [];
}
複製代碼
  1. 在業務不須要用到的內部函數,能夠重構在函數外,實現解除閉包
  2. 避免建立過多生命週期較長的對象,或將對象分解成多個子對象
  3. 避免過多使用閉包
  4. 注意清除定時器和事件監聽器

六、開啓gzip

首先,明確gzip是一種壓縮格式,須要瀏覽器支持纔有效(不過通常如今瀏覽器都支持),並且gzip壓縮效率很好(高達70%左右)。而後gzip通常是由apachetomcat等web服務器開啓。

固然服務器除了gzip外,也還會有其它壓縮格式(如 deflate,沒有gzip高效,且不流行),因此通常只須要在服務器上開啓了gzip壓縮,而後以後的請求就都是基於gzip壓縮格式的,很是方便

七、利用瀏覽器緩存

對於web應用來講,緩存是提高頁面性能同時減小服務器壓力的利器。

緩存能夠簡單的劃分紅兩種類型: 強緩存(200fromcache)與 協商緩存(304

區別簡述以下:

  • 強緩存(200fromcache)時,瀏覽器若是判斷本地緩存未過時,就直接使用,無需發起http請求
  • 協商緩存(304)時,瀏覽器會向服務端發起http請求,而後服務端告訴瀏覽器文件未改變,讓瀏覽器使用本地緩存

對於協商緩存,使用 Ctrl+F5強制刷新可使得緩存無效。可是對於強緩存,在未過時時,必須更新資源路徑才能發起新的請求(更改了路徑至關因而另外一個資源了,這也是前端工程化中經常使用到的技巧)。

屬於強緩存控制的:

(http1.1) Cache-Control(瀏覽器)/Max-Age(服務端)
(http1.0)Pragma(瀏覽器)/Expires(服務端)
複製代碼

注意: Max-Age不是一個頭部,它是Cache-Control頭部的值。

屬於協商緩存控制的:

(http1.1) If-None-Match(瀏覽器)/E-tag(服務端)
(http1.0) If-Modified-Since(瀏覽器)/Last-Modified(服務端)
複製代碼

能夠看到,上述有提到http1.1http1.0,這些不一樣的頭部是屬於不一樣http時期的。

再提一點,其實HTML頁面中也有一個meta標籤能夠控制緩存方案- Pragma。

<META HTTP-EQUIV="Pragma" CONTENT="no-cache">
複製代碼

不過,這種方案仍是比較少用到,由於支持狀況不佳,譬如緩存代理服務器確定不支持,因此不推薦。

頭部的區別

首先明確,http的發展是從http1.0到http1.1,而在http1.1中,出了一些新內容,彌補了http1.0的不足。

http1.0中的緩存控制:

  • Pragma:嚴格來講,它不屬於專門的緩存控制頭部,可是它設置 no-cache時可讓本地強緩存失效(屬於編譯控制,來實現特定的指令,主要是由於兼容http1.0,因此之前又被大量應用)
  • Expires:服務端配置的,屬於強緩存,用來控制在規定的時間以前,瀏覽器不會發出請求,而是直接使用本地緩存,注意,Expires通常對應服務器端時間,如Expires:Fri,30Oct 1998 14:19:41
  • If-Modified-Since/Last-Modified:這兩個是成對出現的,屬於協商緩存的內容,其中瀏覽器的頭部是 If-Modified-Since,而服務端的是Last-Modified,它的做用是,在發起請求時,若是If-Modified-SinceLast-Modified匹配,那麼表明服務器資源並未改變,所以服務端不會返回資源實體,而是隻返回頭部,通知瀏覽器可使用本地緩存。Last-Modified,顧名思義,指的是文件最後的修改時間,並且只能精確到 1s之內

http1.1中的緩存控制:

  • Cache-Control:緩存控制頭部,是瀏覽器的頭部,有no-cachemax-age等多種取值
  • Max-Age:服務端配置的,用來控制強緩存,在規定的時間以內,瀏覽器無需發出請求,直接使用本地緩存,注意,Max-AgeCache-Control頭部的值,不是獨立的頭部,譬如 Cache-Control:max-age=3600,並且它值得是絕對時間,由瀏覽器本身計算
  • If-None-Match/E-tag:這兩個是成對出現的,屬於協商緩存的內容,其中瀏覽器的頭部是 If-None-Match,而服務端的是E-tag,一樣,發出請求後,若是If-None-MatchE-tag匹配,則表明內容未變,通知瀏覽器使用本地緩存,和Last-Modified不一樣,E-tag更精確,它是相似於指紋同樣的東西,基於FileEtagINodeMtimeSize生成,也就是說,只要文件變,指紋就會變,並且沒有1s精確度的限制。

Max-Age相比Expires?

Expires使用的是服務器端的時間,可是有時候會有這樣一種狀況-客戶端時間和服務端不一樣步。那這樣,可能就會出問題了,形成了瀏覽器本地的緩存無用或者一直沒法過時,因此通常http1.1後不推薦使用Expires。而 Max-Age使用的是客戶端本地時間的計算,所以不會有這個問題,所以推薦使用 Max-Age

注意,若是同時啓用了Cache-ControlExpiresCache-Control優先級高。

E-tag相比Last-Modified?

Last-Modified

  • 代表服務端的文件最後什麼時候改變的
  • 它有一個缺陷就是隻能精確到1s,
  • 而後還有一個問題就是有的服務端的文件會週期性的改變,致使緩存失效

E-tag

  • 是一種指紋機制,表明文件相關指紋
  • 只有文件變纔會變,也只要文件變就會變,
  • 也沒有精確時間的限制,只要文件一遍,立馬E-tag就不同了

若是同時帶有E-tagLast-Modified,服務端會優先檢查E-tag

各大緩存頭部的總體關係以下圖:

6、var arr = [0,245,7,986],請用apply()和call()方法求數組的最大值

// apply
let arr = [0,245,7,986];
let result = Math.max.apply(null,arr)
console.log(result) // 986
// call
let arr2 = [0,245,7,986];
let result2 = Math.max.call(null,[...arr])
console.log(result2) // 986
複製代碼

7、編寫一個方法去掉數組裏面重復的內容var arr = [1,2,3,4,5,1,2,1]

// 第一種最方便
let arr = [1,2,3,4,5,1,2,1]
let result = Array.from(new Set(arr))
console.log(result) // [1,2,3,4,5]

// 第二種利用對象的屬性不能相同的特色進行去重(兼容性好)
let arr = [1,2,3,4,5,1,2,1];
let array = [];
var obj = {};
for (var i = 0; i < arr.length; i++) {
  if (!obj[arr[i]]) {
    obj[arr[i]] = true;
    array.push(arr[i]);
  }
}
console.log(array); //[1,2,3,4,5]

// 第三種雙層循環 (兼容性好)
var arr = [1,2,3,4,5,1,2,1];
var array = [];
for (var i = 0; i < arr.length; i++) {
  for (var j = 0; j < array.length; j++) {
    if (arr[i] == array[j]) {
      break;
    }
  }
  //若是這兩個數相等說明循環完了,沒有相等的元素
  if (j == array.length) {
    array.push(arr[i]);
  }
}
console.log(array); //[1,2,3,4,5]

// 第四種利用 indexOf()
var arr = [1,2,3,4,5,1,2,1];
var array = [];
for (var i = 0; i < arr.length; i++) {
  if (array.indexOf(arr[i]) == -1) {
    array.push(arr[i]);
  }
}
console.log(array); //[1,2,3,4,5]

// 第五種利用 forEach 和 indexOf()
var arr = [1,2,3,4,5,1,2,1];
var array = [];
arr.forEach(function(item, index) {
  if (array.indexOf(item) == -1) {
    array.push(item);
  }
});
console.log(array); //[1,2,3,4,5]

// 第六種利用 filter()和 indexOf()
var arr = [1,2,3,4,5,1,2,1];
var array = arr.filter(function(item, index) {
  return arr.indexOf(item) == index;
});
console.log(array); //[1,2,3,4,5]

// 第7種ES6 的 includes 實現去重
var arr = [1,2,3,4,5,1,2,1];
var array = [];
arr.forEach(function(item, index) {
  if (!array.includes(item)) {
    array.push(item);
  }
});
console.log(array); //[1,2,3,4,5]
複製代碼

8、怎樣添加,移除,插入,建立和查找節點

  1. 建立新節點
createDocumentFragment()    //建立一個DOM片斷
createElement()   //建立一個具體的元素
createTextNode()   //建立一個文本節點
複製代碼
  1. 添加、移除、替換、插入
appendChild() // 添加
removeChild() // 移除
replaceChild() // 替換
insertBefore() // 插入
複製代碼
  1. 查找節點
getElementsByTagName()    //經過標籤名稱
getElementsByName()       //經過元素的Name屬性的值
getElementById()          //經過元素Id,惟一性
querySelector()           // 經過選擇器獲取一個元素
querySelectorAll()        // 經過選擇器獲取一組元素
getElementsByClassName()  // 經過類名
document.documentElement  // 獲取html的方法
document.body             // 獲取body的方法
複製代碼

9、如下代碼輸出什麼

(1) function Foo () {
    getName = function () { alert(1) };
    return this;
}
(2) Foo.getName = function () { alert(2) };

(3) Foo.prototype.getName = function () { alert(3) };

(4) var getName = function () { alert(4) };

(5) function getName () { alert(5) };

//輸出的值
Foo.getName();   // 2
getName();       // 4
Foo().getName(); // 1
getName();       // 1
new Foo.getName(); // 2
new Foo().getName(); // 3
new new Foo().getName(); 3
複製代碼

此題涉及的知識點衆多,包括變量定義提高、this指針指向、運算符優先級、原型、繼承、全局變量污染、對象屬性及原型屬性優先級等等

此題包含7小問,分別說下。

第一問

先看此題的上半部分作了什麼,首先定義了一個叫Foo的函數,以後爲Foo建立了一個叫getName的靜態屬性存儲了一個匿名函數,以後爲Foo的原型對象新建立了一個叫getName的匿名函數。以後又經過函數變量表達式建立了一個getName的函數,最後再聲明一個叫getName函數。

第一問的 Foo.getName 天然是訪問Foo函數上存儲的靜態屬性,天然是2,沒什麼可說的

第二問

直接調用 getName 函數。既然是直接調用那麼就是訪問當前上文做用域內的叫getName的函數,因此跟1 2 3都沒什麼關係。此題有無數面試者回答爲5。此處有兩個坑,一是變量聲明提高,二是函數表達式。

變量聲明提高

即全部聲明變量或聲明函數都會被提高到當前函數的頂部。

備註:

  1. 初始化不會提高。
  2. 函數提高優先級高於變量提高,且不會被變量聲明覆蓋,可是會被變量賦值以後覆蓋

例以下代碼:

console.log('x' in window); // true 
var x;
x = 0;
複製代碼

代碼執行時js引擎會將聲明語句提高至代碼最上方,變爲:

var x;
console.log('x' in window); // true
x = 0;
複製代碼

函數表達式var getNamefunction getName 都是聲明語句,區別在於 var getName 是函數表達式,而 function getName 是函數聲明。

函數表達式最大的問題,在於js會將此代碼拆分爲兩行代碼分別執行。

例以下代碼:

console.log(x); // 輸出:function x(){} ?
function x(){}
var x=1;
function x(){}
複製代碼

實際執行的代碼爲,先將 var x=1 拆分爲 var x; 和 x = 1; 兩行,再將 var x; 和 function x(){} 兩行提高至最上方變成:

function x(){}
var x;
console.log(x); // function x(){}
x=1;
複製代碼

因此最終函數聲明的x覆蓋了變量聲明的x,log輸出爲x函數

同理,原題中代碼最終執行時的是:

function Foo() {    
    getName = function () { alert (1); };   
     return this;
}
function getName() { alert (5);}//提高函數聲明,覆蓋var的聲明
var getName;//只提高聲明不提高初始化
Foo.getName = function () { alert (2);};
Foo.prototype.getName = function () { alert (3);};
getName = function () { alert (4);};//最終的賦值再次覆蓋function getName聲明
----------------------------------------------------------------------------
getName();//最終輸出4
複製代碼

第三問

第三問的 Foo().getName(); 先執行了Foo函數,而後調用Foo函數的返回值對象的getName屬性函數。Foo函數的第一句 getName = function () { alert (1); };

是一句函數賦值語句,注意它沒有var聲明,因此先向當前Foo函數做用域內尋找getName變量,沒有。再向當前函數做用域上層,即外層做用域內尋找是否含有getName變量,找到了,也就是第二問中的alert(4)函數,將此變量的值賦值爲 function(){alert(1)}。

此處其實是將外層做用域內的getName函數修改了。

**注意:**此處若依然沒有找到會一直向上查找到window對象,若window對象中也沒有getName屬性,就在window對象中建立一個getName變量。以後Foo函數的返回值是this,而JS的this問題各大文章中已經有很是多的文章介紹,這裏再也不多說。

簡單的講,this的指向是由所在函數的調用方式決定的。而此處的直接調用方式,this指向window對象。

遂Foo函數返回的是window對象,至關於執行 window.getName() ,而window中的getName已經被修改成alert(1),因此最終會輸出1

此處考察了兩個知識點,一個是變量做用域問題,一個是this指向問題。

第四問

直接調用getName函數,至關於 window.getName() ,由於這個變量已經被Foo函數執行時修改了,遂結果與第三問相同,爲1

第五問

第五問 new Foo.getName(); ,此處考察的是js的運算符優先級問題。

經過查文檔能夠得知點(.)的優先級高於new(無參數列表)操做,遂至關因而:

new (Foo.getName)();
複製代碼

因此實際上將getName函數做爲了構造函數來執行,遂彈出2。

第六問

第六問 new Foo().getName() ,首先看運算符優先級括號高於new,實際執行爲

(new Foo()).getName()
複製代碼

遂先執行Foo函數,而Foo此時做爲構造函數卻有返回值,因此這裏須要說明下js中的構造函數返回值問題。

構造函數的返回值

在傳統語言中,構造函數不該該有返回值,實際執行的返回值就是此構造函數的實例化對象。

而在js中構造函數能夠有返回值也能夠沒有。

一、沒有返回值則按照其餘語言同樣返回實例化對象。

二、如有返回值則檢查其返回值是否爲引用類型。若是是非引用類型,如基本類型

(string,number,boolean,null,undefined)則與無返回值相同,實際返回其實例化對象。

三、若返回值是引用類型,則實際返回值爲這個引用類型。

原題中,返回的是this,而this在構造函數中原本就表明當前實例化對象,遂最終Foo函數返回實例化對象。

以後調用實例化對象的getName函數,由於在Foo構造函數中沒有爲實例化對象添加任何屬性,遂到當前對象的原型對象(prototype)中尋找getName,找到了。

遂最終輸出3。

第七問

第七問, new new Foo().getName(); 一樣是運算符優先級問題。

最終實際執行爲:

new ((new Foo()).getName)();
複製代碼

先初始化Foo的實例化對象,而後將其原型上的getName函數做爲構造函數再次new。

遂最終結果爲3

相關文章
相關標籤/搜索