JavaScript核心知识点(下篇)

596 阅读9分钟

本篇是《JavaScript核心知识点》的下篇,主要讲解JavaScript中的进阶知识,包括ES6、原型、异步,以及当前流行框架的基本原理、混和开发的原理和应用。

ES6

问题:

  1. ES6 模块化如何使用,开发环境如何打包
  2. class 与普通构造函数有什么区别
  3. Promise 的基本使用和原理
  4. ES6 的其他常用功能

ES6 模块化如何使用,开发环境如何打包

  • 模块化的基本语法
  • 开发环境配置
  • 关于 JS 众多模块化标准

export 语法

// test1.js
export default {
    a: 100
}

// test2.js
export function f1() {
    console.log('f1')
}
export function f2() {
    console.log('f2')
}

// index.js
import Test1 from './test1'
import { f1, f2 } from './test2'
console.log(Test1)

f1()
f2()

开发环境1 babel

使用 npm init -y 创建 package.json 。

安装 babel

npm install --save-dev babel-core babel-preset-es2015 babel-preset-latest
npm install babel-cli --save-dev

创建 .babelrc

{
    "presets": [
        "es2015",
        "latest"
    ],
    "plugins": []
}

创建 index.js

[1, 2, 3].map(item => item + 1)

使用 npx babel index.js 编译 index.js 。

开发环境2 webpack

关于使用 webpack 配置 babel ,见 Babel

开发环境3 rollup

类似于 webpack 的打包工具。

其他模块化标准

  • AMD

  • CommonJS

  • ES6 模块化

总结

语法:import export (注意有无 default)

环境:使用 babel 编译 ES6 语法,模块化使用 webpack 或者 rollup

class 与普通构造函数有什么区别

  • JS 构造函数
  • class 的基本语法
  • 语法糖
  • 继承

JS 构造函数

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

Mathematics.prototype.add = function () {
    return this.x + this.y
}

var m = new Mathematics(1, 2)
console.log(m.add())

class 的基本语法

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

    add() {
        return this.x + this.y;
    }
}

const m = new Mathematics(1, 2)
console.log(m.add())

语法糖

class Mathematics {
    // ...
}

typeof Mathematics // "function"
Mathematics === Mathematics.prototype.constructor // true

继承

JS 继承

function Animal() {
    this.eat = function () {
        console.log('animal eat')
    }
}

function Dog() {
    this.bark = function () {
        console.log('dog bark')
    }
}

Dog.prototype = new Animal()

var dog = new Dog()

class 继承

class Animal {
    constructor(name) {
        this.name = name
    }
    eat() {
        console.log(`${this.name} eat.`)
    }
}

class Dog extends Animal {
    constructor(name) {
        super(name)
        this.name = name
    }
    say() {
        console.log(`${this.name} say`)
    }
}

const dog = new Dog('秋田犬')
dog.say()
dog.eat()

总结

class 在语法上更加贴合面向对象的写法

class 实现继承更加易读,易理解

本质还是语法糖,使用 prototype

Promise 的基本使用

  • 回调地狱
  • Promise 语法

回调地狱

$.get(url1, (data1) => {
    console.log(data1)

    $.get(url2, (data2) => {
        console.log(data2)

        $.get(url3, (data3) => {
            console.log(data3)
        })
    })
})

Promise 语法

function getData(url) {
    return new Promise((resolve, reject) => {
        $.ajax({
            url,
            success(data) {
                resolve(data)
            },
            error(err) {
                reject(err)
            }
        })
    })
}

const url1 = '/data1.json'
const url2 = '/data2.json'
const url3 = '/data3.json'

getData(url1).then(data1 => {
    console.log(data1)
    return getData(url2)
}).then(data2 => {
    console.log(data2)
    return getData(url3)
}).then(data3 => {
    console.log(data3)
}).catch(err => console.error(err))

总结

new Promise 实例,而且要 return

new Promise 时要传入函数,函数有 resolve reject 两个参数

成功时执行 resolve() ,失败时执行 reject()

then 监听结果

ES6 的其他常用功能

  • let / const
  • 多行字符串 / 模板变量
  • 解构赋值
  • 块级作用域
  • 函数默认参数
  • 箭头函数

原型

问题:

  1. 说一个原型的实际应用
  2. 原型如何体现它的扩展性

原型的实际应用

  • jquery 和 zepto 的简单使用
  • zepto 如何使用原型
  • jquery 如何使用原型

jquery 和 zepto 的简单使用

let $p = $('p')
$p.css('color', 'red') // css 是原型方法
console.log($p.html()) // html 是原型方法

