前端技術演進(四):前端三層結構與應用

姜小抖發表於2018-12-14
這個來自之前做的培訓,刪減了一些業務相關的,參考了很多資料(參考資料列表),謝謝前輩們,麼麼噠 ?

前端有三個基本構成:結構層HTML、表現層CSS和行為層Javascript。
他們分別成熟的版本是HTML5、CSS3和ECMAScript 6+。
這裡我們主要了解現代前端三層結構的演進歷程以及如何在三層結構的基礎之上進行高效開發。

HTML

HTML(超文字標記語言——HyperText Markup Language)是構成 Web 世界的基石。

演進

image.png | center | 571x951

DOCTYPE

<!DOCTYPE> 宣告不是 HTML 標籤;它是指示 web 瀏覽器關於頁面使用哪個 HTML 版本進行編寫的指令。如果 DOCTYPE 不存在或者格式不正確,則會導致文件以相容模式呈現,這時瀏覽器會使用較低的瀏覽器標準模式來解析整個HTML文字。

HTML 5:

<!DOCTYPE html>複製程式碼

HTML5中的doctype是不區分大小寫的。

HTML 4.01 Strict:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">複製程式碼

語義化標籤

HTML語義化能讓頁面內容更具結構化且更加清晰,便於瀏覽器和搜尋引擎進行解析,因此要儘量使用帶有語義化結構標籤。

image.png | center | 400x616

一般情況下,具有良好Web語義化的頁面結構在沒有樣式檔案的情況下也是能夠閱讀的,例如列表會以列表的樣式展現,標題文字會加粗,而不是全部內容都以無層次的文字內容形式呈現。

image.png | center | 622x243

CSS規範規定,每個標籤都是有 display 屬性的。所以根據標籤元素的display屬性特點,可以將HTML標籤分為以下幾類:

  • 行內元素:包括 <a>、 <b>、<span>、<img>、<input>、<button>、<select>、<strong> 等標籤元素,其預設寬度是由內容寬度決定的。
  • 塊級元素:包括 <div>、<ul>、<ol>、<li>、<dl>、<dt>、<dd>、<h1>、<h2>、<h3>、<h4>、 <h5>、 <h6>、<p>、<table> 等標籤元素,其預設寬度為父元素的100%。
  • 空元素:例如 <br>、<hr>、 <link>、<meta>、<area>、 <base>、<col> 、<command>、<embed>、 <keygen>、 <param>、<source>、<track> 等不能顯示內容,甚至不會在頁面中出現,但是對頁面的解析有著其他重要作用的元素。

有時候使用語義化的標籤可能會造成一些相容性問題或者效能問題,比如頁面中使用 <table> 這個語義化標籤是會導致內容渲染較慢,因為<table>裡面的內容渲染是等表格內容全部解析完生成渲染樹後一次性渲染到頁面上的,如果表格內容較多,就可能產生渲染過程較慢的問題,因此我們有時可能需要通過其他的方式來模擬<table>元素,例如使用無序列表來模擬表格。

我們在書寫標籤的時候,還要注意加上必要的屬性,比如:<img> 標籤,需要加上 alt 和 title 屬性(注意alt屬性和title 屬性是有區別的,alt 屬性一般表示圖片載入失敗時提示的文字內容,title 屬性則指滑鼠放到元素上時顯示的提示文字)。加上這些屬性有助於搜尋引擎優化。

Web Component

image.png | center | 579x953

看下面的程式碼:jsfiddle.net/humtd6v1/

點選預覽

不知道你有沒有想過,為什麼這麼簡單的標籤定義能生成這樣兩個較複雜的選擇輸入介面呢?

image.png | left | 827x247

image.png | center | 620x371

Shadow DOM是HTML的一個規範,它允許瀏覽器開發者封裝自己的HTML標籤、CSS樣式和特定的JavaScript 程式碼,同時也可以讓開發人員建立類似<video>這樣的自定義一級標籤,建立這些新標籤內容和相關的API被稱為Web Component。

Shadow root是Shadow DOM的根節點,它和它的後代元素,都將對使用者隱藏,但是它們是實際存在的;Shadow tree為這個Shadow DOM包含的節點子樹結構,例如<div> 和<input>等; Shadow host則稱為Shadow DOM的容器元素,也就是宿主元素,即上面的標籤<input>。

新版本的瀏覽器提供了建立Shadow DOM的API,指定一個元素,然後可以使用document.createShadowRoot() 方法建立一個Shadow root,在Shadow root上可以任意通過DOM的基本操作API新增任意的Shadow tree,同時指定樣式和處理的邏輯,並將自己的API暴露出來。完成建立後需要通過document.registerElement()在文件中註冊元素,這樣Shadow DOM的建立就完成了。

比如:jsfiddle.net/t6wg2joe/

點選預覽

使用 Shadow DOM 有什麼好處呢?

  • 隔離 DOM:元件的 DOM 是獨立的(例如,document.querySelector() 不會返回元件 shadow DOM 中的節點)。
  • 作用域 CSS:shadow DOM 內部定義的 CSS 在其作用域內。樣式規則不會洩漏,頁面樣式也不會滲入。
  • 組合:為元件設計一個宣告性、基於標記的 API。
  • 簡化 CSS - 作用域 DOM 意味著您可以使用簡單的 CSS 選擇器,更通用的 id/類名稱,而無需擔心命名衝突。
  • 效率 - 將應用看成是多個 DOM 塊,而不是一個大的(全域性性)頁面。

image.png | center | 827x230

現行的元件都是開放式的,即最終生成的 HTML DOM 結構難以與元件外部的 DOM 進行有效結構區分,樣式容易互相混淆。Shadow-dom 的 封裝隱藏性為我們提供瞭解決這些問題的方法。在 Web 元件化的規範中也可以看到 Shadow-dom 的身影,使用具有良好密封性的 Shadow-dom 開發下一代 Web 元件將會是一種趨勢。

CSS

演進

CSS (Cascading Style Sheets)是隨著前端表現分離的提出而產生的,因為最早網頁內容的樣式都是通過center、strike等標籤或fontColor等屬性內容來體現的,而CSS提出使用樣式描述語言來表達頁面內容,而不是用HTML的標籤來表達。

image.png | center | 827x378

繼CSS1後,W3C在1998年釋出了CSS2規範,CSS2的出現主要是為了解決早期網頁開發過程中排版時表現分離的問題,後來隨著頁面表現的內容越來越複雜,瀏覽器平臺廠商繼續推動W3C組織對CSS規範進行更多的改進和完善,新增了例如 border-radius、 text-shadow、ransform、animation等更靈活的表現層特性,逐漸形成了一套全新的W3C標準,即CSS3。CSS3可以認為是在CSS2規範的基礎上進行補充和增強形成的,讓CSS體系更能適應現代瀏覽器的需要,擁有更強的表現能力,尤其對於移動端瀏覽器。

目前CSS4的草案也在制定中,CSS4 中更強大的選擇器、偽類和偽元素特性已經被曝光出來,但具體釋出時間仍不確定。

模組

從形式上來說,CSS3 標準自身已經不存在了。每個模組都被獨立的標準化。

image.png | center | 827x825

有些 CSS 模組已經十分穩定,其狀態為 CSSWG 規定的三個推薦品級之一:Candidate Recommendation(候選推薦), Proposed Recommendation(建議推薦)或 Recommendation(推薦)。表明這些模組已經十分穩定,使用時也不必新增字首。處於改善階段(refining phase)的規範已基本穩定。雖然還有可能被修改,但不會和當前的實現產生衝突。處於修正階段的模組沒處於改善階段的模組穩定。它們的語法一般還需要詳細審查,可能還會有些大變化,還有可能不相容之前的規範。

下面列出一些常用的模組:

CSS Color Module Level 3

增加 opacity 屬性,還有 hsl(), hsla(), rgba() 和 rgb() 函式來建立 <color> 值。

Selectors Level 3

增加:

  • 子串匹配的屬性選擇器, E[attribute^="value"], E[attribute&dollar;="value"], E[attribute*="value"]。
  • 新的偽類::target, :enabled 和 :disabled, :checked, :indeterminate, :root, :nth-child 和 :nth-last-child, :nth-of-type 和 :nth-last-of-type, :last-child, :first-of-type 和 :last-of-type, :only-child 和 :only-of-type, :empty, 和 :not。
  • 偽元素使用兩個冒號而不是一個來表示::after 變為 ::after, :before 變為 ::before, :first-letter 變為 ::first-letter, 還有 :first-line 變為 ::first-line。
  • 新的 general sibling combinator(普通兄弟選擇器) ( h1~pre )。

Media Queries

將之前的媒體型別 ( print, screen,……) 擴充為完整的語言, 允許使用類似 only screen 和 (color) 來實現 裝置媒體能力查詢功能。

CSS Backgrounds and Borders Module Level 3

