面试复习二

ES6

问题一:ES6模块化如何使用,开发环境如何打包?

  • 基本语法为:import export(注意有无default)
  • 可以用 webpack 或者 rollup 等打包工具打包
  • 需要配置安装多个 babel ,以及 .babelrc 文件

问题二: Class 和普通构造函数的区别?

  • class 在语法上更加贴合面向对象的写法
  • class 实现继承更加易读、易理解
  • 更易于写后 java 等后端语言的开发者快速入门使用
  • 本质还是语法糖,使用 prototype

问题三:Promise 的基本使用和原理

  • new Promise 实例,而且要 return
  • new Promise 时要传入参数,函数有 resolve reject 两个参数
  • 成功时执行 resolve() 失败时执行 reject()
  • then 监听结果

  • promise里面的then函数仅仅是注册了后续需要执行的代码,真正的执行是在resolve方法里面执行的

  • 通过Promise.prototype.then和Promise.prototype.catch方法将观察者方法注册到被观察者Promise对象中,同时返回一个新的Promise对象,以便可以链式调用
  • 被观察者管理内部pending、fulfilled和rejected的状态转变,同时通过构造函数中传递的resolve和reject方法以主动触发状态转变和通知观察者。

问题四: 总结一下 ES6 的其它常用功能

  • let/const
  • 多行字符串/模板变量
  • 解构赋值
  • 块级作用域
  • 函数默认参数
  • 箭头函数
  • 拓展运算符
  • Symbol类型
  • Iterator 遍历器
  • Generator 函数
  • Async 函数

原型高级

问题一:说一说原型的实际应用

  • 描述一下 jquery 如何使用原型
  • 描述一下 zepto 如何使用原型
  • instanceof 原理
  • 再结合自己的项目经验,说一个自己的开发例子

zepto 源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
(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) {
//注意new,返回一个实例
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)
}

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

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

//调用
var $p = $('p')
$p.css('font-size', '40px') //css()与html()是原型方法
alert($p.html())

var $div1 = $('#div1')
$div1.css('color', 'blue')
alert($div1.html())

jquery 源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
(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)

//调用
var $p = $('p')
$p.css('font-size', '40px')
alert($p.html())

var $div1 = $('#div1')
$div1.css('color', 'blue')
alert($div1.html())

为什么要把原型方法放在 $.fn 或者 Jquery.fn?

因为要扩展插件(本质就是扩展原型方法):

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

好处:

  • 只有 $ 会暴露在 window 全局变量,限制暴露对象,避免全局污染
  • 将插件扩展统一到 $.fn.xxx 这个接口,方便使用且统一

问题二:原型如何体现它的扩展性

  • 说一下 jquery 和 zepto 的插件机制
    • 封装对象方法
    • 封装全局函数
    • 选择器插件
  • 结合自己的开发经验,做过的基于原型的插件
  • jQuery插件的机制很简单,就是利用jQuery提供的jQuery.fn.extend()和jQuery.extend()方法,扩展jQuery的功能。
  • jQuery.fn.extend()多用于扩展上面提到的3种类型中的第一种,jQuery.extend()用于扩展后两种插件。这两个方法都接受一个参数,类型为Object。Object对象的“名/值对”分别代表“函数或方法名/函数主体”。

异步

问题一:什么是单线程,和异步有什么关系?

  • 单线程:只有一个线程,只能做一件事情
  • 原因:避免 DOM 渲染的冲突。
    • 浏览器需要渲染 dom
    • js 可以修改 dom 的结构
    • js 执行的时候,浏览器 dom 渲染就会暂停
    • 两段 js 也不能同时执行(都修改 dom 就冲突了)
    • H5 中的 webworker 支持多线程,但是不能访问 dom
  • 解决方案就是异步
  • 实现方式就是 even-loop

问题二:什么是 even-loop

  • 事件轮询,js 实现异步的具体解决方案
  • 同步代码,直接执行
  • 异步函数先放在异步队列中
  • 待同步函数执行完毕,轮询执行异步队列的函数