zepto 使用原型

(function (window) {
    var zepto = {}

    function Z(dom, selector) {
        var i, len = dom ? dom.length : 0
        for (i = 0; i < len; i++) {
            this[i] = dom[i]
        }
        this.length = len
        this.selector = selector || ''
    }

    zepto.Z = function (dom, selector) {
        return new Z(dom, selector)
    }

    zepto.init = function (selector) {
        var slice = Array.prototype.slice
        var dom = slice.call(document.querySelectorAll(selector))
        return zepto.Z(dom, selector)
    }

    var $ = function (selector) {
        return zepto.init(selector)
    }
    
    window.$ = $

    $.fn = {
        css: function (key, value) {
            alert('css')
        },
        html: function (value) {
            return '这是一个模拟的 html 函数'
        }
    }
    
    Z.prototype = $.fn
})(window)

jquery 使用原型

(function (window) {
    var jQuery = function (selector) {
        return new jQuery.fn.init(selector)
    }

    jQuery.fn = {
        css: function (key, value) {
            alert('css')
        },
        html: function (value) {
            return 'html'
        }
    }

    var init = jQuery.fn.init = function (selector) {
        var slice = Array.prototype.slice
        var dom = slice.call(document.querySelectorAll(selector))

        var i, len = dom ? dom.length : 0
        for (i = 0; i < len; i++) {
            this[i] = dom[i]
        }
        this.length = len
        this.selector = selector || ''
    }

    init.prototype = jQuery.fn

    window.$ = jQuery

})(window)

总结

描述一下 jquery 、 zepto 如何使用原型

结合自己的项目经验,说一个自己开发的例子

原型如何体现它的扩展性

  • 总结 zepto 和 jquery 原型的使用
  • 插件机制

为什么要把原型方法放在 $.fn

只有 $ 会暴露在 window 全局对象

将插件扩展统一到 $.fn.xxx 这一接口,方便使用

$.fn.getNodeName = function(){
    return this[0].nodeName
}

总结

说一下 jquery 和 zepto 的插件机制

结合自己的开发经验,做过的基于原型的插件

异步

问题:

  1. 什么是单线程,和异步有什么关系
  2. 什么是 event-loop(事件轮询)
  3. 是否用过 jquery 的Deferred
  4. Promise 的基本使用和原理
  5. 介绍一下 async/await(和 Promise 的区别、联系)
  6. 介绍一下当前 JS 解决异步的方案

什么是单线程,和异步的关系

单线程

只有一个线程,同一时间只能做一件事情。

原因

避免 DOM 渲染的冲突。

浏览器需要渲染 DOM ,JS 可以修改 DOM 结构。JS 执行的时候,浏览器 DOM 渲染会暂停。两段 JS 也不能同时执行(都修改 DOM 就冲突了)。

webworker 支持多线程,但是不能访问 DOM 。

解决方案

异步。

console.log(100)
setTimeOut(()=>{
    console.log(200)
}, 100)
console.log(300)

异步的缺陷:

  1. 没有按照代码的顺序执行,可读性差
  2. callback 中不容易模块化

实现方式

event-loop 。见后面。

总结

单线程就是同时只能做一件事,两段 JS 不能同时执行。

原因是为了避免 DOM 渲染的冲突。

异步是一种解决方案。

event-loop 是实现方式。

event-loop

event-loop 是异步的实现方式。

对于同步代码,会直接执行。异步函数,先放在异步队列中。等待同步函数执行完毕,轮询执行异步队列中的函数。

看下面的代码:

setTimeout(function () {
    console.log(1)
}, 100)
setTimeout(function () {
    console.log(2)
})
$.ajax({
    url: 'xxx',
    success(res) {
        console.log(3)
    }
})
console.log(4)

其中,主进程

console.log(4)

主进程代码执行完毕后,JS 引擎会轮询查看异步队列中有无代码,并将异步队列中的代码放到主进程中执行:

// 立即被放入异步队列
function () {
    console.log(2)
}

// ajax 请求成功后被放入异步队列
success(res) {
    console.log(3)
}

// 100ms 后被放入异步队列
function () {
    console.log(1)
}

是否用过 jquery 的Deferred

无法改变 JS 异步和单线程的本质,只能从写法上杜绝 callback 这种形式。它是一种语法糖形式,但是解耦了代码。很好的体现了开放封闭原则。

jquery 1.5 之前:

var ajax = $.ajax({
    url: './data.json',
    success: function () {
        console.log('success 1')
        console.log('success 2')
    },
    error: function () {
        console.log('error')
    }
})