增加:

  • 背景支援各種型別的 <image>, 並不侷限於之前定義的 url()。
  • 支援 multiple background images(多背景圖片)。
  • background-repeat 屬性的 space 和 round 值,還有支援兩個值的語法。
  • background-attachment local 值。
  • CSS background-origin,background-size 和 background-clip 屬性。
  • 支援帶弧度的 border corner(邊框角) CSS 屬性:border-radius,border-top-left-radius,border-top-right-radius,border-bottom-left-radius 和 border-bottom-right-radius 。
  • 支援邊框使用 <image>: border-image,border-image-source,border-image-slice,border-image-width,border-image-outset 和 border-image-repeat 。
  • 支援元素的陰影:box-shadow 。

CSS Values and Units Module Level 3

增加:

  • 定義了新的相對字型長度單位:rem 和 ch。
  • 定義了相對視口長度單位:vw,vh,vmax 和 vmin 。
  • 精確了絕對長度單位的實際尺寸,此前它們並非是絕對值,而是使用了 reference pixel(參考畫素) 來定義。
  • 定義 <angle>,<time>, <frequency>,<resolution>。
  • 規範 <color>,<image> 和 <position> 定義的值。
  • calc(),attr()和 toggle() 函式符號的定義。

CSS Flexible Box Layout Module

為 CSS display 屬性增加了 flexbox layout(伸縮盒佈局) 及多個新 CSS 屬性來控制它:flex,flex-align,flex-direction,flex-flow,flex-item-align,flex-line-pack,flex-order,flex-pack 和 flex-wrap。

CSS Fonts Module Level 3

增加:

  • 通過 CSS @font-face @ 規則來支援可下載字型。
  • 藉助 CSS font-kerning 屬性來控制 contextual inter-glyph spacing(上下文 inter-glyph 間距)。
  • 藉助 CSS font-language-override 屬性來選擇語言指定的字形。
  • 藉助 CSS font-feature-settings 屬性來選擇帶有 OpenType 特性的字形。
  • 藉助 CSS font-size-adjust 屬性來控制當使用 fallback fonts(備用字型) 時的寬高比。
  • 選擇替代字型,使用 CSS font-stretch,font-variant-alternates,font-variant-caps,font-variant-east-asian,font-variant-ligatures,font-variant-numeric,和 font-variant-position 屬性。還擴充套件了相關的 CSS font-variant 速記屬性,並引入了 @font-features-values @ 規則。
  • 當這些字型在 CSS font-synthesis 屬性中找不到時自動生成斜體或粗體的控制。

CSS Transitions

通過增加 CSS transition,transition-delay,transition-duration, transition-property,和 transition-timing-function 屬性來支援定義兩個屬性值間的 transitions effects(過渡效果)。

CSS Animations

允許定義動畫效果, 藉助於新增的 CSS animation, animation-delay, animation-direction, animation-duration, animation-fill-mode, animation-iteration-count, animation-name, animation-play-state, 和 animation-timing-function 屬性, 以及 @keyframes @ 規則。

CSS Transforms Level 1

增加:

  • 支援適用於任何元素的 bi-dimensional transforms(二維變形),使用 CSS transform 和 transform-origin 屬性。支援的變形有: matrix(),translate(),translateX(),translateY(, scale(),scaleX(),scaleY(),rotate(),skewX(),和 skewY()。
  • 支援適用於任何元素的 tri-dimensional transforms(三維變形),使用 CSS transform-style, perspective, perspective-origin, 和 backface-visibility 屬性和擴充套件的 transform 屬性,使用以下變形: matrix 3d(), translate3d(),translateZ(),scale3d(),scaleZ(),rotate3d(),rotateX() ,rotateY(),rotateZ(),和 perspective()。

樣式統一化

目前訪問Web網站應用時,使用者使用的瀏覽器版本較多,由於瀏覽器間核心實現的差異性,不同瀏覽器可能對同一元素標籤樣式的預設設定是不同的,如果不對CSS樣式進行統一化處理,可能會出現同一個網頁在不同瀏覽器下開啟時顯示不同或樣式不一致的問題。要處理這一問題,目前主要有三種實現思路:reset、normalize 和neat。

reset

reset的思路是將不同瀏覽器中標籤元素的預設樣式全部清除,消除不同瀏覽器下預設樣式的差異性。典型的reset預設樣式的程式碼如下:

html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed, 
figure, figcaption, footer, header, hgroup, 
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
    margin: 0;
    padding: 0;
    border: 0;
    font-size: 100%;
    font: inherit;
    vertical-align: baseline;
}複製程式碼

這種方式可以將不同瀏覽器上大多數標籤的內外邊距清除。消除預設樣式後重新定義元素樣式時,常常需要針對具體的元素標籤重寫樣式來覆蓋reset中的預設規則,所以這種情況下我們常常需要去重寫樣式來對元素新增各自的樣式規則。

normalize

Normalize.css主要是指:necolas.github.io/normalize.c… 這個庫。它是一種CSS reset的替代方案。相比reset,normalize.css 有如下特點:

  • 保護了有價值的預設值:Reset通過為幾乎所有的元素施加預設樣式,強行使得元素有相同的視覺效果。相比之下,Normalize.css保持了許多預設的瀏覽器樣式。這就意味著你不用再為所有公共的排版元素重新設定樣式。當一個元素在不同的瀏覽器中有不同的預設值時,Normalize.css會力求讓這些樣式保持一致並儘可能與現代標準相符合。
  • 修復了瀏覽器的bug:Normalize.css修復了常見的桌面端和移動端瀏覽器的bug。這往往超出了Reset所能做到的範疇。
  • 擁有詳細的文件。
  • 不會讓除錯工具變的雜亂:使用Reset最讓人困擾的地方莫過於在瀏覽器除錯工具中大段大段的繼承鏈,如下圖所示。在Normalize.css中就不會有這樣的問題。

image.png | left | 600x367

neat

neat可以認為是對上面兩種實現的綜合,因為我們通常不能保證網站介面上的所有元素的內外邊距都是確定的,又不想將所有樣式都清除後再進行覆蓋重寫。neat相當於一個折中的方案,任何前端專案都可以根據自己的標準寫出自己的neat來。

一個neat的實現:thx.github.io/cube/doc/ne…

現階段國內大部分團隊使用的是reset,國外大部分使用normalize,我個人偏向使用normalize。

預處理

CSS 自誕生以來,基本語法和核心機制一直沒有本質上的變化,它的發展幾乎全是表現力層面上的提升。如今網站的複雜度已經不可同日而語,原生 CSS 已經讓開發者力不從心。

當一門語言的能力不足而使用者的執行環境又不支援其它選擇的時候,這門語言就會淪為 “編譯目標” 語言。開發者將選擇另一門更高階的語言來進行開發,然後編譯到底層語言以便實際執行。於是,CSS 前處理器應運而生。

簡單來說,CSS 前處理器為我們帶來了幾項重要的能力:

  • 檔案切分
  • 模組化
  • 選擇符巢狀
  • 變數
  • 運算
  • 函式

LESS、SASS

image.png | center | 730x131

SassLess 是兩種 CSS 前處理器,擴充套件了 CSS 語法,目的都是為了讓 CSS 更容易維護。

Sass 有兩種語法,最常用是的 SCSS(Sassy CSS),是 CSS3 的超集。另一個語法是 SASS(老的,縮排語法,類 Python)。

image.png | center | 638x479

兩個處理器都很強大,相比較 Sass 功能更多,Less 更好上手。對於CSS複雜的專案,建議用 Sass。

PostCSS

PostCSS 是一個用 JavaScript 工具和外掛轉換 CSS 程式碼的工具。

PostCSS 擁有非常多的外掛,諸如自動為CSS新增瀏覽器字首的外掛autoprefixer、當前移動端最常用的px轉rem外掛px2rem,還有支援尚未成為CSS標準但特定可用的外掛cssnext,讓CSS相容舊版IE的CSSGrace,還有很多很多。著名的Bootstrap在下一個版本Bootstrap 5也將使用PostCSS作為樣式的基礎。

image.png | center | 827x463

現在更多的使用 PostCSS 的方式是對現有前處理器的補充,比如先通過Sass編譯,再加上autoprefixer自動補齊瀏覽器字首。

動畫

前端實現動畫的方式有很多種。比如一個方塊元素從左到右移動:

image.png | center | 400x154.42176870748298

Javascript 實現動畫

JavaScript直接實現動畫的方式在前端早期使用較多,其主要思想是通過JavaScript 的setInterval方法或setTimeout方法的回撥函式來持續呼叫改變某個元素的CSS樣式以達到元素樣式持續變化的結果,例如:jsfiddle.net/cm2vdbzt/1/

點選預覽

