天天看點

【直接收藏】前端JavaScript面試100問(終)

作者:陸榮濤

87、事件代理

事件代理 也就是 事件委托

不是直接給标簽添加事件 是給标簽的父級添加事件 通過 事件對象 判斷觸發事件的标簽對象是誰 執行不同的函數程式的文法形式

委托的優點

減少記憶體消耗

試想一下,若果我們有一個清單,清單之中有大量的清單項,我們需要在點選清單項的時候響應一個事件

如果給每個清單項一一都綁定一個函數,那對于記憶體消耗是非常大的,效率上需要消耗很多性能;

是以,比較好的方法就是把這個點選事件綁定到他的父層,也就是 ul 上,然後在執行事件的時候再去比對判斷目标元素;

是以事件委托可以減少大量的記憶體消耗,節約效率。

動态綁定事件

比如上述的例子中清單項就幾個,我們給每個清單項都綁定了事件;

在很多時候,我們需要通過 AJAX 或者使用者操作動态的增加或者去除清單項元素,那麼在每一次改變的時候都需要重新給新增的元素綁定事件,給即将删去的元素解綁事件;

如果用了事件委托就沒有這種麻煩了,因為事件是綁定在父層的,和目标元素的增減是沒有關系的,執行到目标元素是在真正響應執行事件函數的過程中去比對的;

是以使用事件在動态綁定事件的情況下是可以減少很多重複工作的。

88、不卡頓

如何在不卡住頁面的情況下渲染資料,也就是說不能一次性将幾萬條 都渲染出來,而應該一次渲染部分 DOM,那麼就可以通過 requestAnimationFrame 來 每 16 ms 重新整理一次。

<ul>控件</ul>
 <script>
    setTimeout(() => {
       // 插入十萬條資料
      const total = 100000
      // 一次插入 20 條,如果覺得性能不好就減少
      const once = 20
      // 渲染資料總共需要幾次
     const loopCount = total / once
      let countOfRender = 0
      let ul = document.querySelector("ul");
      function add() {
      // 優化性能,插入不會造成回流
       const fragment = document.createDocumentFragment();
      for (let i = 0; i < once; i++) {
        const li = document.createElement("li");
        li.innerText = Math.floor(Math.random() * total);
        fragment.appendChild(li);
      }
     ul.appendChild(fragment);
     countOfRender += 1;
     loop();
  }
  function loop() {
      if (countOfRender < loopCount) {
       window.requestAnimationFrame(add);
   }
  }
  loop();
  }, 0);           

89、JavaScript中的instanceof

JavaScript中變量的類型判斷常常使用typeof運算符,但使用typeof時存在一個缺陷,就是判斷引用類型存儲值時,無論引用的是什麼類型的對象,它都傳回 object。ECMAScript 引入了另一個 Java 運算符 instanceof 來解決這個問題。instanceof 運算符與 typeof 運算符相似,用于識别正在處理的對象的類型。與 typeof 方法不同的是,instanceof 方法要求開發者明确地确認對象為某特定類型。

1.instanceof運算符用法

var strObj = new String("字元串");
console.log(strObj instanceof String);// true


該段代碼判斷的是變量strObj是否為String對象的執行個體,strObj 是 String 對象的執行個體,是以是”true”。盡管不像 typeof 方法那樣靈活,但是在 typeof 方法傳回 “object” 的情況下,instanceof 方法就很有用。

// 判斷 foo 是否是 Foo 類的執行個體
function Foo(){}
var foo = new Foo();

console.log(foo instanceof Foo)


2.instanceof在繼承關系中使用

// 判斷 foo 是否是 Foo 類的執行個體 , 并且是否是其父類型的執行個體
function Aoo(){}
function Foo(){}
Foo.prototype = new Aoo(); //JavaScript 原型繼承

var foo = new Foo();
console.log(foo instanceof Foo)//true
console.log(foo instanceof Aoo)//true
foo作為構造函數Foo的執行個體,因為構造函數Foo原型繼承了構造函數Aoo,是以傳回true。該代碼中是判斷了一層繼承關系中的父類,在多層繼承關系中,instanceof 運算符同樣适用。

3.instanceof運算符代碼
function instance_of(L, R) { //L 表示左表達式,R 表示右表達式
var O = R.prototype; // 取 R 的顯示原型
L = L.__proto__; // 取 L 的隐式原型
while (true) {
  if (L === null)
    return false;
  if (O === L) // 這裡重點:當 O 嚴格等于 L 時,傳回 true
    return true;
  L = L.__proto__;
}
}           

90、forEach中的await

不知道你是否寫過類似的代碼:

function test() {
     let arr = [3, 2, 1]
     arr.forEach(async item => {
      const res = await fetch(item)
      console.log(res)
     })
     console.log('end')
    }

 function fetch(x) {
 return new Promise((resolve, reject) => {
  setTimeout(() => {
   resolve(x)
  }, 500 * x)
 })
}

test()           

我當時期望的列印順序是

