本博客转自:「作者:若愚链接:https://zhuanlan.zhihu.com/p/22361337来源:知乎著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
1、什么是异步?
什么样的代码是异步代码?
我们先不深入异步概念,先从「表象」来看看怎么样的代码是异步代码:
书写顺序与执行顺序不同的代码,是异步代码。(只是从表象上来说,这并不是异步的定义
console.log(1)
setTimeout(function(){
console.log(2)
},0)
console.log(3)
上面代码的书写顺序是 1 -> 2 -> 3;
但是执行顺序是 1 -> 3 -> 2。
中间的 console.log(2) 就是异步执行的。
你现在知道了「代码的书写顺序和执行顺序居然可以不同!」
什么是异步?
同步:
一定要等任务执行完了,得到结果,才执行下一个任务。
var taskSync = function(){
return '同步任务的返回值'
}
var result = taskSync() // 那么 result 就是同步任务的结果
otherTask() // 然后执行下一个任务
异步
:不等任务执行完,直接执行下一个任务。
var taskAsync = function(){
var result = setTimeout(function(){
console.log('异步任务的结果')
}, 3000)
return result
}
var result = taskAsync() // result 不是异步任务的结果,而是一个 timer id
otherTask() // 立即执行其他任务,不等异步任务结束
聪明的你可能会发现,我们拿到的 result 不是异步执行的结果,而是一个 timer id,那么要怎么拿到异步任务的结果呢?
用回调。
改下代码如下:
所以「回调」经常用于获取「异步任务」的结果。
什么情况下需要用到异步?
现在有三个函数,taskA()、taskB() 和 taskC(),三个任务互不影响。taskA 和 taskC 执行得很快,但是 taskB 执行需要 10 秒钟。
// 同步的写法
function taskB(){
var response = $.ajax({
url:"/data.json",
async: false // 注意这里 async 为 false,表示是同步
})
return response // 十秒钟后,返回 response
}
taskA()
taskB()
taskC()
taskC 一定要等 taskB 执行完了才能执行,这就是同步。
执行顺序为:
A -> B -> AJAX 请求 -> C ---------------------------
现在换成异步:
// 异步的写法
function taskB(){
var result = $.ajax({
url:"/data.json",
async: true // 异步
})
return result // 一定要注意,现在的 result 不是上面的 response
}
taskA()
taskB()
taskC()
这样写之后,执行顺序就是
A -> B -> C ---------------------------------------
-> AJAX 请求 --------------------------------
就是说 AJAX 请求和任务C 同时执行。但是请注意执行的主体。AJAX 请求是由浏览器的网络请求模块执行的,taskC 是由 JS 引擎执行的。
综上,如果几个任务互相独立,其中一个执行时间较长,那么一般就用异步地方式做这件事。
JS 引擎不能同时做两件事
有些人说异步是同时做两件事,但其实 JS 引擎不会这样。以 setTimeout 为例,setTimeout 里面的代码一定会在当前环境中的任务执行完了「之后」才执行。异步意味着不等待任务结束,并没有强制要求两个任务「同时」进行。但是 AJAX 请求是可以与 JS 代码同时进行的,因为这个请求不是由 JS 引擎负责,而是由浏览器网络模块负责。
以上,就是异步的简介。
2、Callback(回调)是什么?
Callback 是什么?
callback 是一种特殊的函数,这个函数被作为参数传给另一个参数去调用。这样的函数就是回调函数。
callback 拆开,就是 call back,在英语里面就是「回拨电话」的意思。那我们就用打电话为例子来说明一下 callback:
- 1、「我打电话给某某」(I call somebody),那么「打电话」的人就是「我」。
- 2、「我」在电话里说:你办完某事后,回拨电话给「我」。
- 3、某某做完事后,就会「回拨电话给我」(calls back to me),那么「打电话」的人就是「某某」。
用编程来解释的话,是这样的:
- 1、「我调用一个函数 f」(I call a function),那么「调用函数」的人是「我」。代码是 f(c)。
- 2、「我」让这个函数 f 在执行完后,调用我传给它的另一个函数 c。
- 3、f 执行完的时候,就会「调用 c」,也叫做「回调 c」(call c back),调用 c 的人是 f。
好了,解释完了:callback 就是(传给另一个函数调用的)函数。把括号里面的内容去掉,简化成:callback 就是一种函数。
Callback 很常见
$button.on('click', function(){})
click 后面的 function 就是一个回调,因为「我」没有调用过这个函数,是 jQuery 在用户点击 button 时调用的。
div.addEventListener('click', function(){})
click 后面的 function 也是一个回调,因为「我」没有调用过这个函数,是浏览器在用户点击 button 时调用的。
一般来说,只要参数是一个函数,那么这个函数就是回调。
Callback 有点反直觉
很多初学者不明白 callback 的用法,因为 callback 有一点「反直觉」。比如说我们用代码做一件事情,分为两步:step1( ) 和 step2( )。符合人类直觉的代码是:
step1()
step2()
callback 的写法却是这样的:
step1(step2)
为什么要这样写?或者说在什么情况下应该用这个「反直觉」的写法?
一般(注意我说了一般),在 step1 是一个异步任务的时候,就会使用 callback。
3、什么是 HTML 5?
其实这个题目的意图是想知道你「会不会搜索」。人人都在说 HTML 5,你却不知道 HTML 5 是什么。为什么会这样?因为你不知道哪里的前端知识是靠谱的,你只能听别人说。
今天介绍一个靠谱的前端知识来源——MDN(Mozilla Developer Network)。
谷歌搜索「HTML 5 MDN」即可搜到权威介绍。
HTML 5 概览
HTML 5 是新版 Web 技术的集合,包含以下八个部分:
- 语义升级
- HTML 5 加了很多新的标签,使 HTML 更富有语义。
- 升级了 iframe 标签,使其更安全。
- 新增 MathML,是数学公式可以在 Web 中展现。
- 服务器增强新增 Web Sockets
- 新增 EventSource API
- 新增 WebRTC
- 离线储存
- 新增 AppCache
- online 与 offline 事件
- localStorage 和 sessionStorage
- IndexedDB
- File API
- 多媒体
- Web 原生支持音视频播放
- Camera API 可控制摄像头
- 图像绘制
- Canvas 可绘制图像和文本
- WebGL 可渲染 3D 影像
- SVG 可制作矢量图形
- 更多集成
- Web Workers 能够把 JavaScript 计算委托给后台线程
- XMLHttpRequest 升级
- History API 允许对浏览器历史记录进行操作
- 新增 conentEditable 属性
- 拖放 API、全屏 API、指针锁定 API
- 可以使用 navigator.registerProtocolHandler() 方法把 web 应用程序注册成一个协议处理程序。
- requestAnimationFrame 允许控制动画渲染以获得更优性能。
- 设备相关 API
- 你现在可以用 JS 来处理摄像头、触控屏幕、地理位置等设备相关功能了。
- 样式
- CSS 全面升级。
H5 是什么?
- H5 就是微信里面长得像 PPT、可以一页一页滑动的网页。
以上,就是 MDN 用法的介绍——获取靠谱知识。
4、你是如何做性能优化的?
为什么要做性能优化?
有些人看到这个题目,一上来就说「减少请求,添加缓存」之类的。不是说你错了,而是说你回答问题的时候没有思路。
首先你要明白一点:做任何事情都是有「目的」的。
吃饭喝水是为了生存,那么做性能优化的「目的」是什么?
想过这个问题么?如果没想过,今后就要刻意问问自己了。
优化的目的可以是:
1、 增强用户体验。但是这样说很虚,具体来说可以是:
1.1. 加快页面展示速度(慢)
1.2. 加快页面运行速度(卡)
2、节约服务器带宽流量
3、减少服务器压力
什么时候做性能优化?
你有目的了,不代表你马上就要去采取行动。
首先,你应该完成了网页的基本功能后再优化。如果你在前期就花时间优化,那么后期有可能没时间做其他功能。
其次,在没有找到性能瓶颈之前,不要优化!
一个网页的性能到底跟哪几方面有关?你优化的地方属于哪一方面?这是需要首先搞清楚的。
一个网页的大概流程包括:
- 1、DNS 查询
- 2、发送请求
- 3、等待服务器响应
- 4、下载服务器响应内容
- 5、解析 HTML、CSS、JS 等
- 6、渲染 HTML、CSS、JS 和图片等
- 7、响应用户的点击事件等
如果你的性能瓶颈在「等待服务器响应」这一步,那么你怎么优化 JS、CSS 都没用。
所以再说一遍:在没有找到性能瓶颈之前,不要优化!
怎么优化?
等你找到了瓶颈所在,就可以「对症下药」了。
- 1、DNS 查询——减少网页所用的域名个数,可可以减少 DNS 查询的时间
- 2、发送请求——添加缓存、合并文件,都可以减少请求数量
- 3、等待服务器响应——这一步的优化只能是在 MySQL 和后台方面做考虑了
- 4、下载服务器响应内容——添加 Etag、Expires 响应头,得到 304 响应,可以降低下载量
- 5、解析 HTML、CSS、JS 等——去掉无用的 HTML、CSS 和 JS 即可减少解析时间
- 6、渲染 HTML、CSS、JS 和图片等——避免使用低效的 HTML、CSS 和 JS 即可
- 7、响应用户的点击事件等——尽量不在前端做复杂的运算等……
整体思路
- 1、为什么要做?
- 2、什么时候做?
- 3、怎么做?
5、Promise 是什么?
window.Promise 已经是 JS 的一个内置对象了。
- Promise 有规格文档吗?
- 你一般如何使用 Promise。
目前的 Promise 都遵循 Promises/A+ 规范。
英文规范:https://promisesaplus.com/**
中文翻译:图灵社区 : 阅读 : 【翻译】Promises/A+规范
看完规范你可以了解 Promise 的全貌,本文主要讲讲 Promise 的用途。
Promise 之前的时代——回调时代
假设我们用 getUser 来说去用户数据,它接收两个回调 sucessCallback 和 errorCallback:
function getUser(successCallback, errorCallback){
$.ajax({
url:'/user',
success: function(response){
successCallback(response)
},
error: function(xhr){
errorCallback(xhr)
}
})
}
看起来还不算复杂。
如果我们获取用户数据之后还要获取分组数组、分组详情等,代码就会是这样:
getUser(function(response){
getGroup(response.id, function(group){
getDetails(groupd.id, function(details){
console.log(details)
},function(){
alert('获取分组详情失败')
})
}, function(){
alert('获取分组失败')
})
}, function(){
alert('获取用户信息失败')
})
三层回调,如果再多一点嵌套,就是「回调地狱」了。
Promise 来了
Promise 的思路呢,就是 getUser 返回一个对象,你往这个对象上挂回调:
var promise = getUser()
promise.then(successCallback, errorCallback)
当用户信息加载完毕,successCallback 和 errorCallback 之一就会被执行。
把上面两句话合并成一句就是这样的:
getUser().then(successCallback, errorCallback)
如果你想在用户信息获取结束后做更多事,可以继续 .then:
getUser().then(success1).then(success2).then(success3)
请求成功后,会依次执行 success1、success2 和 success3。
如果要获取分组信息:
getUser().then(function(response){
getGroup(response.id).then(function(group){
getDetails(group.id).then(function(){
},error3)
},error2)
}, error1)
这种 Promise 写法跟前面的回调看起来其实变化不大。真的,Promise 并不能消灭回调地狱,但是它可以使回调变得可控。你对比下面两个写法就知道了。
getGroup(response.id, success2, error2)
getGroup(response.id).then(success2, error2)
用 Promise 之前,你不能确定 success2 是第几个参数;
用 Promise 之后,所有的回调都是
.then(success, error)
这样的形式。
以上是 Promise 的简介,想完整了解 Promise,请参考下面的自学链接。
Promise对象 -- JavaScript 标准参考教程(alpha)
6、Babel 是什么?
Babel 作为一个工具,其实只要跟着它官网文档过一遍就知道它怎么用了。
一句话,Babel 能把你写的 JS 变成其他版本的 JS。
这样一来,你就可以写 IE 不支持的 JS 语法了,因为最终会被翻译成 IE 支持的语法。
比如你写 ES6
// src/index.js
[1,2,3].map(n => n + 1);
Babel 可以把它翻译成 ES5
// lib/index.js
[1,2,3].map(function(n) {
return n + 1;
});
如何安装
进入你的项目目录,用这句话安装 Babel:
npm install --save-dev babel-cli babel-preset-latest
然后新建一个文件,命名为 .babelrc,文件内容如下:
{
"presets": ["es2015"]
}
然后在 package.json 里面添加一个 script:
"scripts": {
"build": "babel src -d lib"
},
然后运行命令
npm run build
那么 src/index.js 就会被翻译成 lib/index.js。
如何实时翻译
怎么能做到我每次改 src/index.js ,lib/index.js 就自动变化呢?
只需要在上面的 script 里面加一个 --watch 选项即可:
这是 package.json 文件
{
...
"scripts": {
"build": "babel --watch src -d lib"
},
...
}
7、什么是响应式页面?
前几年火的一个概念:响应式页面。
- 什么样的页面是响应式页面?
- 响应式页面用到哪些技术?
- 响应式页面和自适应页面有什么区别?
什么是响应式页面?
首先你要理解什么是「响应」。
- 悟空拿着宝瓶,对金角大王叫了「金角大王」,金角大王应了一声。这就是「响应」。
- 武昌起义成功之后,各地纷纷也开始革命。这也是「响应」。
「响应」就是「你动,我也动」。
「响应式页面」就是「随着设备属性(如宽高)的变化,网页也随着变化。」
如上图,左边是 PC 上页面的样子,右边是手机上页面的样子。
响应式页面用到哪些技术?
- 多使用 max-width、min-width,不写死宽度
- 使用 media 查询来响应不同分辨率
- 使用动态 REM 方案保证手机端的显示效果
响应式页面和自适应页面的区别
自适应页面(流体布局、fluid layout)指的是页面宽度不固定。跟响应式页面没有什么关系。
自适应页面强调「不写死宽度」;响应式页面强调「响应」。
自适应页面可以是响应式的,也可以不是响应式的。
响应式页面可以是自适应的,也可以是不自适应的(也就是定宽的)。
8、简述浏览器缓存是如何控制的
1. 情形1,无缓存
浏览器向服务器请求资源 a.jpg,服务器找到对应资源把内容返回给浏览器。当浏览器再次向服务器请求资源a.jpg时,服务器重新发送完整的数据文件给浏览器。
- 优点:简单,啥都不用做
- 缺点:每次请求都查找并返回原始文件,浪费带宽
2. 情形2,有缓存无更新
浏览器第一次请求a.jpg 时服务器会发送完整的文件,浏览器可以把这个文件存到本地(缓存),下次再需要这个文件时直接从本地获取就行了,这样就能省下带宽了。
- 优点: 省带宽
- 缺点: 如果服务器上a.jpg的文件内容变了,浏览器每次都从缓存读取无法获取最新文件
3. 情形3, 缓存+更新机制
浏览器第一次请求a.jpg 时服务器会发送完整的文件,服务器在发送文件的时候还附带发送一些额外信息——过期时间,如 Expires: Mon,10 Dec 1990 02:25:22GMT。浏览器可以把这个文件和额外信息存到本地。当再次需要a.jpg的时候浏览器用当前浏览器时间和Expires做个比较,如果当前时间在过期时间以内,就直接使用缓存文件(状态为304);如果在过期时间以外就重新向服务器发送请求要资源(200)。 服务器在每次给资源的时候都会发送新的过期时间
- 优点:缓存可控制
- 缺点:控制的功能太单一;这种格式的时间很容易写错
4. 情形4, 缓存+更新机制升级版
比如:浏览器第一次请求a.jpg 时,服务器会发送完整的文件并附带额外信息
Cach-Control: max-age=300;
浏览器把文件和附带信息保存起来。当再次需要a.jpg 时,如果是在300秒以内发起的请求则直接使用缓存(304),否则重新发起网络请求(200)。下面是Cache-Control常见的几个值:
- Public表示响应可被任何中间节点缓存,如 Browser <-- proxy1 <-- proxy2 <-- Server,中间的proxy可以缓存资源,比如下次再请求同一资源proxy1直接把自己缓存的东西给 Browser 而不再向proxy2要。
- Private表示中间节点不允许缓存,对于Browser <-- proxy1 <-- proxy2 <-- Server,proxy 会老老实实把Server 返回的数据发送给proxy1,自己不缓存任何数据。当下次Browser再次请求时proxy会做好请求转发而不是自作主张给自己缓存的数据。
- no-cache表示不使用 Cache-Control的缓存控制方式做前置验证,而是使用 Etag 或者Last-Modified字段来控制缓存
- no-store ,真正的不缓存任何东西。浏览器会直接向服务器请求原始文件,并且请求中不附带 Etag 参数(服务器认为是新请求)。
- max-age,表示当前资源的有效时间,单位为秒。
优点:缓存控制功能更强大
缺点:假如浏览器再次请求资源a.jpg的时间间隔超过了max-age,这时候向服务器发送请求服务器应该会重新返回a.jpg的完整文件。但如果 a.jpg 在服务器上未做任何修改,发送a.jpg的完整文件就太浪费带宽了,其实只要发送一个「a.jpg未被更改」的短消息标示就好了。
5. 情形5, 缓存+更新机制终极版
比如:浏览器第一次请求a.jpg 时,服务器会发送完整的文件并附带额外信息,其中Etag 是 对a.jpg文件的编码,如果a.jpg在服务端未被修改,这个值就不会变
Cache-Control: max-age=300;
ETag:W/"e-cbxLFQW5zapn79tQwb/g6Q"
浏览器把a.jpg和额外信息保存到本地。假如浏览器在300秒以内再次需要获取a.jpg时,浏览器直接从缓存读取a.jpg(304)。假如浏览器在300秒之后再次需要获取a.jpg时,浏览器发现该缓存的文件已经不新鲜了,于是就向服务器发送请求 重新获取a.jpg, 在发送请求的时候附带刚刚保存的a.jpg的ETag ( If-None-Match:W/"e-cbxLFQW5zapn79tQwb/g6Q")。 服务器在接收到请求后拿浏览器请求的 Etag 和当前文件重新计算后端 Etag 做个比较,如果二者相等表示文件在未修改则发送个短消息(响应头,不包含图片内容),如果二者不等则发送新文件和新的 ETag,浏览器获取新文件并更新该文件的 Etag。
与 ETag 类似功能的是Last-Modified/If-Modified-Since。当资源过期时(max-age超时),发现资源具有Last-Modified声明,则再次向web服务器请求时带上头 If-Modified-Since,表示请求时间。web服务器收到请求后发现有头If-Modified-Since 则与被请求资源的最后修改时间进行比对。若最后修改时间较新,说明资源又被改动过,则响应整片资源内容(200);若最后修改时间较旧,说明资源无新修改,则响应HTTP 304 ,告知浏览器继续使用所保存的cache。
9、什么是JS原型链?
我们知道 JS 有对象,比如:
var obj = { name: 'obj' }
我们可以对 obj 进行一些操作,包括
- 「读」属性
- 「新增」属性
- 「更新」属性
- 「删除」属性
下面我们主要来看一下「读」和「新增」属性。
为什么有 valueOf / toString 属性呢?
在我们没有对 obj 进行任何其他操作之前,发现 obj 已经有几个属性(方法)了:
那么问题来了:valueOf / toString / constructor 是怎么来?我们并没有给 obj.valueOf 赋值呀。
要搞清楚 valueOf / toString / constructor 是怎么来的,就要用到 console.dir 了。
上面这个图有点难懂,我手画一个示意图:
我们发现 console.dir(obj) 打出来的结果是:
obj 本身有一个属性 name(这是我们给它加的)
obj 还有一个属性叫做 proto(它是一个对象)
obj.proto 有很多属性,包括 valueOf、toString、constructor 等
obj.proto 其实也有一个叫做 proto 的属性(console.log 没有显示),值为 null
现在回到我们的问题:obj 为什么会拥有 valueOf / toString / constructor 这几个属性?
答案:
这跟 proto 有关。当我们「读取」 obj.toString 时,JS 引擎会做下面的事情:
- 看看 obj 对象本身有没有 toString 属性。没有就走到下一步。
- 看看 obj.proto 对象有没有 toString 属性,发现 obj.proto 有 toString 属性,于是找到了
所以 obj.toString 实际上就是第 2 步中找到的 obj.proto.toString。
可以想象, - 如果 obj.proto 没有,那么浏览器会继续查看 obj.proto.proto
- 如果 obj.proto.proto 也没有,那么浏览器会继续查看 obj.proto.proto.proto__
- 直到找到 toString 或者 proto 为 null。
上面的过程,就是「读」属性的「搜索过程」。而这个「搜索过程」,是连着由 proto 组成的链子一直走的。
这个链子,就叫做「原型链」。
共享原型链
现在我们有另一个对象
var obj2 = { name: 'obj2' }
那么 obj.toString 和 obj2.toString 其实是同一个东西,也就是 obj2.proto.toString。
这有什么意义呢?
如果我们改写 obj2.proto.toString,那么 obj.toString 其实也会变!
这样 obj 和 obj2 就是具有某些相同行为的对象,这就是意义所在。
差异化
如果我们想让 obj.toString 和 obj2.toString 的行为不同怎么做呢?直接赋值就好了:
obj.toString = function(){ return '新的 toString 方法' }
总结:
「读」属性时会沿着原型链搜索。
「新增」属性时不会去看原型链。