核心程式碼:

  let timer = setInterval(() => {
    if (left < window.innerWidth - 200) {
      element.style.marginLeft = left + 'px';
      left++;
    } else {
      clearInterval(timer);
    }
  }, 16);複製程式碼

JavaScript直接實現動畫也就是不斷執行setInterval 的回撥改變元素的marginLeft樣式屬性達動畫的效果,例如jQuery 的animate()方法就屬於這種實現方式。不過要注意的是,通過JavaScript實現動畫通常會導致頁面頻繁性重排重繪,很消耗效能,如果是稍微複雜的動畫,在效能較差的瀏覽器上,就會明顯感覺到卡頓,所以我們儘量避免使用它。

我們設定setInterval 的時間間隔是16ms,為什麼呢?一般認為人眼能辨識的流暢動畫為每秒60幀,這裡16ms比1000ms/60幀略小一點,所以這種情況下可以認為動畫是流暢的。在很多移動端動畫效能優化時,一般使用16ms來進行節流處理連續觸發的瀏覽器事件,例如對touchmove、 scroll 事件進行節流等。我們通過這種方式來減少持續性事件的觸發頻率,可以大大提升動畫的流暢性。

SVG 動畫

SVG又稱可伸縮向量圖形,原生支援一些動畫效果,通過組合可以生成較複雜的動畫,而且不需要使用JavaScript 參與控制。SVG動畫由SVG元素內部的元素屬性控制,通常通過 <set>、 <animate>、<animateColor>、<animateTransform>、<animateMotion> 這幾個元素來實現。<set>可以用於控制動畫延時,例如一段時間後設定SVG中元素的位置,就可以使用<set>在動畫中設定延時;<animate>可以對屬性的連續改變進行控制,例如實現左右移動動畫效果等;<animateColor> 表示顏色的變化,不過現在用<animate>就可以控制了,所以用的基本不多;<animateTransform>可以控制如縮放、旋轉等幾何變化;<animateMotion>則用於控制SVG內元素的移動路徑。

例如:jsfiddle.net/cm2vdbzt/2/

點選預覽

<svg id="box" width="800" height="400" version="1.1" xmIns="http://www.w3.org/2000/svg">
    <rect width="100" height="100" style="fill :rgb(255,0,0) ;">
        <set attributeName="x" attributeType="XML" to="100" begin="4s" />
        <animate attributeName="x" attributeType="XML" begin="0s" dur="4s" from="O" to="300" />
        <animate attributeName="y" attributeType="XML" begin="Os" dur="4s" from="O" to="O" />
        <animateTransform attributeName="transform" begin="Os" dur="4s" type="scale"
            from="1" to="2" repeatCount="1" />
        <animateMotion path="M10,80 q100, 120 120,20 q140,-50 160,0" begin="Os" dur="4s" repeatCount="1" />
    </rect>
</svg>複製程式碼

需要注意的是,SVG 內部元素的動畫只能在元素內進行,超出<svg>標籤元素,就可以認為是超出了動畫邊界。通過理解上面的程式碼可以看出,在網頁中<svg>元素內部定義了一個邊長100畫素的正方形,並且在4秒時間延時後開始向右移動,經過4秒時間向右移動300畫素。相對於JavaScript 直接控制動畫的方式,使用SVG的一個很大優勢是含有較豐富的動畫功能,原生可以繪製各種圖形、濾鏡和動畫,繪製的動畫為向量圖,而且實現動畫的原生元素依然是可被JavaScript呼叫的。然而另一方面,元素較多且複雜的動畫使用SVG渲染會比較慢,而且SVG格式的動畫繪製方式必須讓內容嵌入到HTML中使用。以前這種動畫實現的場景相對比較多,但隨著CSS3的出現,這種動畫實現方式相對使用得越來越少了。

CSS3 transition

CSS3出現後,增加了兩種CSS3實現動畫的方式:transition 和 animation。

演示:jsfiddle.net/cm2vdbzt/3/

點選預覽

    <style>
        * {
            margin: 0;
            padding: 0;
        }
        div {
            width: 200px;
            height: 200px;
            background-color: red;
            margin-left: 0;
            transition: all 3s ease-in-out 0s;
        }
        .right {
            margin-left: 400px;
            background-color: blue;
        }
    </style>
</head>
<body>
<div id="box"></div>
<script>
  let timer = setTimeout(function() {
    let element = document.getElementById('box');
    element.setAttribute('class', 'right');
  }, 500);
</script>複製程式碼

我們一般通過改變元素的起始狀態,讓元素的屬性自動進行平滑過渡產生動畫,當然也可以設定元素的任意屬性進行過渡變化。transition 應用於處理元素屬性改變時的過渡動畫,而不能應用於處理元素獨立動畫的情況,否則就需要不斷改變元素的屬性值來持續觸發動畫過程了。

在移動端開發中,直接使用transition 動畫會讓頁面變慢甚至變卡頓,所以我們通常通過新增 transform: translate3D(0, 0, 0)transform: translateZ(0) 來開啟移動端動畫的GPU加速,讓動畫過程更加流暢。

CSS3 animation

CSS3 animation的動畫則可以認為是真正意義上頁面內容的CSS3動畫,通過對關鍵幀和迴圈次數的控制,頁面標籤元素會根據設定好的樣式改變進行平滑過渡,而且關鍵幀狀態的控制一般是通過百分比來控制的,這樣我們就可以在這個過程中實現很多動畫的動作了。定義動畫的keyframes中from值和0%的意義是相同的,表示動畫的開始關鍵幀。to和100%的意義相同,表示動畫的結束關鍵幀。

演示:jsfiddle.net/cm2vdbzt/5/

點選預覽

   <style>
        div {
            width: 200px;
            height: 200px;
            background-color: red;
            margin-left: 0;
            animation: move 4s infinite;
        }
        @keyframes move {
            from {
                margin-left: 0;
            }
            50% {
                margin-left: 400px;
            }
            to {
                margin-left: 0;
            }
        }
    </style>複製程式碼

CSS3實現動畫的最大優勢是脫離JavaScript 的控制,而且能用到硬體加速,可以用來實現較複雜的動畫效果。

Canvas 動畫

<canvas>作為HTML5的新增元素,也可以藉助Web API實現頁面動畫。Canvas 動畫的實現思路和SVG的思路有點類似,都是藉助元素標籤來達到頁面動畫的效果,都需要藉助對應的一套API來實現,不過SVG的API可以認為主要是通過SVG元素內部的配置規則來實現的,而Canvas則是通過JavaScript API來實現的。需要注意的是,和SVG動畫一樣,Canvas動畫的進行只能在<canvas>元素內部,超出<canvas>元素邊界將不被顯示。

演示:jsfiddle.net/cm2vdbzt/7/

點選預覽

<canvas id="canvas" width="700" height="550">
    瀏覽器不支援canvas
</canvas>
<script>
  let canvas = document.getElementById('canvas');
  let ctx = canvas.getContext('2d');
  let left = 0;
  let timer = setInterval(function() {
    // 不斷清空畫布
    ctx.clearRect(0, 0, 700, 550);
    ctx.beginPath();
    //將顏色塊填充為紅色
    ctx.fillStyle = '#f00';
    //持續在新的位置上繪製矩形
    ctx.fillRect(left, 0, 100, 100);
    ctx.stroke();
    if (left > 700)
      clearInterval(timer);
    left += 1;
  }, 16);
</script>複製程式碼

元素DOM物件通過呼叫getContext ()可以獲取元素的繪製物件,然後通過clearRect不斷清空畫布並在新的位置上使用fillStyle繪製新矩形內容來實現頁面動畫效果。使用Canvas的主要優勢是可以應對頁面中多個動畫元素渲染較慢的情況,完全通過JavaScript 來渲染控制動畫的執行,這就避免了DOM效能較慢的問題,可用於實現較複雜的動畫。

requestAnimationFrame

requestAnimationFrame是前端表現層實現動畫的另一種API實現,它的原理和setTimeout及setInterval 類似,都是通過JavaScript 持續迴圈的方法呼叫來觸發動畫動作的,但是requestAnimationFrame是瀏覽器針對動畫專門優化而形成的API,在實現動畫方面效能比setTimeout及setInterval要好,可以將動畫每一步的操作方法傳入到requestAnimationFrame中,在每一次執行完後進行非同步回撥來連續觸發動畫效果。

演示:jsfiddle.net/cm2vdbzt/8/

點選預覽

<script>
  //獲取requestAnimationFrame API物件
  window.requestAnimationFrame = window.requestAnimationFrame;
  let element = document.getElementById('box');
  let left = 0;
  //自動執行持續性回撥
  requestAnimationFrame(step);

  // 持續改變元素位置
  function step() {
    if (left < window.innerWidth - 200)
      left += 1;
    element.style.marginLeft = left + 'px';
    requestAnimationFrame(step);
  }
</script>複製程式碼