3
2
1
end
結果現實與我開了個玩笑,列印順序居然是

end
1
2
3
為什麼?           

其實原因很簡單,那就是 forEach 隻支援同步代碼。

我們可以參考下 Polyfill 版本的 forEach,簡化以後類似就是這樣的僞代碼

while (index < arr.length) {
  callback(item, index)   //也就是我們傳入的回調函數
}           

從上述代碼中我們可以發現,forEach 隻是簡單的執行了下回調函數而已,并不會去處理異步的情況。并且你在 callback 中即使使用 break 也并不能結束周遊。

怎麼解決?

一般來說解決的辦法有2種,for...of和for循環。

使用 Promise.all 的方式行不行,答案是:不行

從上述代碼中我們可以發現,forEach 隻是簡單的執行了下回調函數而已,并不會去處理異步的情況。并且你在 callback 中即使使用 break 也并不能結束周遊。

怎麼解決?

一般來說解決的辦法有2種,for...of和for循環。

使用 Promise.all 的方式行不行,答案是:不行           

可以看到并沒有按照我們期望的輸出。

這樣可以生效的原因是 async 函數肯定會傳回一個 Promise 對象,調用 map 以後傳回值就是一個存放了 Promise 的數組了,這樣我們把數組傳入 Promise.all 中就可以解決問題了。但是這種方式其實并不能達成我們要的效果,如果你希望内部的 fetch 是順序完成的,可以選擇第二種方式。

第1種方法是使用 for...of

async function test() {
  let arr = [3, 2, 1]
  for (const item of arr) {
   const res = await fetch(item)
   console.log(res)
  }
  console.log('end')
 }           

這種方式相比 Promise.all 要簡潔的多,并且也可以實作開頭我想要的輸出順序。

但是這時候你是否又多了一個疑問?為啥 for...of 内部就能讓 await 生效呢。

因為 for...of 内部處理的機制和 forEach 不同,forEach 是直接調用回調函數,for...of 是通過疊代器的方式去周遊。

async function test() {
 let arr = [3, 2, 1]
 const iterator = arr[Symbol.iterator]()
 let res = iterator.next()
 while (!res.done) {
  const value = res.value
  const res1 = await fetch(value)
  console.log(res1)
  res = iterator.next()
 }
 console.log('end')
}           

第2種方法是使用 for循環

async function test() {
  let arr = [3, 2, 1]
  for (var i=0;i<arr.length;i++) {
    const res = await fetch(arr[i])
    console.log(res)
  }
  console.log('end')
}

function fetch(x) {
 return new Promise((resolve, reject) => {
  setTimeout(() => {
   resolve(x)
  }, 500 * x)
 })
}

test()           

第3種方法是使用 while循環

async function test() {
  let arr = [3, 2, 1]
  var i=0;
  while(i!==arr.length){
    const res = await fetch(arr[i])
    console.log(res)
    i++;
  }
  console.log('end')
}

function fetch(x) {
 return new Promise((resolve, reject) => {
  setTimeout(() => {
   resolve(x)
  }, 500 * x)
 })
}

test()           

要想在循環中使用async await,請使用for...of 或者 for 循環, while循環

forEach支援async awaitforEach 在正常情況像下面這麼寫肯定是做不到同步的,程式不會等一個循環中的異步完成再進行下一個循環。原因很明顯,在上面的模拟中,while 循環隻是簡單執行了 callback,是以盡管 callback 内使用了 await ,也隻是影響到 callback 内部。

arr.myforeach(async v => {
    await fetch(v);
});           

要支援上面這種寫法,隻要稍微改一下就好

Array.prototype.myforeach = async function (fn, context = null) {
    let index = 0;
    let arr = this;
    if (typeof fn !== 'function') {
        throw new TypeError(fn + ' is not a function');
    }
    while (index < arr.length) {
        if (index in arr) {
            try {
                await fn.call(context, arr[index], index, arr);
            } catch (e) {
                console.log(e);
            }
        }
        index ++;
    }
};           

91、src和href

src和href都是用在外部資源的引入上,比如圖像,CSS檔案,HTML檔案,以及其他的web頁面等等,那麼src和href的差別都有哪些呢?

1、請求資源類型不同

(1) href是Hypertext Reference的縮寫,表示超文本引用。用來建立目前元素和文檔之間的連結。常用的有:link、a。

(2)在請求 src 資源時會将其指向的資源下載下傳并應用到文檔中,常用的有script,img 、iframe;

2、作用結果不同

(1)href 用于在目前文檔和引用資源之間确立聯系;

(2)src 用于替換目前内容;

3、 浏覽器解析方式不同

(1)若在文檔中添加href ,浏覽器會識别該文檔為 CSS 檔案,就會并行下載下傳資源并且不會停止對目前文檔的處理。這也是為什麼建議使用 link 方式加載 CSS,而不是使用 @import 方式。