问题三:是否用过 jquery 的 Deferred?

  • jquery 1.5的变化
    • 无法改变 js 异步和单线程的本质
    • 只能从写法上杜绝 callback 这种形式
    • 它是一种语法糖形式,但是解耦了
    • 很好的体现了:开放封闭原则
  • 使用 Jquery Deferred
    • 总结,dtd 的 API 可以分成两类,用意不同
    • 第一类:dtd.resolve dtd.reject
    • 第二类:dtd.then dtd.done dtd.fail
    • 这两类应该分开,否则后果很严重
  • 初步引入 Promise 概念
  • then() 方法返回一个 Promise 。它最多需要有两个参数:Promise 的成功和失败情况的回调函数,而它的行为与then中的回调函数的返回值有关:
    • 如果then中的回调函数返回一个值,那么then返回的Promise将会成为接受状态,并且将返回的值作为接受状态的回调函数的参数值。
    • 如果then中的回调函数抛出一个错误,那么then返回的Promise将会成为拒绝状态,并且将抛出的错误作为拒绝状态的回调函数的参数值。
    • 如果then中的回调函数返回一个已经是接受状态的Promise,那么then返回的Promise也会成为接受状态,并且将那个Promise的接受状态的回调函数的参数值作为该被返回的Promise的接受状态回调函数的参数值。
    • 如果then中的回调函数返回一个已经是拒绝状态的Promise,那么then返回的Promise也会成为拒绝状态,并且将那个Promise的拒绝状态的回调函数的参数值作为该被返回的Promise的拒绝状态回调函数的参数值。
    • 如果then中的回调函数返回一个未定状态(pending)的Promise,那么then返回Promise的状态也是未定的,并且它的终态与那个Promise的终态相同;同时,它变为终态时调用的回调函数参数与那个Promise变为终态时的回调函数的参数是相同的。

问题四:Promise 的基本使用和原理

  • 基本语法回顾
  • 异常捕获
  • 多个串联
  • Promise.all 和 promise.race
  • Promise 标准
    • 三种状态:pending fulfilled reject
    • 初始状态是 pending
    • pending 变为 fulfilled,或者 pending 变为 reject
    • 状态是不可逆的
    • promise 实例必须实现 then 这个方法
    • then() 必须可以接收两个函数作为参数
    • then() 返回的必须是一个 promise 实例(对象)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//基本使用
function loadImg(src) {
var promise = new Promise(function (resolve, reject) {
var img = document.createElement('img')
//throw new Error("图片加载失败")//模仿语法报错
img.onload = function () {
resolve(img)
}
img.onerror = function () {
reject('图片加载失败')
}
img.src = src
})
return promise
}