可以看出,和setInterval方法類似,requestAnimationFrame 只是將回撥的方法傳入到自身的引數中處理執行,而不是通過setInterval 呼叫,其他的實現過程則基本一樣。

考慮到相容性的問題,在專案實踐中,一般我們在桌面瀏覽器端仍然推薦使用JavaScript直接實現動畫的方式或SVG動畫的實現方式,移動端則可以考慮使用CSS3 transition、CSS3 animation、canvas 或requestAnimationFrame。

響應式

通常認為,響應式設計是指根據不同裝置瀏覽器尺寸或解析度來展示不同頁面結構層、行為層、表現層內容的設計方式。

談到響應式設計網站,目前比較主流的實現方法有兩種:

  • 一是通過前喘或後端判斷userAgent來跳轉不同的頁面完成不同裝置瀏覽器的適配,也就是維護兩個或多個不同的網站,根據使用者裝置進行對應的跳轉
  • 二是使用mediaquery媒體查詢等手段,讓頁面根據不同裝置瀏覽器自動改變頁面的佈局和顯示,但不做跳轉。

image.png | center | 450x567

兩種方式各有利弊:

第一種方案:
Pros:可以根據不同的裝置載入相應的網頁資源,針對移動端的瀏覽器可以請求載入更加優化後的執行指令碼或更小的靜態資源。移動端和PC端頁面差異比較大也無所謂。
Cons:需要開發並維護至少兩個站點;多了一次跳轉。

第二種方案:
Pros:桌面瀏覽器和移動端瀏覽器使用同一個站點域名來載入內容,只需要開發維護一個站點就可以了。適用於訪問量較小、效能要求不高或PC端和移動端差別不大的應用場景。
Cons:移動端可能會載入到冗餘或體積較大的資源;只實現了內容佈局顯示的適應,但是要做更多差異性的功能比較難。

響應式頁面設計一直是一個很難完美解決的問題,因為多多少少都存在這些問題:

  • 能否使用同一個站點域名避免跳轉的問題
  • 能否保證移動端載入的資源內容最優
  • 如何做移動端和桌面端瀏覽器的差異化功能
  • 如何根據更多的資訊進行更加靈活的判斷,而不僅僅是userAgent

通過合理的開發方式和網站訪問架構設計,再加上適當的取捨,可以解決上述的大部分問題。

結構層響應式

結構層響應式設計可以理解成HTML內容的自適應渲染實現方式,即根據不同的裝置瀏覽器渲染不同的頁面內容結構,而不是直接進行頁面跳轉。這裡頁面中結構層渲染的方式可能不同,包括前端渲染資料和後端渲染資料,這樣主要就有兩種不同的設計思路:一是頁面內容是在前端渲染,二是頁面內容在後端渲染。

現在很多網站使用了前後分離,前端渲染頁面,為了保證我們使用移動端開啟的頁面載入到相對最優的頁面資源內容,我們可以使用非同步的方式來載入CSS檔案和JS檔案,這樣就可以做到根據移動端頁面和桌面端頁面載入到不同的資源內容了。

除了前端資料渲染的方式,目前還有一部分網站的內容生成使用了後端渲染的實現方式。這種情況的處理方式其實可以做到更優化,只要儘可能將桌面端和移動的業務層模板分開維護就可以了。在模板選擇判斷時仍是可以通過userAgent甚至URL引數來進行的。

表現層響應式

響應式佈局是根據瀏覽器寬度、解析度、橫屏、豎屏等情況來自動改變頁面元素展示的一種佈局方式,一般可以使用柵格方式來實現,實現思路有兩種:一種是桌面端瀏覽器優先,擴充套件到移動端瀏覽器適配;另一種則是以移動端瀏覽器優先,擴充套件到桌面端瀏覽器適配。由於移動端的網路和計算資源相對較少,所以一般比較推薦從移動端擴充套件到桌面端的方式進行適配,這樣就避免了在移動端載入冗餘的桌面端CSS樣式內容。

螢幕適配佈局則是主要針對移動端的,由於目前移動端裝置螢幕大小各不相同,螢幕適配佈局是為了實現網頁內容根據移動端裝置螢幕大小等比例縮放所提出的一種佈局計算方式。

表現層的響應式,主要是通過響應式佈局和螢幕適配佈局,來完成網頁針對不同裝置的適配。一般包含如下技術點和設計原則:

設定視口

元視口程式碼會指示瀏覽器如何對網頁尺寸和縮放比例進行控制。

<meta name="viewport" content="width=device-width, initial-scale=1.0">複製程式碼

為了提供最佳體驗,移動瀏覽器會以桌面裝置的螢幕寬度(通常大約為 980 畫素,但不同裝置可能會有所不同)來呈現網頁,然後再增加字型大小並將內容調整為適合螢幕的大小,從而改善內容的呈現效果。對使用者來說,這就意味著,字型大小可能會不一致,他們必須點按兩次或張合手指進行縮放,才能檢視內容並與之互動。

使用元視口值 width=device-width 指示網頁與螢幕寬度(以裝置無關畫素為單位)進行匹配。這樣一來,網頁便可以重排內容,使之適合不同的螢幕大小。

image.png | left | 400x711image.png | left | 400x711

根據視口大小應用媒體查詢

媒體查詢是實現響應式的最主要依據。通過媒體查詢語法,我們可以建立可根據裝置特點應用的規則。

@media (query) {
  /* CSS Rules used when query matches */
}複製程式碼

儘管我們可以查詢多個不同的專案,但自適應網頁設計最常使用的專案為:min-width、max-width、min-height 和 max-height。比如:

<link rel="stylesheet" media="(max-width: 640px)" href="max-640px.css">
<link rel="stylesheet" media="(min-width: 640px)" href="min-640px.css">
<link rel="stylesheet" media="(orientation: portrait)" href="portrait.css">
<link rel="stylesheet" media="(orientation: landscape)" href="landscape.css">
<style>
  @media (min-width: 500px) and (max-width: 600px) {
    h1 {
      color: fuchsia;
    }

    .desc:after {
      content:" In fact, it's between 500px and 600px wide.";
    }
  }
</style>複製程式碼
  • 當瀏覽器寬度介於 0 畫素640 畫素之間時,系統將會應用 max-640px.css。
  • 當瀏覽器寬度介於 500 畫素600 畫素之間時,系統將會應用 @media。
  • 當瀏覽器寬度為 640 畫素或大於此值時,系統將會應用 min-640px.css。
  • 當瀏覽器寬度大於高度時,系統將會應用 landscape.css。
  • 當瀏覽器高度大於寬度時,系統將會應用 portrait.css。

使用相對單位

與固定寬度的版式相比,自適應設計的主要概念基礎是流暢性和比例可調節性。使用相對衡量單位有助於簡化版式,並防止無意間建立對視口來說過大的元件。

常用的相對單位有:

  • 百分比%。
  • em:根據使用它的元素的大小決定(很多人錯誤以為是根據父類元素,實際上是使用它的元素繼承了父類的屬性才會產生的錯覺)。
  • rem:基於html元素的字型大小來決定。

image.png | center | 827x398

由於em計算比較複雜,有很多不確定性,現在基本上不怎麼使用了。

選擇斷點

以從小螢幕開始、不斷擴充套件的方式選擇主要斷點,儘量根據內容建立斷點,而不要根據具體裝置、產品或品牌來建立。

一般來說,常選取的端點可以參考Bootstrap:

image.png | center | 600x589.3860561914672

柵格化佈局

image.png | center | 827x852

柵格化佈局(Grid Layout)通常會把螢幕寬度分成多個固定的柵格,比如12個,它有助於內容的呈現和實現響應式佈局,比如使用Bootstrap框架,柵格就會根據不同裝置自適應排列。

1_Amme_PqOYttyGUO5aSCYwg.gif | center | 827x635

響應式影象

image.png | center | 400x278

根據統計,目前主要網站60%以上的流量資料來自圖片,所以如何在保證使用者訪問網頁體驗不降低的前提下儘可能地降低網站圖片的輸出流量具有很重要的意義。

通常在我們手機訪問網頁時,請求的圖片可能還是載入了與桌面端瀏覽器相同的大圖,件體積大,消耗流量多,請求延時長。媒體響應式要解決的一個關鍵問題就是讓瀏覽器上的展示媒體內容尺寸根據螢幕寬度或螢幕解析度進行自適應調節。我們需要根據瀏覽器裝置螢幕寬度和螢幕的解析度來載入不同大小尺寸的圖片,避免在移動端上載入體積過大的資源。一般有如下方式來處理圖片:

影象使用相對尺寸

因為 CSS 允許內容溢位其容器,因此一般需要使用 max-width: 100% 來保證影象及其他內容不會溢位。