(2)當浏覽器解析到src ,會暫停其他資源的下載下傳和處理,直到将該資源加載、編譯、執行完畢,圖檔和架構等也如此,類似于将所指向資源應用到目前内容。這也是為什麼建議把 js 腳本放在底部而不是頭部的原因。

92、JavaScript中事件綁定的方法

在JavaScript的學習中,我們經常會遇到JavaScript的事件機制,例如,事件綁定、事件監聽、事件委托(事件代理)等。這些名詞是什麼意思呢,有什麼作用呢?

一、事件綁定要想讓 JavaScript 對使用者的操作作出響應,首先要對 DOM 元素綁定事件處理函數。所謂事件處理函數,就是處理使用者操作的函數,不同的操作對應不同的名稱。

在JavaScript中,有三種常用的綁定事件的方法:

在DOM元素中直接綁定;在JavaScript代碼中綁定;綁定事件監聽函數。

1、在DOM中直接綁定事件

我們可以在DOM元素上綁定onclick、onmouseover、onmouseout、onmousedown、onmouseup、ondblclick、onkeydown、onkeypress、onkeyup等。好多不一一列出了。如果想知道更多事件類型請檢視, DOM事件 。

<input type="button" value="click me" onclick="hello()">


<script>
function hello(){
 alert("hello world!");
}           

2、在JavaScript代碼中綁定事件

在 JS 代碼中(即 script 标簽内)綁定事件可以使 JS 代碼與HTML标簽分離,文檔結構清晰,便于管理和開發。

<input type="button" value="click me" id="btn">

<script>
document.getElementById("btn").onclick = function(){
 alert("hello world!");
}           

3、使用事件監聽綁定事件

綁定事件的另一種方法是用 addEventListener() 或 attachEvent() 來綁定事件監聽函數。下面詳細介紹,事件監聽。

1)事件監聽

關于事件監聽,W3C規範中定義了3個事件階段,依次是捕獲階段、目标階段、冒泡階段。

起初Netscape制定了JavaScript的一套事件驅動機制(即事件捕獲)。随即IE也推出了自己的一套事件驅動機制(即事件冒泡)。最後W3C規範了兩種事件機制,分為捕獲階段、目标階段、冒泡階段。IE8以前IE一直堅持自己的事件機制(前端人員一直頭痛的相容性問題),IE9以後IE也支援了W3C規範。

W3C規範

element.addEventListener(event, function, useCapture)
event : (必需)事件名,支援所有 DOM事件 。
function:(必需)指定要事件觸發時執行的函數。
useCapture:(可選)指定事件是否在捕獲或冒泡階段執行。true,捕獲。false,冒泡。預設false。
注:IE8以下不支援。

<input type="button" value="click me" id="btn1">

<script>
document.getElementById("btn1").addEventListener("click",hello);
function hello(){
 alert("hello world!");
}
IE标準


element.attachEvent(event, function)
event:(必需)事件類型。需加“on“,例如:onclick。
function:(必需)指定要事件觸發時執行的函數。
<input type="button" value="click me" id="btn2">
<script>
document.getElementById("btn2").attachEvent("onclick",hello);
function hello(){
 alert("hello world!");
}           
<script>
document.getElementById("btn2").attachEvent("onclick",hello);
function hello(){
 alert("hello world!");
}           

2)事件監聽的優點

1、可以綁定多個事件;正常的事件綁定隻執行最後綁定的事件。

<input type="button" value="click me" id="btn3">
<input type="button" value="click me" id="btn4">

var btn3 = document.getElementById("btn3");
btn3.onclick = function(){                  //動态綁定事件
 alert("hello 1");           //不執行
}
btn3.onclick = function(){
 alert("hello 2");           //執行
}


var btn4 = document.getElementById("btn4");
btn4.addEventListener("click",hello1);      //添加事件監聽器
btn4.addEventListener("click",hello2);

function hello1(){
 alert("hello 1");        //執行
}
function hello2(){
 alert("hello 2");        //執行 (順序執行)
}           

2、可以解除相應的綁定

<input type="button" value="click me" id="btn5">

<script>
var btn5 = document.getElementById("btn5");
btn5.addEventListener("click",hello1);//執行了
btn5.addEventListener("click",hello2);//不執行
btn5.removeEventListener("click",hello2);


function hello1(){
 alert("hello 1");
}
function hello2(){
 alert("hello 2");
}           

3)封裝事件監聽

<input type="button" value="click me" id="btn5">

//綁定監聽事件
function addEventHandler(target,type,fn){
 if(target.addEventListener){
 target.addEventListener(type,fn);
 }else{
 target.attachEvent("on"+type,fn);
 }
}

//移除監聽事件
function removeEventHandler(target,type,fn){
 if(target.removeEventListener){
 target.removeEventListener(type,fn);
 }else{
 target.detachEvent("on"+type,fn);
 }
}           

93、git常見分支

git 分支命名規範

為規範開發,保持代碼送出記錄以及 git 分支結構清晰,友善後續維護,現規範 git 的相關操作。

主要規範兩點:

git 分支命名規範