jquery deferred 写法 1:

var ajax = $.ajax('./data.json')
ajax.done(function () {
    console.log('success 1')
}).fail(function () {
    console.log('fail 1')
}).done(function () {
    console.log('success 2')
}).fail(function () {
    console.log('fail 2')
})

jquery deferred 写法 2:

var ajax = $.ajax('./data.json')
ajax.then(function () {
    console.log('success 1')
}, function () {
    console.log('error 1')
}).then(function () {
    console.log('success 2')
}, function () {
    console.log('error 2')
})

Deferred

// var wait = function () {
//     var task = function () {
//         console.log('执行完成')
//     }
//     setTimeout(task, 2000)
// }
// wait()

// 已经封装好的(A 员工)
function waitHandle() {
    // 定义
    var dtd = $.Deferred()
    var wait = function (dtd) {
        var task = function () {
            console.log('执行完成')
            // 成功
            dtd.resolve()
            // 失败
            // dtd.reject()
        }
        setTimeout(task, 1000)
        // wait 返回 promise
        return dtd.promise()
    }
    // 最终返回
    return wait(dtd)
}

// 使用(B 员工)
var w = waitHandle()  // promise 对象

/*
// 如果 w 是 dtd,那么可以:
w.then(function () {
    console.log('success')
}, function () {
    console.log('error')
})
// 还可以使用 done , fail
*/

// 但是 w 是 promise 对象,所以:
$.when(w).then(function () {
    console.log('ok 1')
}, function () {
    console.log('err 1')
})
// 此时 w.reject() // 执行这句话会直接报错

jquery 的 API 可分为两类,用意不同

  1. dtd.resolve , dtd.reject
  2. dtd.then , dtd.done , dtd.fail

这两类应该分开,否则后果严重。可以通过返回 dtd.promise() 避免。

Promise 的基本使用和原理

基本语法回顾

略。

异常捕获

Error 和 reject 都要考虑。

// 规定:then 只接受一个参数,最后统一用 catch 捕获异常
result.then(function (img) {
    console.log(img.width)
    return img
}).then(function (img) {
    console.log(img.height)
    return img
}).catch(function (e) {
    // 最后统一 catch
    console.log(e)
})

多个串联

var result1 = loadImg(src1)
var result2 = loadImg(src2)

// 规定:then 只接受一个参数,最后统一用 catch 捕获异常
result1.then(function (img1) {
    console.log('第一个图片加载完成')
    return result2 // 重要
}).then(function (img2) {
    console.log('第二个图片加载完成')
}).catch(function (e) {
    // 最后统一 catch
    console.log(e)
})

Promise.all 和 Promise.race

// Promise.all 接收一个 Promise 对象的数组
// 带全部完成之后,统一执行 success
Promise.all([result1, result2]).then(datas => {
    console.log(datas[0])
    console.log(datas[1])
})

// Promise.race 接收一个包含多个 Promise 对象的数组
// 只要有一个完成,就执行 success
Promise.race([result1, result2]).then(data => {
    console.log(data)
})

Promise 标准

三种状态:pending、fulfilled、rejected。

初始状态是 pending

pending 变为 fulfilled,或者 pending 变为 rejected。

状态变化不可逆。


Promise 实例必须实现 then 方法

then() 必须可以接受两个函数作为参数。

then() 返回的必须是一个 Promise 实例。

async/await

then 只是将 callback 拆分了。

而 async/await 是最直接的同步写法。

用法

  • 使用 await ,函数必须用 async 标识。
  • await 后面跟的是一个 Promise 实例。
  • 需要用 babel-polyfill

虚拟 DOM

Virtual Dom 是 Vue 和 React 的核心,先讲哪个都绕不开它。

问题:

  1. Virtual Dom 是什么?为何会存在 Virtual Dom ?
  2. Virtual Dom 如何应用?核心 API 是什么?
  3. 介绍一下 diff 算法。

什么是 Virtual Dom ,为何使用 Virtual Dom?

Virtual Dom 即使用 JS 模拟 DOM 结构。DOM 变化的对比,放在 JS 层来做。 提高重绘性能。

<ul id="list">
    <li class="item">Item 1</li>
    <li class="item">Item 2</li>
</ul>

↓↓↓

{
    tag: 'ul',
    attrs: {
        id: 'list'
    },
    children: [
        {
            tag: 'li',
            attrs: {
                className: 'item'
            },
            children: ['Item 1']
        },
        {
            tag: 'li',
            attrs: {
                className: 'item'
            },
            children: ['Item 2']
        }
    ]
}