img, embed, object, video {
  max-width: 100%;
}複製程式碼
使用 srcset 來增強 img
<img src="lighthouse-200.jpg" sizes="50vw"
     srcset="lighthouse-100.jpg 100w, lighthouse-200.jpg 200w,
             lighthouse-400.jpg 400w, lighthouse-800.jpg 800w,
             lighthouse-1000.jpg 1000w, lighthouse-1400.jpg 1400w,
             lighthouse-1800.jpg 1800w" alt="a lighthouse">複製程式碼

在不支援 srcset 的瀏覽器上,瀏覽器只需使用 src 屬性指定的預設影象檔案。

用 picture 實現藝術指導

picture 元素定義了一個宣告性解決辦法,可根據裝置大小、裝置解析度、螢幕方向等不同特性來提供一個影象的多個版本。

<picture>
  <source media="(min-width: 800px)" srcset="head.jpg, head-2x.jpg 2x">
  <source media="(min-width: 450px)" srcset="head-small.jpg, head-small-2x.jpg 2x">
  <img src="head-fb.jpg" srcset="head-fb-2x.jpg 2x" alt="a head carved out of wood">
</picture>複製程式碼
通過媒體查詢指定影象
.example {
  height: 400px;
  background-image: url(small.png);
  background-repeat: no-repeat;
  background-size: contain;
  background-position-x: center;
}

@media (min-width: 500px) {
  body {
    background-image: url(body.png);
  }
  .example {
    background-image: url(large.png);
  }
}複製程式碼

媒體查詢不僅影響頁面佈局,還可以用於有條件地載入影象。

媒體查詢可根據裝置畫素比建立規則,可以針對 2x 和 1x 螢幕分別指定不同的影象。

.sample {
  width: 128px;
  height: 128px;
  background-image: url(icon1x.png);
}

@media (min-resolution: 2dppx), /* Standard syntax */ 
(-webkit-min-device-pixel-ratio: 2)  /* Safari & Android Browser */ 
{
  .sample {
    background-size: contain;
    background-image: url(icon2x.png);
  }
}複製程式碼
為圖示使用 SVG

儘可能使用 SVG 圖示,某些情況下,可以使用 unicode 字元。比如:

You're a super &#9733;複製程式碼

You're a super ★

優化影象

選擇正確的影象格式:

  • 攝影影象使用 JPG。
  • 徽標和藝術線條等向量插畫及純色圖形使用 SVG。 如果向量插畫不可用,試試 WebP 或 PNG。
  • 使用 PNG 而非 GIF,因為前者可以提供更豐富的顏色和更好的壓縮比。
  • 長動畫考慮使用 <video>,它能提供更好的影象質量,還允許使用者控制回放。

儘量將圖片放在CDN。

在可以接受的情況下,儘可能的壓縮圖片到最小。tinypng.com/

使用 image sprites,將許多影象合併到一個“精靈表”影象中。 然後,通過指定元素背景影象(精靈表)以及指定用於顯示正確部分的位移,可以使用各個影象。

image.png | center | 190x352

延緩載入

在主要內容載入和渲染完成之後載入影象。或者內容可見後才載入。

避免使用影象

如果可以,不要使用影象,而是使用瀏覽器的原生功能實現相同或類似的效果。比如CSS效果:

image.png | left | 827x155

<style>
  div#noImage {
    color: white;
    border-radius: 5px;
    box-shadow: 5px 5px 4px 0 rgba(9,130,154,0.2);
    background: linear-gradient(rgba(9, 130, 154, 1), rgba(9, 130, 154, 0.5));
  }
</style>複製程式碼

展望

目前CSS的成熟標準版本是CSS3, 而且在移動端使用較多。CSS4的規範仍在制定中,CSS4的處境將會比較尷尬,類似於現在的ES6,釋出後不能相容仍需要轉譯。

image.png | center | 827x379

就目前來看,CSS4新新增的特性優勢並不明顯(最主要的實用的是一些新的選擇器,比如 not),很多特性暫時來說實用性不強,而且不如現有的預處理語法。所以只能看它後面的發展情況了。

Javascript

演進

image.png | left | 827x174

JavaScript 因為網際網路而生,緊隨著瀏覽器的出現而問世。

1994年12月,Navigator釋出了1.0版,市場份額一舉超過90%。Netscape 公司很快發現,Navigator瀏覽器需要一種可以嵌入網頁的指令碼語言,用來控制瀏覽器行為。比如,如果使用者忘記填寫“使用者名稱”,就點了“傳送”按鈕,到伺服器再發現這一點就有點太晚了,最好能在使用者發出資料之前,就告訴使用者“請填寫使用者名稱”。這就需要在網頁中嵌入小程式,讓瀏覽器檢查每一欄是否都填寫了。

1995年,Netscape公司僱傭了程式設計師Brendan Eich開發這種網頁尾本語言。Brendan Eich只用了10天,就設計完成了這種語言的第一版。

1996年8月,微軟模仿JavaScript開發了一種相近的語言,取名為JScript,Netscape公司面臨喪失瀏覽器指令碼語言的主導權的局面。Netscape公司決定將JavaScript提交給國際標準化組織ECMA(European Computer Manufacturers Association),希望JavaScript能夠成為國際標準,以此抵抗微軟。

1997年7月,ECMA組織釋出262號標準檔案(ECMA-262)的第一版,規定了瀏覽器指令碼語言的標準,並將這種語言稱為ECMAScript。這個版本就是ECMAScript 1.0版。因此,ECMAScript和JavaScript的關係是,前者是後者的規格,後者是前者的一種實現。在日常場合,這兩個詞是可以互換的。

1999年12月,ECMAScript 3.0版釋出,成為JavaScript的通行標準,得到了廣泛支援。

2009年12月,ECMAScript 5.0版正式釋出(ECMAScript 4.0爭議太大被廢棄,ECMAScript 3.1改名為ECMAScript 5)。

2011年6月,ECMAscript 5.1版釋出,並且成為ISO國際標準(ISO/IEC 16262:2011)。到了2012年底,所有主要瀏覽器都支援ECMAScript 5.1版的全部功能。

2015年6月,ECMAScript 6正式釋出,並且更名為“ECMAScript 2015”。

2017年6月,ECMAScript 2017 標準釋出,正式引入了 async 函式。

2017年11月,所有主流瀏覽器全部支援 WebAssembly,這意味著任何語言都可以編譯成 JavaScript,在瀏覽器執行。

ECMAScript 6+

image.png | center | 827x425

<div data-type="alignment" data-value="center" style="text-align:center">
<div data-type="p">

<a target="_blank" rel="noopener noreferrer nofollow" href="http://es6katas.org/" class="bi-link">http://es6katas.org/</a>複製程式碼

</div>
</div>

ES6 主要新增瞭如下特性:

塊級作用域變數宣告

之前JS的作用域非常的奇怪,只有全域性作用域和函式作用域,沒有塊級作用域。比如:var 命令會發生”變數提升“現象,即變數可以在宣告之前使用,值為undefined。var 還可以重複宣告。

ES6 的let實際上為 JavaScript 新增了塊級作用域。

{
  let a = 10;
  var b = 1;
}

a // ReferenceError: a is not defined.
b // 1複製程式碼

ES5 只有兩種宣告變數的方法:var命令和function命令。ES6 除了新增let和const命令,還有另外兩種宣告變數的方法:import命令和class命令。所以,ES6 一共有 6 種宣告變數的方法。

字串模板

字串模板設計主要來自其他語言和前端模板的設計思想,即當有字串內容和變數混合連線時,可以使用字串模板進行更高效的程式碼書寫並保持程式碼的格式和整潔性。如果沒有字串模板,我們依然需要像以前一樣藉助“字串+操作符”拼接或陣列join()方法來連線多個字串變數。

// ES5
$('#result').append(
  'There are <b>' + basket.count + '</b> ' +
  'items in your basket, ' +
  '<em>' + basket.onSale +
  '</em> are on sale!'
);

// ES6
$('#result').append(`
  There are <b>${basket.count}</b> items
   in your basket, <em>${basket.onSale}</em>
  are on sale!
`);複製程式碼

解構賦值

ES6 允許按照一定模式,從陣列和物件中提取值,對變數進行賦值,這被稱為解構(Destructuring)。

let a = 1;
let b = 2;
let c = 3;

let [a, b, c] = [1, 2, 3]; // ES6

let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]

let { bar, foo } = { foo: "aaa", bar: "bbb" };
foo // "aaa"
bar // "bbb"複製程式碼

一道前端面試題:怎樣用一行程式碼把陣列中的元素去重?

let newArr = [...new Set(sourceArr)];複製程式碼

陣列新特性

之前JS的Array大概有如下這些方法:

image.png | center | 827x522

ES6又增加了很多實用的方法:

Array.from('hello'); // ['h', 'e', 'l', 'l', 'o']

Array.of(3, 11, 8); // [3,11,8]

[1, 4, -5, 10].find((n) => n < 0); // -5

[1, 5, 10, 15].findIndex((value) => value > 9); // 2