git 送出記錄規範

1. git 分支命名規範

git 分支分為內建分支、功能分支和修複分支,分别命名為 develop、feature 和 hotfix,均為單數。不可使用 features、future、hotfixes、hotfixs 等錯誤名稱。

master(主分支,永遠是可用的穩定版本,不能直接在該分支上開發)

develop(開發主分支,所有新功能以這個分支來建立自己的開發分支,該分支隻做隻合并操作,不能直接在該分支上開發)

feature-xxx(功能開發分支,在develop上建立分支,以自己開發功能子產品命名,功能測試正常後合并到develop分支)

feature-xxx-fix(功能bug修複分支,feature分支合并之後發現bug,在develop上建立分支修複,之後合并回develop分支。PS:feature分支在申請合并之後,未合并之前還是可以送出代碼的,是以feature在合并之前還可以在原分支上繼續修複bug)

hotfix-xxx(緊急bug修改分支,在master分支上建立,修複完成後合并到 master)

注意事項:

一個分支盡量開發一個功能子產品,不要多個功能子產品在一個分支上開發。

feature 分支在申請合并之前,最好是先 pull 一下 develop 主分支下來,看一下有沒有沖突,如果有就先解決沖突後再申請合并。

94、前端引擎模闆

JavaScript随着各種神奇的實用功能庫日漸豐富,而越來越受到Web開發者與設計師的追捧,例如jQuery,MooTools,Prototype等。

1) Jade

Jade是一個有着完善API和驚豔特性的JavaScript模闆引擎。使用空白與縮進敏感的代碼格式編寫HTML頁面。基于Node.js,運作在伺服器端。

2) Mustache

Mustache是一個logic-less(無邏輯或輕邏輯)文法模闆。可以用于組織HTML、配置檔案、源代碼在内的任何東西。Mustache使用JavaScript對象的值,用來擴充模闆代碼中的大括号标簽。

3) Transparency

Transparency是一個強大的用戶端模闆引擎,用來将資料綁定到Web頁面的BOM結構中。其模闆無需特殊格式,直接完全符合HTML。直接使用JavaScript邏輯,無需新學特殊的“模闆語言”。相容IE9+、Chrome、Fx、iOS、安卓等浏覽器。

4) Underscore.js

Underscore.js是一個JavaScript庫,提供一系列實用的工具函數(helper)。Underscore.js僅作為額外的工具函數獨立工作,不擴充(污染)任何JavaScript内建對象的本身。

5) Embeddedjs

EJS以類似PHP的JS/HTML通過标簽混排的形式,幫助開發者将JavaScript和HTML部分有效分離。

6) DoTjs

最快和簡潔的JavaScript模闆引擎,同時用于Node.js和浏覽器。

7) Handlebarsjs

一套語義化模闆引擎。相容Mustache。

8) T.js

一個用簡單的JavaScript資料結構去渲染表現html/xml内容的模闆引擎。

9) Dustjs

一套同時可用于浏覽器或Node.js的異步模闆引擎。

10) Nunjucks

Nunjucks是一套富功能的模闆引擎。模闆語言功能強大,支援塊繼承、自動轉義、宏、異步控制等功能。

怎麼樣的模闆引擎是适合前端的

前端模闆引擎需要有開發時的透明性我認為前端任何架構和工具都要有對開發的透明性,模闆引擎也不例外。所謂透明性即指我在搭建好開發環境後,随手寫代碼随手重新整理浏覽器就能看到最新的效果,而不需要額外地執行任何指令或有任何的等待過程是以一切依賴編譯過程的模闆引擎并不适合前端使用,編譯隻能是模闆引擎的一個特性,而不能是使用的前提更嚴格地說,使用FileWatch等手段進行檔案變更檢測并自動編譯也不在我的考慮範圍之内,因為這會造成額外的等待,像我這種手速極快的人可能編譯速度跟不上由此可以推出,前端的模闆引擎應該是具備可在純前端環境中解析使用的能力的

前端模闆引擎要有良好的運作時調試能力

前端并不像後端,任何錯誤都可以有嚴格的日志記錄和調用堆棧以供分析。

由于使用者行為的不确定性、執行環境的不确定性、各種第三方腳本的影響等,前端很難做到完全的錯誤處理和跟蹤,這也導緻前端必然存在需要直接線上上排查問題的情況而當問題出現在模闆引擎這一層時,就需要模闆引擎提供良好的調試能力

一般來說,編譯後生成的函數的調試能力是弱于原先手動編寫的模闆片斷的,因為自動生成的函數基本不具備可讀性和可斷點跟蹤性是以在這一點上,一個供前端使用的模闆引擎應該具備在特定情況下從“執行編譯後函數擷取HTML”換回“解析原模闆再執行函數擷取HTML”的模式,即應該支援在兩種模式間切換或者更好地,

一個強大的前端模闆引擎編譯生成的函數,可以使用Source Map或其它自定義的手段直接映射回原模闆片段,不過現在并沒有什麼模闆引擎實作了這一功能

