本篇是《JavaScript核心知识点》的下篇,主要讲解JavaScript中的进阶知识,包括ES6、原型、异步,以及当前流行框架的基本原理、混和开发的原理和应用。
ES6
问题:
- ES6 模块化如何使用,开发环境如何打包
- class 与普通构造函数有什么区别
- Promise 的基本使用和原理
- 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
- 多行字符串 / 模板变量
- 解构赋值
- 块级作用域
- 函数默认参数
- 箭头函数
原型
问题:
- 说一个原型的实际应用
- 原型如何体现它的扩展性
原型的实际应用
- 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 的插件机制
结合自己的开发经验,做过的基于原型的插件
异步
问题:
- 什么是单线程,和异步有什么关系
- 什么是 event-loop(事件轮询)
- 是否用过 jquery 的Deferred
- Promise 的基本使用和原理
- 介绍一下 async/await(和 Promise 的区别、联系)
- 介绍一下当前 JS 解决异步的方案
什么是单线程,和异步的关系
单线程
只有一个线程,同一时间只能做一件事情。
原因
避免 DOM 渲染的冲突。
浏览器需要渲染 DOM ,JS 可以修改 DOM 结构。JS 执行的时候,浏览器 DOM 渲染会暂停。两段 JS 也不能同时执行(都修改 DOM 就冲突了)。
webworker 支持多线程,但是不能访问 DOM 。
解决方案
异步。
console.log(100)
setTimeOut(()=>{
console.log(200)
}, 100)
console.log(300)
异步的缺陷:
- 没有按照代码的顺序执行,可读性差
- 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 可分为两类,用意不同
- dtd.resolve , dtd.reject
- 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 的核心,先讲哪个都绕不开它。
问题:
- Virtual Dom 是什么?为何会存在 Virtual Dom ?
- Virtual Dom 如何应用?核心 API 是什么?
- 介绍一下 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
问题:
- 说一下使用 jQuery 和使用 MVVM 框架的区别
- 说一下对 MVVM 的理解
- Vue 中如何实现响应式
- Vue 中如何解析模板
- 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 协议,只不过你当时没有 “协议” “标准” 等这些概念,再次强调 “协议” “标准” 的重要性。
- file 协议:本地文件,快。
- http(s) 协议:网络加载,慢。
hybrid 实现流程
不是所有场景都适合使用 hybrid,hybrid 适合体验要求高,变化频繁的场景。
-
前端做好静态页面(html js css),将文件交给客户端,客户端拿到前端静态页面,以文件形式存储到 app 中,客户端在一个webview中,使用file协议加载静态页面。
-
app 发布之后,静态页面如何实时更新?- 客户端替换每个客户端中的静态文件,客户端去server端对比版本号来下载最新文件(压缩包)。
-
静态页面如何获取内容?后面介绍。
hybrid 和 h5 的主要区别
hybrid 优点:
- 体验更好,跟 NA 体验基本一致
- 可快速迭代,无需 app 审核
hybrid 缺点:
- 开发成本高,联调、测试、查bug都比较麻烦
- 运维成本高。
适用的场景:
- hebrid:产品的稳定功能,体验要求高,迭代频繁
- h5:单次的运营活动(如红包)或不常用的功能
JS 和客户端如何通讯
前面遗留的问题:
- 新闻详情页适用 hybrid,前端如何获取新闻内容?
- 不能用 ajax获取,第一 跨域,第二 速度慢
hybrid 是通过客户端获取新闻内容(客户端可以提前获取内容),然后 JS 通讯拿到内容,再渲染。
基本形式
- JS 访问客户端能力,传递参数和回调函数
- 客户端通过回调函数返回内容
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 协议,更安全