['a', 'b', 'c'].fill(7); // [7, 7, 7]

for (let index of ['a', 'b'].keys()) {
  console.log(index);
}
// 0
// 1

[1, 2, 3].includes(2);     // true

[1, 2, [3, 4]].flat();  // [1, 2, 3, 4]

[2, 3, 4].flatMap((x) => [x, x * 2]);  // [2, 4, 3, 6, 4, 8]
複製程式碼

函式新特性

// 引數預設值
function log(x, y = 'World') {
  console.log(x, y);
}

// 箭頭函式
var sum = (num1, num2) => num1 + num2;

// 雙冒號運算子
foo::bar;
// 等同於
bar.bind(foo);複製程式碼

箭頭函式有幾個使用注意點。

  • 函式體內的this物件,就是定義時所在的物件,而不是使用時所在的物件。
  • 不可以當作建構函式,也就是說,不可以使用new命令,否則會丟擲一個錯誤。
  • 不可以使用arguments物件,該物件在函式體內不存在。如果要用,可以用 rest 引數代替。
  • 不可以使用yield命令,因此箭頭函式不能用作 Generator 函式。

函式繫結運算子是並排的兩個冒號(::),雙冒號左邊是一個物件,右邊是一個函式。該運算子會自動將左邊的物件,作為上下文環境(即this物件),繫結到右邊的函式上面。

物件新特性

// 屬性的簡潔表示法
function f(x, y) {
  return { x, y };
}

// 等同於
function f(x, y) {
  return { x: x, y: y };
}

// 屬性名錶達式
obj['a' + 'bc'] = 123;

// Object.is() 比較兩個值是否嚴格相等
Object.is(NaN, NaN) // true

// Object.assign() 物件合併,後面的屬性會覆蓋前面的屬性
Object.assign({ a: 1 }, { b: 2 }, { c: 3 });

// Object.keys()
var obj = { foo: 'bar', baz: 42 };
Object.keys(obj)
// ["foo", "baz"]複製程式碼

類 Class

ES6 提供了更接近傳統語言的寫法,引入了 Class(類)這個概念,作為物件的模板。通過class關鍵字,可以定義類。

ES6 的class可以看作只是一個語法糖,他的內部實現和 Java 之類的語言差別很大。傳統的類被例項化時,它的行為會被複制到例項中。類被繼承時,行為也會被複制到子類中。多型看起來似乎是從子類引用父類,但是本質上引用的其實是複製的結果。

javascript 中的類機制有一個核心區別,就是不會進行復制,物件之間是通過內部的 [[Prototype]] 鏈關聯的。

new 操作符在 JavaScript 當中本身就是一個充滿歧義的東西,只是貼合程式設計師習慣而已。

執行new fn()會進行以下簡化過程:

  • 新建一個物件,記作o。
  • 把o.__proto__指向fn.prototype(如果fn.prototype不是一個Object,則指向Object.prototype)。
  • 執行fn,並用o作為this(即內部實現的fn.call(this))。如果fn返回是一個object,則返回object, 否則把o返回。
//定義一個函式,正常函式會具有__call__, __construct__
//同時Parent.__proto__指向Function.prototype
function Parent() {
  this.sayAge = function() {
    console.log('age is: ' + this.age);
  }
}

//原型上新增一個方法
Parent.prototype.sayParent = function() {
  console.log('this is Parent Method');
}

//定義另一個函式
function Child(firstname) {

  //這裡就是呼叫Parent的__call__, 並且傳入this
  //而這裡的this,是Child接受new時候生成的物件
  //因此,這一步會給生成的Child生成的例項新增一個sayAge屬性
  Parent.call(this);

  this.fname = firstname;
  this.age = 40;
  this.saySomething = function() {
    console.log(this.fname);
    this.sayAge();
  }
}

//這一步就是new的呼叫,按原理分步來看
//1. 新建了個物件,記作o
//2. o.__proto__ = Parent.prototype, 因此o.sayParent會訪問到o.__proto__.sayParent(原型鏈查詢機制)
//3. Parent.call(o), 因此o也會有個sayAge屬性(o.sayAge)
//4. Child.prototype = o, 因此 Child.prototype 通過o.__proto__ 這個原型鏈具有了o.sayParent屬性,同時通過o.sayAge 具有了sayAge屬性(也就是說Child.prototype上具有sayAge屬性,但沒有sayParent屬性,但是通過原型鏈,也可以訪問到sayParent屬性)
Child.prototype = new Parent();

//這也是一步new呼叫
//1. 新建物件,記作s
//2. s.__proto__ = Child.prototype, 此時s會具有sayAge屬性以及sayParent這個原型鏈上的屬性
//3. Child.call(s), 執行後, 增加了fname, age, saySomething屬性, 同時由於跑了Parent.call(s), s還具有sayAge屬性, 這個屬性是s身上的, 上面那個sayAge是Child.prototype上的, 即s.__proto__上的。
//4. child = s
var child = new Child('張')

//child本身屬性就有,執行
child.saySomething();

//child本身屬性沒有, 去原型鏈上看, child.__proto__ = s.__proto__ = Child.prototype = o, 這裡沒找到sayParent, 繼續往上找, o.__proto__ = Parent.prototype, 這裡找到了, 執行(第二層原型鏈找到)
child.sayParent();
複製程式碼

之前的寫法:

function Point(x, y) {
  this.x = x;
  this.y = y;
}

Point.prototype.toString = function () {
  return '(' + this.x + ', ' + this.y + ')';
};

var p = new Point(1, 2);複製程式碼

ES6的寫法:

class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}複製程式碼

事實上,類的所有方法都定義在類的prototype屬性上面。

class Point {
  constructor() {
    // ...
  }

  toString() {
    // ...
  }

  toValue() {
    // ...
  }
}

// 等同於

Point.prototype = {
  constructor() {},
  toString() {},
  toValue() {},
};複製程式碼

由於類的方法都定義在prototype物件上面,所以類的新方法可以新增在prototype物件上面。也就是說類的方法可以隨時增加。

class Point {
  constructor(){
    // ...
  }
}

Object.assign(Point.prototype, {
  toString(){},
  toValue(){}
});複製程式碼

constructor方法是類的預設方法,通過new命令生成物件例項時,自動呼叫該方法。一個類必須有constructor方法,如果沒有顯式定義,一個空的constructor方法會被預設新增。

class Point {
}

// 等同於
class Point {
  constructor() {}
}複製程式碼

Class 可以通過extends關鍵字實現繼承:

class Point {
}

class ColorPoint extends Point {
}複製程式碼

子類必須在constructor方法中呼叫super方法,否則新建例項時會報錯。ES5 的繼承,實質是先創造子類的例項物件this,然後再將父類的方法新增到this上面(Parent.apply(this))。ES6 的繼承機制完全不同,實質是先將父類例項物件的屬性和方法,加到this上面(所以必須先呼叫super方法),然後再用子類的建構函式修改this。

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // 呼叫父類的constructor(x, y)
    this.color = color;
  }

  toString() {
    return this.color + ' ' + super.toString(); // 呼叫父類的toString()
  }
}複製程式碼

Symbol

ES5 的物件屬性名都是字串,這容易造成屬性名的衝突。如果有一種機制,保證每個屬性的名字都是獨一無二的就好了,這樣就從根本上防止屬性名的衝突。這就是 ES6 引入Symbol的原因。

ES6 引入了一種新的原始資料型別Symbol,表示獨一無二的值。它是 JavaScript 語言的第七種資料型別,前六種是:undefined、null、布林值(Boolean)、字串(String)、數值(Number)、物件(Object)。

let s1 = Symbol('foo');
let s2 = Symbol('bar');

s1 // Symbol(foo)
s2 // Symbol(bar)

s1.toString() // "Symbol(foo)"
s2.toString() // "Symbol(bar)"複製程式碼

由於每一個 Symbol 值都是不相等的,這意味著 Symbol 值可以作為識別符號,用於物件的屬性名,就能保證不會出現同名的屬性。這對於一個物件由多個模組構成的情況非常有用,能防止某一個鍵被不小心改寫或覆蓋。

let mySymbol = Symbol();

// 第一種寫法
let a = {};
a[mySymbol] = 'Hello!';

// 第二種寫法
let a = {
  [mySymbol]: 'Hello!'
};

// 第三種寫法
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });

// 以上寫法都得到同樣結果
a[mySymbol] // "Hello!"複製程式碼

Set 和 Map

也許很多人會疑惑,既然陣列和物件可以儲存任何型別的值,為什麼還需要Map和Set呢?考慮幾個問題:一是物件的鍵名一般只能是字串,而不能是另一個物件;二是物件沒有直接獲取屬性個數等這些方便操作的方法;三是我們對於物件的任何操作都需要進入物件的內部資料中完成,例如查詢、刪除某個值必須迴圈遍歷物件內部的所有鍵值對來完成。總之我們使用簡單物件的方式仍然顯得很低效,沒有一個高效的方法集來管理物件資料。