前端模闆引擎要對檔案合并友好

在HTTP/2普及之前,檔案合并依舊是前端性能優化中的一個重要手段,模闆作為檔案的一部分,依舊是需要合并的

在提供編譯功能的模闆引擎中,我們可以使用編譯的手段将模闆變為JavaScript源碼,再在JavaScript的基礎上做檔案合并

但是如果我們出于上文所說的調試能力等原因希望保留原模闆片段,那就需要模闆引擎本身支援模闆片段合并為一個檔案了

大部分僅支援将一段輸入的字元串作為模闆解析的引擎并不具備這一能力,他們天生并不能将一整個字元串切分為多個模闆片段,因而無法支援模闆片段層面上的檔案合并

需要實作對檔案合并的支援,最好的辦法就是讓模闆的文法是基于“片段”的

前端模闆引擎要擔負XSS的防範

從安全性上來說,前端對XSS的控制是有嚴格要求的

我在 單頁面(SPA)開發會不會比多頁面有更多的安全問題?- 張立理的回答 中有提到過,前端對XSS的防範比較合适的方法是使用“預設轉義”的白名單式政策

基于此,一個合理的模闆引擎是必須支援預設轉義的,即所有資料的輸出都預設經過escape的邏輯處理,将關鍵符号轉為對應的HTML實體符号,以從根源上杜絕XSS的入侵路徑

當然并不是所有的内容都必須經過轉義的,在系統中免不了有對使用者輸入富文本的需求,是以需要支援特定的文法來産生無轉義的輸出,但時刻注意無轉義輸出才是特例,預設情況下必須是轉義輸出的

前端模闆引擎要支援片段的複用

這并不是前端模闆引擎的需求,事實上任何模闆引擎都應該支援片段的複用,後端如Velocity、Smarty等無不擁有此功能

所謂片段複用,應該有以下幾個層次的應用:

一個片段可以被引入到另一處,相當于一個變量到處用的效果

一個片段被引入時,可以向其傳遞不同的資料,相當于一個函數到處用的效果

一個片段可以被外部替換,但外部不提供此片段的話保持一個預設的内容,類似設計模式中的政策模式

滿足第1和第2點的模闆引擎并不少,而滿足第3點的前端模闆引擎卻不多見,而後端的Razor、Smarty等都具備這一功能

話說我當時設計我們自己的模闆引擎的第3個版本時,就想出了block這一個概念來實作第3點,在做完傳遞将近半年之後,有人告訴我說Smarty上就有這概念,頓時有種不知應該高興還是悲傷的不知所措感。還好他并沒有懷疑我直接抄了别人的功能,不然真是冤枉

前端模闆引擎要支援資料輸出時的處理

所謂資料輸出時處理,指一個資料要在輸出時做額外的轉換,最常見的如字元串的trim操作,比較技術性的如markdown的轉換等

誠然資料的轉換完全可以在将資料交給模闆引擎前就通過JavaScript的邏輯處理完,但這會導緻不少有些醜陋又有些備援的代碼,對邏輯本身的複用性也會造成負面的影響

通常模闆引擎對資料做額外處理會使用filter的形式實作,類似bash中的管道的邏輯。filter的實作和注冊也會有不同的設計,如mustache其實注冊的是fitler工廠,而另一些模闆引擎則會直接注冊filter本身,不同設計有不同的考量點,我們很難說誰好誰壞

但是,模闆引擎支援資料的輸出處理後,會另我們在編碼過程中産生一個新的糾結,即哪些資料處理應該交由模闆引擎的filter實作,哪些應該在交給模闆引擎前由自己的邏輯邏輯實作。這個話題展開來又是一篇長長的論述,于目前的話題無關就略過吧

前端模闆引擎要支援動态資料

在開發過程中,其實有不少資料并不是靜态的,如EmberJS就提供了Computed Property這樣的概念,Angular也有類似的東西,Backbone則可以通過重寫Model的get方法來變相實作

雖然ES5在語言層面上直接提供了getter的支援,但我們在前端開發的大部分場景下依舊不會使用這一語言特性,而會選擇将動态的資料封裝為某種對象的get等方法而模闆引擎在将資料轉為HTML片段的過程中,同樣應該關注這一點,對這些動态計算的資料有良好的支援

說得更明白一些,模闆引擎不應該僅僅接受純對象(Plain Object)作為輸入,而應該更開放地接受類似帶有get方法的動态的資料

一個比較合理的邏輯是,如果一個對象有一個get方法(模闆引擎決定這個接口),則資料通過該方法擷取,其它情況下視輸入的對象為純對象(Plain Object),使用标準的屬性擷取邏輯

前端模闆引擎要與異步流程嚴密結合

前端有一個很大的特點,就是到處充斥着異步的流程。由于JavaScript在浏覽器提供的引擎中單線程執行的特性、大部分與IO相關的API都暴露為異步的事實,以及多數子產品定義規範中模闆的動态擷取是異步的這一現象,注定我們無法将這個世界當作完全同步來看

