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

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

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

HTML

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

演進

image.png | center | 571x951

DOCTYPE

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

HTML 5:前端

<!DOCTYPE html>複製代碼

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

HTML 4.01 Strict:git

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

語義化標籤

HTML語義化能讓頁面內容更具結構化且更加清晰,便於瀏覽器和搜索引擎進行解析,所以要儘可能使用帶有語義化結構標籤。程序員

image.png | center | 400x616

通常狀況下,具備良好Web語義化的頁面結構在沒有樣式文件的狀況下也是可以閱讀的,例如列表會以列表的樣式展示,標題文字會加粗,而不是所有內容都以無層次的文本內容形式呈現。es6

image.png | center | 622x243

CSS規範規定,每一個標籤都是有 display 屬性的。因此根據標籤元素的display屬性特色,能夠將HTML標籤分爲如下幾類:github

  • 行內元素:包括 <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>元素,例如使用無序列表來模擬表格。web

咱們在書寫標籤的時候,還要注意加上必要的屬性,好比:<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…

相關文章
相關標籤/搜索