因此ECMAScript 6增加了Map、Set、WeakMap、WeakSet, 試圖彌補這些不足。這樣我們就可以使用它們提供的has. add、delete、 clear 等方法來管理和運算元據集合,而不用具體進入到物件內部去操作了,這種情況下Map和Set就類似一個可用於儲存資料的黑盒,我們只管向裡面高效存取資料,而不用知道它裡面的結構是怎樣的。我們甚至可以這樣理解:集合型別是對物件的增強型別,是一類使資料管理操作更加高效的物件型別。

Set 類似於陣列,但是成員的值都是唯一的,沒有重複的值。

const set = new Set([1, 2, 3, 4, 4]);
[...set]
// [1, 2, 3, 4]複製程式碼

WeakSet 的成員只能是物件,而不能是其他型別的值。WeakSet 中的物件都是弱引用,即垃圾回收機制不考慮 WeakSet 對該物件的引用,也就是說,如果其他物件都不再引用該物件,那麼垃圾回收機制會自動回收該物件所佔用的記憶體,不考慮該物件還存在於 WeakSet 之中。

一道筆試題:用一行程式碼實現陣列去掉重複元素、從小到大排序、去掉所有偶數。

let arr = [13, 4, 8, 14, 1, 12, 17, 2, 7, 8, 13, 9, 6, 4, 9, 3, 2, 1, 17, 19, 12, 4, 14];

let arr2 = [...new Set(arr)].filter(v => v % 2 !== 0).sort((a, b) => a - b);

console.log(arr2); // [ 1, 3, 7, 9, 13, 17, 19 ]複製程式碼

Object 結構提供了“字串—值”的對應,Map 結構提供了“值—值”的對應,是一種更完善的 Hash 結構實現。如果需要“鍵值對”的資料結構,Map 比 Object 更合適。

const m = new Map();
const o = {p: 'Hello World'};

m.set(o, 'content')
m.get(o) // "content"

m.has(o) // true
m.delete(o) // true
m.has(o) // false複製程式碼

WeakMap只接受物件作為鍵名(null除外),不接受其他型別的值作為鍵名。WeakMap的鍵名所指向的物件,不計入垃圾回收機制。

WeakSet 和 WeakMap 結構主要有助於防止記憶體洩漏。

模組 Module

歷史上,JavaScript 一直沒有模組(module)體系,無法將一個大程式拆分成互相依賴的小檔案,再用簡單的方法拼裝起來。其他語言基本上都有這項功能,這對開發大型的、複雜的專案形成了巨大障礙。

在 ES6 之前,社群制定了一些模組載入方案,最主要的有 CommonJS 和 AMD 兩種。前者用於伺服器,後者用於瀏覽器。ES6 在語言標準的層面上,實現了模組功能,而且實現得相當簡單,完全可以取代 CommonJS 和 AMD 規範。

// profile.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;

export {firstName, lastName, year};

// main.js
import {firstName, lastName, year} from './profile.js';

function setName(element) {
  element.textContent = firstName + ' ' + lastName;
}複製程式碼

Promise

非同步程式設計對 JavaScript 語言很重要。Javascript 語言的執行環境是“單執行緒”的,如果沒有非同步程式設計,根本沒法用,非卡死不可。ES6 誕生以前,非同步程式設計的方法,大概有下面四種。

  • 回撥函式
  • 事件監聽
  • 釋出/訂閱
  • Promise 物件

所謂回撥函式,就是把任務的第二段單獨寫在一個函式裡面,等到重新執行這個任務的時候,就直接呼叫這個函式。

fs.readFile('/etc/passwd', 'utf-8', function (err, data) {
  if (err) throw err;
  console.log(data);
});複製程式碼

Callback Hell:使用大量回撥函式時,程式碼閱讀起來晦澀難懂,並不直觀。

image.png | center | 638x479

Promise 是非同步程式設計的一種解決方案,比傳統的解決方案“回撥函式和事件”更合理和更強大。它由社群最早提出和實現,ES6 將其寫進了語言標準,統一了用法,原生提供了Promise物件。所謂Promise,簡單說就是一個容器,裡面儲存著某個未來才會結束的事件(通常是一個非同步操作)的結果。

const promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* 非同步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

promise
  .then(function(value) { console.log(value) })
  .catch(function(error) { console.log(error) });複製程式碼

resolve函式的作用是,將Promise物件的狀態從“未完成”變為“成功”(即從 pending 變為 resolved),在非同步操作成功時呼叫,並將非同步操作的結果,作為引數傳遞出去;reject函式的作用是,將Promise物件的狀態從“未完成”變為“失敗”(即從 pending 變為 rejected),在非同步操作失敗時呼叫,並將非同步操作報出的錯誤,作為引數傳遞出去。

Promise例項生成以後,可以用then和catch方法分別指定resolved狀態和rejected狀態的回撥函式。

舉個例子,我們可以把老的Ajax GET呼叫方式封裝成Promise:

function get(url) {
  return new Promise(function(resolve, reject) {
    var req = new XMLHttpRequest();
    req.open('GET', url);

    req.onload = function() {
      if (req.status == 200) {
        resolve(req.response);
      }
      else {
        reject(Error(req.statusText));
      }
    };

    req.onerror = function() {
      reject(Error("Network Error"));
    };

    req.send();
  });
}複製程式碼

然後就可以這樣使用:

get('story.json')
.then(function(response) {
  console.log("Success!", response);
})
.catch(function(error) {
  console.error("Failed!", error);
})複製程式碼

非同步是JS的核心,幾乎所有前端面試都會涉及到Promise的內容。

迭代器 Iterator

迭代器(Iterator)是一種介面,為各種不同的資料結構提供統一的訪問機制。任何資料結構只要部署 Iterator 介面,就可以完成遍歷操作(即依次處理該資料結構的所有成員)。

迭代器其實就是維護一個當前的指標,這個指標可以指向當前的元素,可以返回當前所指向的元素,可以移到下一個元素的位置,通過這個指標可以遍歷容器的所有元素。

Iterator 的作用有三個:一是為各種資料結構,提供一個統一的、簡便的訪問介面;二是使得資料結構的成員能夠按某種次序排列;三是 ES6 創造了一種新的遍歷命令for...of迴圈,Iterator 介面主要供for...of消費。

let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator]();

iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }複製程式碼

當迴圈迭代中每次單步迴圈操作都不一樣時,使用Interator就很有用了。

生成器 Generator

如果對Iterator理解較深的話,那麼你會發現生成器Generator和Interator的流程是有點類似的。但是,Generator 不是針對物件上內容的遍歷控制,而是針對函式內程式碼塊的執行控制,如果將一個特殊函式的程式碼使用yield關鍵字來分割成多個不同的程式碼段,那麼每次Generator呼叫next()都只會執行yield關鍵字之間的一段程式碼。

Generator可以認為是一個可中斷執行的特殊函式,宣告方法是在函式名後面加上*來與普通函式區分。

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();
hw.next(); // { value: 'hello', done: false }
hw.next(); // { value: 'world', done: false }
hw.next(); // { value: 'ending', done: true }
hw.next(); // { value: undefined, done: true }複製程式碼

呼叫 Generator 函式後,該函式並不執行,返回的也不是函式執行結果,而是一個指向內部狀態的指標物件。下一步,必須呼叫遍歷器物件的next方法,使得指標移向下一個狀態。也就是說,每次呼叫next方法,內部指標就從函式頭部或上一次停下來的地方開始執行,直到遇到下一個yield表示式(或return語句)為止。換言之,Generator 函式是分段執行的,yield表示式是暫停執行的標記,而next方法可以恢復執行。

Generator 非同步應用

回到之前說過的非同步。

對於其他程式語言,早有非同步程式設計的解決方案(其實是多工的解決方案)。其中有一種叫做"協程"(coroutine),意思是多個執行緒互相協作,完成非同步任務。它的執行流程大致如下。

  • 第一步,協程A開始執行。
  • 第二步,協程A執行到一半,進入暫停,執行權轉移到協程B。
  • 第三步,(一段時間後)協程B交還執行權。
  • 第四步,協程A恢復執行。

上面流程的協程A,就是非同步任務,因為它分成兩段(或多段)執行。比如你打電話就是A,吃蛋糕就是B,講一句電話,吃一口蛋糕。

舉例來說,讀取檔案的協程寫法如下。

function* asyncJob() {
  // ...其他程式碼
  var f = yield readFile(fileA);
  // ...其他程式碼
}複製程式碼

上面程式碼的函式asyncJob是一個協程,它的奧妙就在其中的yield命令。它表示執行到此處,執行權將交給其他協程。也就是說,yield命令是非同步兩個階段的分界線。