一個很常見的例子是,我們有一個AMD子產品存放了全局使用的常量,模闆引擎需要使用這些常量。當然我們可以在使用模闆引擎之前讓JavaScript去異步擷取這一子產品,随後将常量作為資料傳遞給模闆引擎,但這是一種業務與視圖相對耦合的玩法,出于強迫症我并不覺得這是一個漂亮的設計,是以我們希望直接在模闆中這麼寫:

{{$globals.ICP_SERIAL}}           

這是我假想的一個文法,通過$globals可以使用AMD Loader擷取globals這一子產品,随後擷取其中的ICP_SERIAL屬性輸出

模闆引擎支援異步是一個比較具有挑戰性的話題,我的計劃是在我們自己的模闆引擎的下一個版本中嘗試實作。這其中涉及很多的技術點,比如:

模闆的輸出本身成了異步的方法,而不再像現在一樣直接傳回字元串

分析模闆對異步操作的依賴,整個字元串的拼接邏輯被打斷成多個異步

異步是需要等待的,且等待是未知的,從性能上考慮,是否需要考慮Stream式的輸出,以便完成一段提供一段

是提供内置的固定幾種異步邏輯,還是基于Promise支援任何自定義的異步邏輯,在複雜度和實用性上作出平衡

至今我還沒有完全明确模闆與異步結合的方式和接口,這個話題也沒辦法繼續深入探讨了

前端模闆引擎要支援不同的開發模式

前端發展至今,有很多不同的開發模式,比如:

最普通的HTML頁面,使用DOMContentLoaded等事件添加邏輯,特定互動下局部重新整理頁面

采用傳統的MVC模型進行單頁式開發

使用MVVM方式以資料為核心,資料與視圖方向綁定進行開發

基于Immutable Data進行資料比對Diff轉DOM更新的開發(其中可能有Virtual DOM的引入)

一個模闆引擎要能支援這麼多種不同的的模式是一個非常大的挑戰,特别是對雙向綁定的支援尤為突出。至今為止幾乎所有的支援雙向綁定的開發架構都自帶了專用的模闆引擎,這是因為雙向綁定對模闆有兩大要求:

能夠從模闆中提取“這一模闆對哪些資料有依賴”的元資訊

能夠知道一個資料變化引擎的是模闆的哪一塊,而不至于整個重新整理

而通用模闆引擎很少提供這兩個特性,是以沒辦法對不同的前端開發模式進行全面到位的支援

從模闆引擎本身的實作上來說,一種方法是直接将模闆解析後的類似AST的結構暴露出去,供其他架構合理地處理,同時提供對模闆局部的重新整理功能(也可與前面所說的模闆片段一起考慮),但是大部分模闆引擎為了性能等考慮,是不會解析出類似AST的文法結構來的

前端模闆引擎要有執行個體間的隔離

在大型的前端項目,特别是單頁式的項目中,會有完全未知個數的模闆片段同時存在,如果這些片段是帶有名稱(出于複用的考慮)的,就很容易造成名稱上的沖突對于同一層級的邏輯(如大家都是業務層代碼,或者大家都是控件層代碼),名稱沖突是可以通過一些開發時的約定來解決的。但不同層之間,由于封裝性的要求,外部不應該知道一些僅内部使用的片段的名稱,此時如果不幸有名稱與其它層有沖突,會讓情況變得比較麻煩,這類問題甚至都不容易跟蹤,往往會導緻大量的精力和時間的浪費

是以,一個好的模闆引擎應該是多執行個體的,且不同執行個體間應該互相具備隔離性,不會出現這種不可預期的沖突

将這個話題再往深地研究,就會發現單純的隔離是不夠的,不同層間除了不沖突的需求,同樣還有片段複用的需求,我們還會需要不同模闆執行個體間可以開放一些固定的片段共享,是以模闆引擎各個執行個體的關系是一種組合依賴但又具備基本的封裝和隔離的狀态

95、datalist 用法

<input list="browsers">
<datalist id="browsers">
  <option value="Internet Explorer">
  <option value="Firefox">
  <option value="Chrome">
  <option value="Opera">
  <option value="Safari">
</datalist>           

這裡注意綁定datalist的id給input的list屬性,這樣在input輸入框下面就會出現清單

96、ajax同步和異步的差別

在使用ajax請求資料的時候,通常情況下我們都是把async:true當做預設來處理,讓我們的請求成為一個異步的請求。但是在某種情況下我們是需要吧async:false設定為false的,友善我們進行觀察資料的走向、去處。那同步和異步有什麼差別呢?

同步請求 async:false