var src = 'https://www.imooc.com/static/img/index/logo_new.png'
var result = loadImg(src)
result.then(function (img) {
console.log(1, img.width)
return img // 非promise对象会自动包装成一个 promise 对象,并且img会作为参数传递,类似于:resolve(img)
}, function () {
console.log('error 1')
}).then(function (img) {
console.log(2, img.height)
return img
},function(){
console.log('error 2')
})
1
2
3
4
5
6
7
8
9
10
11
12
// 统一捕获异常
var src = 'https://www.imooc.com/static/img/index/logo_new.png'
var result = loadImg(src)
result.then(function (img) {
console.log(1, img.width)
return img
}).then(function (img) {
console.log(2, img.height)
}).catch(function (ex) {
// 统一捕获异常
console.log(ex)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//多个串联,第一个加载完后加载第二个,以此类推
var src1 = 'https://www.imooc.com/static/img/index/logo_new.png'
var result1 = loadImg(src1)
var src2 = 'https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg'
var result2 = loadImg(src2)
result1.then(function (img1) {
console.log('第一个图片加载完成', img1.width)
return result2 // 重要!!!
}).then(function (img2) {
console.log('第二个图片加载完成', img2.width)
}).catch(function (ex) {
//统一catch
console.log(ex)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//promise.all 接收一个 promise 对象的数组
//待全部完成后,统一执行 success
var src1 = 'https://www.imooc.com/static/img/index/logo_new.png'
var result1 = loadImg(src1)
var src2 = 'https://img1.mukewang.com/545862fe00017c2602200220-100-100.jpg'
var result2 = loadImg(src2)
Promise.all([result1, result2]).then(function (datas) {
//接收到的 datas 是一个数组,依次包含了多个 promise 返回内容
console.log('all', datas[0])
console.log('all', datas[1])
})
//promise.race 接收一个包含多个 promise对象数组
//只要有一个完成,就执行 success
Promise.race([result1, result2]).then(function (data) {
//data 即最先执行完成的 promises 的返回值
console.log('race', data)
})

问题五:介绍一下 async/await(和 promise 的区别、联系)

  • then 只是将 callback 拆分了
  • async/await 是最直接的同步写法,再也没有回调函数
  • 使用了 promise ,并没有和 promise 冲突
  • 但是:改变不了 js 单线程,异步的本质
  • 需要 babel-polyfill

问题六:总结一下当前 js 解决异步的方案

  • 回调函数(setTimeout())
  • 事件监听
  • 发布订阅(jQuery.subscribe(“done”, f2);jQuery.publish(“done”);)
  • promise
  • generator
  • async/await

virtual dom

问题一: 什么是 vdom?

  • virtual dom,虚拟 DOM
  • 用 JS 模拟 DOM 结构
  • DOM 变化对比,放在 JS 层来做(图灵完备语言)
  • 目的是提高重绘性能
    • DOM 操作是“昂贵”的,JS 运行效率高
    • 尽量减少 DOM 操作,而不是 “推到重来”
    • 项目越复杂,影响就越严重
    • vdom 可以解决这个问题
  • 核心api
    • h(‘标签名’,{属性},[子元素]),对应 vue 中的vm._c,有子元素
    • h(‘标签名’,{属性},[字符串]),对应 vue 中的vm._c,无子元素
    • patch(containter,vnode),对应 vue 中的vm.patch,首次渲染
    • patch(vnode,newVnode),对应 vue 中的vm.patch,diff 对比修改

问题二: 介绍一下 diff 算法?

  • 知道什么是 diff 算法,是 linux 的基础命令
  • vdom 中应用 diff 算法是为了找到需要更新的节点
  • diff 实现,path(container,vnode);patch(vnode,newVnode)
  • 核心逻辑,createElement 和 updataChildren

MVVM 和 Vue

问题一:说一下使用 Jquery 和使用框架的区别

  • 数据和视图分离,解耦(开放封闭原则,数据可修改,DOM操作渲染封装)
  • 以数据驱动视图,只关心数据变化,DOM 操作被封装

问题二:说一下对 MVVM(Model View ViewModel) 的理解,如何实现 MVVM?

  • Model 数据、模型
  • View 视图、模板(视图和模型是分离的)
  • ViewModel 连接 Model 和 View(View 通过 DOM Listeners 事件监听 Model 来控制 Model 的改变,Model 通过 Data Bindings 操控视图的更新)

问题三:响应式,vue 中如何实现响应式

  • 关键是理解 Object.defineProperty
  • 将 data 的属性代理到 vm 上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<div id="app">
<input type="text" id="testId">
<p id="show-text"></p>
</div>
<script>
//双向绑定
var obj = {}
Object.defineProperty(obj, 'theValue', {
get: function () {
return obj
},
set: function (newValue) {
console.log("自动执行set方法")
document.getElementById('testId').value = newValue
document.getElementById('show-text').innerHTML = newValue
}
})
document.addEventListener('keyup', function (e) {
obj.theValue = e.target.value
})
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//将 data 的属性代理到 vm 上
var vm = {}
var data = {
name: 'zhangsan',
age: 20
}

var key, value
for (key in data) {
(function (key) {
Object.defineProperty(vm, key, {
get: function () {
console.log('get', data[key]) // 监听
return data[key]
},
set: function (newVal) {
console.log('set', newVal) // 监听
data[key] = newVal
}
})
})(key)
}

问题四:模板引擎是什么? vue 中如何解析模板? vue 的模板如何被渲染成 html?

  • 跟 html 类似,但是有很大区别
  • 模板本质是字符串,模板最终必须转换成 js 代码
  • 有逻辑,如 v-if,v-for 等,逻辑必须用 js 才能实现(图灵完备)
  • 最终还是要转换为 hmtl来显示,必须用 js 来转换
  • 因此,模板最重要转换成一个 js 函数(通过 render 函数[vm._c,vm._s,vm._l…])
  • 知道 render 函数长什么样子,render 函数返回 vnode
  • updateComponent 函数 patch(),首次直接渲染,第二次开始 diff 对比旧的 vnode

问题五:vue 的整个实现流程是怎么样的?

  1. 第一步,解析模板成 render 函数
    1. with 用法
    2. 模板中用的所有信息都被 render 函数包含
    3. 模板中用到的 data 中的属性,都变成了 js 变量
    4. 模板中的 v-model v-for v-on 都变成了 js 逻辑
    5. render 函数返回 vnode
  2. 第二步,响应式开始监听(data,watch等)
    • Object.defineProperty
    • 将 data 的属性代理到 vm 上
  3. 第三步,首次渲染,显示页面,且绑定依赖
    1. 初次渲染,执行 updataComponent,执行 vm._render()
    2. 执行 render 函数,会访问到 vm.list 和 vm.title 等 data 上的数据
    3. 会被响应式的 get 方式监听到
      • 为何要监听 get,直接监听 set 不行吗?
      • data 中有许多属性,有些被用到,有些可能不被用到
      • 被用到的会走 get,不被用到的不会走 get
      • 未走 get 中的属性,set 的时候我们也无需关心
      • 避免不必要的重复渲染
    4. 执行 updateComponent,会走 vnode 的 patch 方法
    5. patch 将 vnode 渲染成 DOM,初次渲染完成
  4. 第四部,data 属性变化,触发 rerender
    1. 修改属性,被响应式的 set 监听到
    2. set 中执行 updateComponent
    3. updateComponent 重新执行 vm._render()
    4. 生成的 vnode 和 preVnode,通过 patch 进行对比
    5. 渲染到 html 中

组件化和 React

问题一:说一下对组件化的理解

  • 组件的封装:封装视图、数据、变化逻辑
  • 组件的复用:props 传递、复用

问题二:JSX 本质是什么

  • JSX 语法(标签、JS 表达式、判断、循环、事件绑定)
  • JSX 本质是语法糖,需要被解析成 JS 才能运行
  • JSX 是独立的标准,可被其他项目使用

问题三:JSX 和 vdom 的关系

  • 为何需要 vdom: JSX 需要渲染成 html,数据驱动视图
  • React.createElement 和 h,都生成 vnode
  • 何时 patch: ReactDOM.render(…) 和 setState
  • 自定义组件的解析:初始化实例,然后执行 render,例如:
1
2
3
//把 React.createElement(App,null); 转换成下面两句:
var app = new App(null); //这里传入的属性是 null,或者 {data:this.state.list},{addTitle:this.addTitle.bind(this)} 等数据或者函数
return app.render();

问题四: 说一下 setState 的过程

  • setState 的异步:效果、原因(提高效率)
    • 可能会一次执行多次 setState
    • 你无法规定、限制用户如何使用 setState
    • 没必要每次 setState 都重新渲染,考虑性能
    • 即便是每次重新渲染,用户也看不到中间的效果(执行 JS 时,DOM 渲染是暂停的)
  • vue 修改属性也是异步的:效果、原因(提高效率)
  • setState 的过程:最终走到 patch(preVnode,newVnode)
    • 每个组件实例,都有 renderComponent 方法
    • 执行 renderComponent 会重新执行实例的 render
    • render 函数返回 newVnode ,然后拿到 preVnode
    • 执行 patch(preVnode,newVnode)

React vs Vue

  • 两者的本质区别
    • vue 本质是 MVVM 框架,由 MVC 发展而来
    • React 本质是前端组件化框架,由后端组件化发展而来
    • 但并不妨碍他们两者都能实现相同的功能
  • 看模板的和组件化的区别
    • 模板
      • vue 使用模板(最初由 angular 提出)
      • React 使用 JSX
      • 模板语法上,我更加倾向于 JSX
      • 模板分离上,我更加倾向于 Vue
    • 组件化
      • React 本身就是组件化,没有组件化就不是 React
      • vue 也支持组件化,不过是在 MVVM 上的扩展
      • 对于组件化,我更加倾向于 React ,做的彻底而清晰
  • 两者共同点
    • 都支持组件化
    • 都是数据驱动视图

Hybird

问题一:hybird 存在的价值,为何会用 hybrid?

  • hybird 是客户端和前端的混合开发
  • 没有访问手机内部信息功能的权限,所以可以快速迭代更新,无需审核
  • 体验流畅(和 NA 的体验基本类似)
  • 减少开发和沟通成本,双端公用一套代码

问题二: webview

  • 是 app 中的一个组件(app 可以有 webview,也可以没有)
  • 用于加载 h5 页面,即一个小型的浏览器内核

问题三: file:// 协议

  • file 协议:本地文件快
  • http(s) 协议:网络加载,慢

问题四:hybrid 使用场景:

  • 不是所有的场景都适合 hybrid
  • 使用 NA:体验要求极致,变化不频繁
  • 使用 hybrid: 体验要求高,变化频繁
  • 使用 h5:体验无要求,不经常使用

问题五: hybrid 具体实现:

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

问题六: app 发布之后,静态文件如何实时更新?

  • 分版本,有版本号,如 201803211015
  • 将静态文件压缩成 zip 包,上传到服务器
  • 客户端每次启动,都去服务端检查版本号
  • 如果服务端本本号大于客户端版本号,就去下载最新的 zip 包
  • 下载完之后解压包,然后将现有文件覆盖

问题七: hybrid 和 h5 的区别

  • 优点
    • 体验好,跟 NA 体验基本一致
    • 可快速迭代,无需 app 审核(关键)
  • 缺点
    • 开发成本高,联调、测试、查 bug 都比较麻烦
    • 运维成本高,更新流程复杂
  • 适用场景
    • hybrid:产品的稳定功能,体验要求高,迭代频繁(产品型)
    • h5:单词的运营活动(如 xx 红包)或不常用功能(运营型)

问题八:前端和客户端通讯

  • 静态页面如何获取内容,例如:新闻详情页,前端如何获取新闻内容?
    • 不能用 ajax 获取,第一跨域,第二速度慢
    • 客户端获取新内容(客户端可以提前获取),然后 js 通讯拿到内容,再渲染
  • JS 和客户端通讯的基本形式
    • 通过 schema 协议——前端和客户端通讯约定
    • 调用能力,传递参数,监听回调
  • schema 协议简介和使用
  • schema 使用封装
  • 内置上线
    • 更快更安全