Virtual Dom 如何应用?核心 API 是什么?

  • 介绍 snabbdom
  • demo
  • 核心 API

🌰 例子:

<script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-class.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-props.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-style.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-eventlisteners.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.1/h.js"></script>
<script type="text/javascript">
    var snabbdom = window.snabbdom

    // 定义 patch
    var patch = snabbdom.init([
        snabbdom_class,
        snabbdom_props,
        snabbdom_style,
        snabbdom_eventlisteners
    ])

    // 定义 h
    var h = snabbdom.h

    var container = document.getElementById('container')

    // 生成 vnode
    var vnode = h('ul#list', {}, [
        h('li.item', {}, 'Item 1'),
        h('li.item', {}, 'Item 2')
    ])
    patch(container, vnode)

    document.getElementById('btn').addEventListener('click', function () {
        // 生成 newVnode
        var newVnode = h('ul#list', {}, [
            h('li.item', {}, 'Item 1'),
            h('li.item', {}, 'Item B'),
            h('li.item', {}, 'Item 3')
        ])
        patch(vnode, newVnode)
    })
</script>

核心 API:

  • h('<标签名>', {...属性...}, [...子元素...])
  • h('<标签名>', {...属性...}, '...')
  • 创建DOM :patch(container, vnode)
  • 更新DOM :patch(vnode, newVnode)

介绍一下 diff 算法

  • 什么是 diff 算法
  • vdom 为何使用 diff 算法
  • diff 算法的实现流程

DOM 操作是昂贵的,因此尽量减少 DOM 操作,找出本次 DOM 必须更新的节点来更新,其他的节点不更新。

这个“找出”的过程,就需要 diff 算法。

创建DOM :patch(container, vnode)

function createElement(vnode) {
    var tag = vnode.tag  // 'ul'
    var attrs = vnode.attrs || {}
    var children = vnode.children || []
    if (!tag) {
        return null
    }

    // 创建真实的 DOM 元素
    var elem = document.createElement(tag)
    // 属性
    var attrName
    for (attrName in attrs) {
        if (attrs.hasOwnProperty(attrName)) {
            // 给 elem 添加属性
            elem.setAttribute(attrName, attrs[attrName])
        }
    }
    // 子元素
    children.forEach(function (childVnode) {
        // 给 elem 添加子元素
        elem.appendChild(createElement(childVnode))  // 递归
    })

    // 返回真实的 DOM 元素
    return elem
}

更新DOM :patch(vnode, newVnode)

function updateChildren(vnode, newVnode) {
    var children = vnode.children || []
    var newChildren = newVnode.children || []

    children.forEach(function (childVnode, index) {
        var newChildVnode = newChildren[index]
        if (childVnode.tag === newChildVnode.tag) {
            // 深层次对比,递归
            updateChildren(childVnode, newChildVnode)
        } else {
            // 替换
            replaceNode(childVnode, newChildVnode)
        }
    })
}

function replaceNode(vnode, newVnode) {
    var elem = vnode.elem  // 真实的 DOM 节点
    var newElem = createElement(newVnode)

    // 替换...
}

MVVM 和 Vue

问题:

  1. 说一下使用 jQuery 和使用 MVVM 框架的区别
  2. 说一下对 MVVM 的理解
  3. Vue 中如何实现响应式
  4. Vue 中如何解析模板
  5. Vue 的整个实现流程

使用 jQuery 和使用 MVVM 框架的区别

  • jQuery 实现 todo-list(略)
  • Vue 实现 todo-list(略)
  • jQuery 和 Vue 的区别
    • 数据和视图的分离(解耦)
    • 以数据驱动视图,只关心数据变化,DOM 操作被封装

hybrid

  • 移动端占大部分流量,已经远超PC
  • 一线互联网公司都有自己的APP
  • 这些APP中有很大比例的前端代码

问题:

  • hybrid 是什么,为何用 hybrid?
  • 介绍一下 hybrid 更新和上线的流程?
  • hybrid 和 h5 的主要区别?
  • 前端 JS 和客户端如何通讯?

hybrid 是什么,为何用 hybrid

hybrid 文字解释

hybrid 即“混合”,即前端和客户端的混合开发,需前端开发人员和客户端开发人员配合完成。某些环节也可能涉及到server端。

存在价值,为何会用hybrid

可以快速迭代更新,无需app审核。体验流畅,和 NA 的体验基本类似。减少开发和沟通成本,双端公用一套代码。