協程遇到yield命令就暫停,等到執行權返回,再從暫停的地方繼續往後執行。它的最大優點,就是程式碼的寫法非常像同步操作,如果去除yield命令,簡直一模一樣。

Generator 函式是協程在 ES6 的實現,最大特點就是可以交出函式的執行權(即暫停執行)。整個 Generator 函式就是一個封裝的非同步任務,或者說是非同步任務的容器。非同步操作需要暫停的地方,都用yield語句註明。

function* gen(){
  var url = 'https://api.github.com/users/github';
  var result = yield fetch(url);
  console.log(result.bio);
}

var g = gen();
var result = g.next();

result.value.then(function(data){
  return data.json();
}).then(function(data){
  g.next(data);
});複製程式碼

上面程式碼中,首先執行 Generator 函式,獲取遍歷器物件,然後使用next方法(第二行),執行非同步任務的第一階段。由於Fetch模組返回的是一個 Promise 物件,因此要用then方法呼叫下一個next方法。

非同步函式 async/await

之前非同步部分我們說過Promise和Generator,ES2017 標準引入了 async 函式,使得非同步操作變得更加方便。

async 函式是什麼?一句話,它就是 Generator 函式的語法糖。

Generator 函式,依次讀取兩個檔案。

const fs = require('fs');

const readFile = function (fileName) {
  return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function(error, data) {
      if (error) return reject(error);
      resolve(data);
    });
  });
};

const gen = function* () {
  const f1 = yield readFile('/etc/fstab');
  const f2 = yield readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};複製程式碼

寫成async函式,就是下面這樣。

const asyncReadFile = async function () {
  const f1 = await readFile('/etc/fstab');
  const f2 = await readFile('/etc/shells');
  console.log(f1.toString());
  console.log(f2.toString());
};複製程式碼

async函式有更好的語義,更廣的適用性,可以直接執行,而且返回值是 Promise。

使用注意

await命令後面的Promise物件,執行結果可能是rejected,所以最好把await命令放在try...catch程式碼塊中。

async function myFunction() {
  try {
    await somethingThatReturnsAPromise();
  } catch (err) {
    console.log(err);
  }
}

// 另一種寫法

async function myFunction() {
  await somethingThatReturnsAPromise()
  .catch(function (err) {
    console.log(err);
  });
}複製程式碼

多個await命令後面的非同步操作,如果不存在繼發關係,最好讓它們同時觸發。

let foo = await getFoo();
let bar = await getBar();

// 寫法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);

// 寫法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;複製程式碼

Proxy

Proxy 用於修改某些操作的預設行為,等同於在語言層面做出修改,所以屬於一種“超程式設計”(meta programming),即對程式語言進行程式設計。

Proxy 可以理解成,在目標物件之前架設一層“攔截”,外界對該物件的訪問,都必須先通過這層攔截,因此提供了一種機制,可以對外界的訪問進行過濾和改寫。

下面是一個攔截讀取屬性行為的例子。

var proxy = new Proxy({}, {
  get: function(target, property) {
    return 35;
  }
});

proxy.time // 35
proxy.name // 35
proxy.title // 35複製程式碼

雙向繫結

現在很多前端框架都實現了雙向繫結(演示:scrimba.com/p/pXKqta/c9…),目前業界分為兩個大的流派,一個是以React為首的單向資料繫結,另一個是以Angular、Vue為主的雙向資料繫結。可以實現雙向繫結的方法有很多,比如Angular基於髒檢查,Vue基於資料劫持等。雙向繫結的思想很重要,我在面試的時候基本上都會問到Vue雙向繫結的實現原理。

常見的基於資料劫持的雙向繫結有兩種實現,一個是目前Vue在用的Object.defineProperty,另一個就是Proxy。

image.png | center | 827x191

資料劫持比較好理解,通常我們利用Object.defineProperty劫持物件的訪問器,在屬性值發生變化時我們可以獲取變化,從而進行進一步操作。

// 這是將要被劫持的物件
const data = {
  name: '',
};

function say(name) {
  if (name === '古天樂') {
    console.log('給大家推薦一款超好玩的遊戲');
  } else if (name === '渣渣輝') {
    console.log('戲我演過很多,可遊戲我只玩貪玩懶月');
  } else {
    console.log('來做我的兄弟');
  }
}

// 遍歷物件,對其屬性值進行劫持
Object.keys(data).forEach(function(key) {
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function() {
      console.log('get');
    },
    set: function(newVal) {
      // 當屬性值發生變化時我們可以進行額外操作
      console.log(`大家好,我係${newVal}`);
      say(newVal);
    },
  });
});

data.name = '渣渣輝';
//大家好,我係渣渣輝
//戲我演過很多,可遊戲我只玩貪玩懶月複製程式碼

我們要實現一個完整的雙向繫結需要以下幾個要點:

  • 利用Proxy或Object.defineProperty生成的Observer針對物件/物件的屬性進行"劫持",在屬性發生變化後通知訂閱者。
  • 解析器Compile解析模板中的Directive(指令),收集指令所依賴的方法和資料,等待資料變化然後進行渲染。
  • Watcher屬於Observer和Compile橋樑,它將接收到的Observer產生的資料變化,並根據Compile提供的指令進行檢視渲染,使得資料變化促使檢視變化。

image.png | center | 711x380

使用Proxy相比Object.defineProperty,有如下優勢:

  • Proxy可以直接監聽物件而非屬性。Proxy直接可以劫持整個物件,並返回一個新物件,不管是操作便利程度還是底層功能上都遠強於Object.defineProperty。
  • Proxy可以直接監聽陣列的變化。Object.defineProperty無法監聽陣列變化。Vue用了一些奇技淫巧,把無法監聽陣列的情況hack掉了,由於只針對了八種方法(push、pop等)進行了hack,所以其他陣列的屬性也是檢測不到的,其中的坑很多。
  • Proxy有多達13種攔截方法,比如apply、ownKeys、deleteProperty、has等等是Object.defineProperty不具備的。

由於Proxy的這麼多優勢,Vue的下一個版本3.0宣稱會用Proxy改寫。

Reflect

Reflect物件與Proxy物件一樣,也是 ES6 為了操作物件而提供的新 API。Reflect物件的設計目的有這樣幾個。

  • 將Object物件的一些明顯屬於語言內部的方法(比如Object.defineProperty),放到Reflect物件上。
  • 修改某些Object方法的返回結果,讓其變得更合理。比如,Object.defineProperty(obj, name, desc)在無法定義屬性時,會丟擲一個錯誤,而Reflect.defineProperty(obj, name, desc)則會返回false。

    // 老寫法
    try {
      Object.defineProperty(target, property, attributes);
      // success
    } catch (e) {
      // failure
    }
    
    // 新寫法
    if (Reflect.defineProperty(target, property, attributes)) {
      // success
    } else {
      // failure
    }複製程式碼
  • 讓Object操作都變成函式行為。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)讓它們變成了函式行為。

    // 老寫法
    'assign' in Object // true
    
    // 新寫法
    Reflect.has(Object, 'assign') // true複製程式碼
  • Reflect物件的方法與Proxy物件的方法一一對應,只要是Proxy物件的方法,就能在Reflect物件上找到對應的方法。也就是說,不管Proxy怎麼修改預設行為,你總可以在Reflect上獲取預設行為。

    var loggedObj = new Proxy(obj, {
      get(target, name) {
        console.log('get', target, name);
        return Reflect.get(target, name);
      },
      deleteProperty(target, name) {
        console.log('delete' + name);
        return Reflect.deleteProperty(target, name);
      },
      has(target, name) {
        console.log('has' + name);
        return Reflect.has(target, name);
      }
    });複製程式碼

TypeScript

TypeScript 是2012年微軟釋出的一種開源語言,和與之結合的開源編輯器VS code ( Visual Studio Code)一起推出供開發者使用。 到今天,TypeScript 已經發生了比較大的變化,就語言特性來說,TypeScript 基本和ECMAScript 6的語法保持一致,可以認為是ECMAScript6的超集,基本包含了ECMAScript 6和ECMAScript6中部分未實現的內容,例如async/await,但仍有一些少數的差異性特徵。

TypeScript 可以使用 JavaScript 中的所有程式碼和編碼概念,TypeScript 是為了使 JavaScript 的開發變得更加容易而建立的。

TypeScript 相比於 JavaScript 的優勢:

  • TypeScript增加了很多功能,比如:型別推斷、型別擦除、介面、列舉、Mixin、泛型程式設計、名字空間、元組。
  • TypeScript支援幾乎所有最新的ES6新特性。
  • TypeScript重構起來非常方便。
  • TypeScript適合Java、C#開發人員的習慣。

展望

今後,JS從語言層還會不斷的完善,ECMAScript 每年都會有更新,還有很多好的特性在審查中:kangax.github.io/compat-tabl…


相關文章