$.ajax({
        async:false,
        type:"POST",
        url:"Venue.aspx?act=init",
        dataType:"html",
        success:function(result){  //function1()
            f1();
            f2();
         }
         failure:function (result) {
            alert('我在彈');
        }
        }
function2();           

分析這個時候ajax塊送出請求後,他會等待在function1()這個地方,不會去執行function2(),直到function1()部分執行完畢。異步請求 async:true

$.ajax({
        async: true, //預設為 true
        type:"POST",
        url:"./xxx/xxx/a/b.html",
        dataType:"html",
        success:function(result){  //function1()
             f1();
             f2();
        }
        failure:function (result) {
              alert('我彈');
        },
        }
function2();           

分析當ajax塊送出請求後,他将停留function1(),等待傳回結果,但同時(在這個等待過程中),function2()就可以跑起來。總結(兩者的差別)同步的請求的時候,代碼好比在排隊,必須是一個挨着一個的去執行,前面的沒有結束,後面的代碼就處于一個阻塞的狀态。異步執行的時候,資料請求的同時,其他代碼語句也可以同步執行,比如,在資料請求的時候,由于某些願意,需要慢慢的傳回請求結果,在這個時候帶寬是很空閑的,那麼,代碼不會等到前面的資料完全請求傳回就可以開始後面的代碼運作。

97、JavaScript僞數組

數組

定義: 數組是一種類清單對象,它的原型中提供了周遊和修改元素的相關操作。JavaScript 數組的長度和元素類型都是非固定的。隻能用整數作為數組元素的索引,而不能用字元串。對象是沒有索引的,是數組的基本特征。

var obj = {};
var arr = [];

obj[2] = 'a';
arr[2] = 'a';

console.log(obj[2]); // => a
console.log(arr[2]); // => a
console.log(obj.length); // => undefined
console.log(arr.length); // => 3           

obj[2]輸出’a’,是因為對象就是普通的鍵值對存取資料而arr[2]輸出’a’ 則不同,數組是通過索引來存取資料,arr[2]之是以輸出’a’,是因為數組arr索引2的位置已經存儲了資料obj.length并不具有數組的特性,并且obj沒有儲存屬性length,那麼自然就會輸出undefined而對于數組來說,length是數組的一個内置屬性,數組會根據索引長度來更改length的值為什麼arr.length輸出3,而不是1在給數組添加元素時,并沒有按照連續的索引添加,是以導緻數組的索引不連續,那麼就導緻索引長度大于元素個數

僞數組

定義:

僞數組是一個對象(Object),而真實的數組是一個數組(Array)擁有length屬性,且必須是number類型,其它屬性(索引)為字元串不具有數組所具有的方法,forEach()等,不過有Object的方法僞數組長度不可變,真數組長度可以變可以通過for in周遊

var fakeArray = {
    length: 3,
    "0": "first",
    "1": "second",
    "2": "third"
}
var arr = [1, 2, 3, 4]

// 真數組的方法來自Array.prototype
console.log(fakeArray instanceof Array) //false
console.log(arr instanceof Array) // true

Array.isArray(fakeArray) // false;
Array.isArray(arr) // true;

console.log(arr.__proto__ === Array.prototype) // true
console.log(fakeArray.__proto__ === Array.prototype) // false
console.log(fakeArray.__proto__ === Object.prototype) // true

arr.forEach(x => console.log(x)) // 1 2 3 4
fakeArray.forEach(x => console.log(x)) // fakeArray.forEach is not a function

Object.keys(fakeArray) //  ["0", "1", "2", "length"]           

常見的僞數組有:

函數内部的 argumentsDOM 對象清單(比如通過 document.getElementsByTags 得到的清單)jQuery 對象(比如 $(“div”) )僞數組是一個 Object,而真實的數組是一個 Array。僞數組存在的意義,是可以讓普通的對象也能正常使用數組的很多方法,比如:

使用Array.prototype.slice.call();
var arr = Array.prototype.slice.call(arguments);

Array.prototype.forEach.call(arguments, function(v) {
  // 循環arguments對象
});

// push
// some
// every
// filter
// map
// ...


使用[].slice.call()
var fakeArray = {
    length: 3,
    "0": "first",
    "1": "second",
    "2": "third"
}
var arr = [].slice.call(fakeArray)
console.log(arr) // ["first", "second", "third"]

使用ES6中的Array.from方法
var fakeArray = {
  length: 3,
    "0": "first",
    "1": "second",
    "2": "third"
  }
var arr = Array.from(fakeArray)
console.log(arr) // ["first", "second", "third"]

使用擴充運算符,也是ES6的文法
var fakeArray = document.querySelectorAll('div')
var newArr= [...fakeArray]
console.log(newArr.__proto__ === Array.prototype) // true


僞數組轉換為真數組原理
Array.prototype.slice = function (start, end) {
  start = start || 0
  end = start || this.length
  const arr = []
  for (var i = start; i < end; i++) {
    arr.push(this[i])
  }
  return arr
}           

結論對象沒有數組 Array.prototype 的屬性值,類型是 Object ,而數組類型是 Array數組是基于索引的實作, length 會自動更新,而對象是鍵值對使用對象可以建立僞數組,僞數組可以正常使用數組的大部分方法

98、同源政策

何為同源?
域名、協定、端口完全一緻即為同源。


www.juejin.com 和juejin.com
不同源,因為域名不同


www.bilibili.tv和http://www.bilibili.com
不同源,因為域名不同


http://localhost:3000 和 http://localhost:3001
不同源,因為端口不同


qq.com 和https://qq.com
不同源,因為協定不同


www.pixiv.net 和 www.pixiv.net/manage/illu…
同源,因為域名,協定,端口都相同


何為政策?
政策主要限制js的能力
1.無法讀取非同源的 cookie、Storage、indexDB的内容
2.無法讀取非同源的DOM
3.無法發送非同源的AJAX,更加準确的說應該是發送了請求但被浏覽器攔截了。
為什麼會有同源政策?

為了保護使用者資料安全

1.為了防止惡意網頁可以擷取其他網站的本地資料。
2.為了防止惡意網站iframe其他網站的時候,擷取資料。
3.為了防止惡意網站在自已網站有通路其他網站的權利,以免通過cookie免登,拿到資料。
跨域問題
前後端分離,和使用服務商資料時,導緻前端頁面位址和後端API不是同源的,例如前端位址為baidu.com,後端API為api.baidu.com。直接通路API會觸發同源政策,是以需要想辦法跨過去。
常見的跨域方法的原理
1.CORS
•CORS(跨域資源共享)使用專用的HTTP頭,伺服器(api.baidu.com)告訴浏覽器,特定URL(baidu.com)的ajax請求可以直接使用,不會激活同源政策。
2.JSONP
•這個方案相當于黑魔法,因為js調用(實際上是所有擁有src屬性的 <\script>、<\img>、<\iframe>)是不會經過同源政策,例如baidu.com引用了CDN的jquery。是以我通過調用js腳本的方式,從伺服器上擷取JSON資料繞過同源政策。
3.nginx反向代理
•當你通路baidu.com/api/login的時候,通過在baidu.com的nginx伺服器會識别你是api下的資源,會自動代理到api.baidu.com/login,浏覽器本身是不知道我實際上是通路的api.baidu.com的資料,和前端資源同源,是以也就不會觸發浏覽器的同源政策。           

99、擷取第二大的數字

方法一将數組從大到小排序然後找第二個當然在JS中有sort()方法可以進行數組排序

var arr=[5,2,10,8,0,4,7,11,9,1];
function array1(){
    var max,min;
    if(arr[0]<arr[1]){
          max=arr[1];
          min=arr[0];
    }
    else
    {
         max=arr[0];
         min=arr[1];
    }
    for(i=2;i<arr.length;i++)
    {
        if(arr[i]>min)
        {
            if(arr[i]>max)
            {
                min=max;
                max=arr[i];
            }
            else
                min=arr[i];
        }
    }
    alert(min);
}
array1();           

方法二

定義兩個變量max min循環周遊分别存儲目前最大和第二大的數然後輸出第二大的數min;

var arr=[5,2,10,8,0,4,7,11,9,1];
function array2(){
    var temp,min;
    for(var i=0;i<arr.length-1;i++){
        min=i;
        for(var j=i+1;j<arr.length;j++){
            if(arr[j]>arr[i]){
                temp= arr[i];
                arr[i] = arr[j];
                arr[j] = temp;
            }
        }
    }
    alert(arr[1]);
}
array2();           

100、forin和Object.keys的差別

使用for in 去周遊 對象會将prototype上面擴充的方法或者屬性也列印出來

// 遞歸寫法
Object.prototype.clone = function(){
    let o = this.constructor === Array ? [] : {};
    for(let e in this){
        o[e] = typeof this[e] === "object" ? this[e].clone() : this[e];
    }
    return o;
}
let obj = {
    a : 1,
    b : {
        c: 2
    }
}
let obj2 = obj.clone();
console.log(obj2);// { a: 1, b: { c: 2, clone: [Function] }, clone: [Function] }           

解決方法可以為每一次的周遊加上hasOwnPropertyhasOwnProperty具體的作用就是判斷該屬性是否屬于對象自身的屬性

// 遞歸寫法

Object.prototype.clone = function(){
    let o = this.constructor === Array ? [] : {};
    for(let e in this){
        if(this.hasOwnProperty(e)){
            o[e] = typeof this[e] === "object" ? this[e].clone() : this[e];
        }
    }
    return o;
}
let obj = {
    a : 1,
    b : {
        c: 2
    }
}
let obj2 = obj.clone();
console.log(obj2); // { a: 1, b: { c: 2 } }


也可以使用Object.keys()方式完成周遊操作

// 遞歸寫法
Object.prototype.clone = function(){
    let o = this.constructor === Array ? [] : {};

    Object.keys(this).forEach(item => {
        o[item] = typeof this[item] === "object" ? this[item].clone() : this[item]
    })
    return o;

}
let obj = {
    a : 1,
    b : {
        c: 2
    }
}
let obj2 = obj.clone();
console.log(obj2);// { a: 1, b: { c: 2 } }           

繼續閱讀