前端技術演進(四):

這個來自之前做的培訓,刪減了一些業務相關的,參考了很多資料(參考資料列表),謝謝前輩們,麼麼噠 😘

前端有三個基本構成:結構層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

看下面的代碼:http://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的創建就完成了。

比如:http://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主要是指:http://necolas.github.io/normalize.css/ 這個庫。它是一種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的實現:https://thx.github.io/cube/doc/neat

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

預處理

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

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

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

  • 文件切分
  • 模塊化
  • 選擇符嵌套
  • 變量
  • 運算
  • 函數

LESS、SASS

image.png | center | 730x131

Sass 和 Less 是兩種 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樣式以達到元素樣式持續變化的結果,例如:http://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內元素的移動路徑。

例如:http://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。

演示:http://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%的意義相同,表示動畫的結束關鍵幀。

演示:http://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>元素邊界將不被顯示。

演示:http://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中,在每一次執行完後進行異步回調來連續觸發動畫效果。

演示:http://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。

在可以接受的情況下,儘可能的壓縮圖片到最小。https://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

雙向綁定

現在很多前端框架都實現了雙向綁定(演示:https://scrimba.com/p/pXKqta/c9ePQT3),目前業界分爲兩個大的流派,一個是以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 每年都會有更新,還有很多好的特性在審查中:http://kangax.github.io/compat-table/esnext/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章