webview

是 app 中的一个组件,用于加载 h5 页面,即一个小型的浏览器内核。

file:// 协议

一开始接触 html 开发的时候,就已经使用了 file 协议,只不过你当时没有 “协议” “标准” 等这些概念,再次强调 “协议” “标准” 的重要性。

  1. file 协议:本地文件,快。
  2. http(s) 协议:网络加载,慢。

hybrid 实现流程

不是所有场景都适合使用 hybrid,hybrid 适合体验要求高,变化频繁的场景。

  1. 前端做好静态页面(html js css),将文件交给客户端,客户端拿到前端静态页面,以文件形式存储到 app 中,客户端在一个webview中,使用file协议加载静态页面。

  2. app 发布之后,静态页面如何实时更新?- 客户端替换每个客户端中的静态文件,客户端去server端对比版本号来下载最新文件(压缩包)。

  3. 静态页面如何获取内容?后面介绍。

hybrid 和 h5 的主要区别

hybrid 优点:

  1. 体验更好,跟 NA 体验基本一致
  2. 可快速迭代,无需 app 审核

hybrid 缺点:

  1. 开发成本高,联调、测试、查bug都比较麻烦
  2. 运维成本高。

适用的场景:

  1. hebrid:产品的稳定功能,体验要求高,迭代频繁
  2. h5:单次的运营活动(如红包)或不常用的功能

JS 和客户端如何通讯

前面遗留的问题:

  1. 新闻详情页适用 hybrid,前端如何获取新闻内容?
  2. 不能用 ajax获取,第一 跨域,第二 速度慢

hybrid 是通过客户端获取新闻内容(客户端可以提前获取内容),然后 JS 通讯拿到内容,再渲染。

基本形式

  1. JS 访问客户端能力,传递参数和回调函数
  2. 客户端通过回调函数返回内容

schema 协议简介和使用

类似于file协议,https 协议。schema 协议——前端和客户端通讯的约定。

如 微信扫一扫:weixin://dl/scan

function invokeScan() {
    window['_invoke_scan_callback_'] = function (result) {
        alert(result)
    }

    var iframe = document.createElement('iframe')
    iframe.style.display = 'none'
    // iframe.src = 'weixin://dl/scan'  // 重要!
    iframe.src = 'weixin://dl/scan?k1=v1&k2=v2&k3=v3&callback=_invoke_scan_callback_'
    var body = document.body
    body.appendChild(iframe)
    setTimeout(function () {
        body.removeChild(iframe)
        iframe = null
    })
}

document.getElementById('btn1').addEventListener('click', function () {
    invokeScan()
})

schema 封装

// invoke.js
(function (window, undefined) {

    // 调用 schema 的封装
    function _invoke(action, data, callback) {
        // 拼装 schema 协议
        var schema = 'myapp://utils/' + action

        // 拼接参数
        schema += '?a=a'
        var key
        for (key in data) {
            if (data.hasOwnProperty(key)) {
                schema += '&' + key + data[key]
            }
        }

        // 处理 callback
        var callbackName = ''
        if (typeof callback === 'string') {
            callbackName = callback
        } else {
            callbackName = action + Date.now()
            window[callbackName] = callback
        }
        schema += 'callback=callbackName'

        // 触发
        var iframe = document.createElement('iframe')
        iframe.style.display = 'none'
        iframe.src = schema  // 重要!
        var body = document.body
        body.appendChild(iframe)
        setTimeout(function () {
            body.removeChild(iframe)
            iframe = null
        })
    }

    // 暴露到全局变量
    window.invoke = {
        share: function (data, callback) {
            _invoke('share', data, callback)
        },
        scan: function (data, callback) {
            _invoke('scan', data, callback)
        },
        login: function (data, callback) {
            _invoke('login', data, callback)
        }
    }

})(window)

// 调用
// 扫一扫
document.getElementById('btn1').addEventListener('click', function () {
    window.invoke.scan({}, function () { })
})
// 分享
document.getElementById('btn2').addEventListener('click', function () {
    window.invoke.share({
        title: 'xxx',
        content: 'yyy'
    }, function (result) {
        if (result.errno === 0) {
            alert('分享成功')
        } else {
            alert(result.message)
        }
    })
})

内置上线

  • 将以上封装的代码打包,叫做 invoke.js,内置到客户端
  • 客户端每次启动 webview, 都默认执行 invoke.js
  • 本地加载,免去网络加载的时间,更快
  • 本地加载,没有网络请求,黑客看不到 schema 协议,更安全