面试

每日一题 (二)

2016-09-30  本文已影响110人  韩宝亿

本博客转自:「作者:若愚链接:https://zhuanlan.zhihu.com/p/22361337来源:知乎著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

1、什么是异步?

什么样的代码是异步代码?
我们先不深入异步概念,先从「表象」来看看怎么样的代码是异步代码:
书写顺序与执行顺序不同的代码,是异步代码。(只是从表象上来说,这并不是异步的定义

console.log(1)
setTimeout(function(){
  console.log(2)
},0)
console.log(3)
Paste_Image.png

上面代码的书写顺序是 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() // 立即执行其他任务,不等异步任务结束

Paste_Image.png

聪明的你可能会发现,我们拿到的 result 不是异步执行的结果,而是一个 timer id,那么要怎么拿到异步任务的结果呢?

用回调。

改下代码如下:

Paste_Image.png

所以「回调」经常用于获取「异步任务」的结果。

什么情况下需要用到异步?
现在有三个函数,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:

用编程来解释的话,是这样的:

好了,解释完了: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 技术的集合,包含以下八个部分:

  1. 语义升级
  1. 服务器增强新增 Web Sockets
  1. 离线储存
  1. 多媒体
  1. 图像绘制
  1. 更多集成
  1. 设备相关 API
  1. 样式

H5 是什么?

4、你是如何做性能优化的?

为什么要做性能优化?
有些人看到这个题目,一上来就说「减少请求,添加缓存」之类的。不是说你错了,而是说你回答问题的时候没有思路。

首先你要明白一点:做任何事情都是有「目的」的。

吃饭喝水是为了生存,那么做性能优化的「目的」是什么?

想过这个问题么?如果没想过,今后就要刻意问问自己了。

优化的目的可以是:

1、 增强用户体验。但是这样说很虚,具体来说可以是:
 1.1. 加快页面展示速度(慢)
 1.2. 加快页面运行速度(卡)
 
2、节约服务器带宽流量

3、减少服务器压力

什么时候做性能优化?
你有目的了,不代表你马上就要去采取行动。

首先,你应该完成了网页的基本功能后再优化。如果你在前期就花时间优化,那么后期有可能没时间做其他功能。

其次,在没有找到性能瓶颈之前,不要优化!

一个网页的性能到底跟哪几方面有关?你优化的地方属于哪一方面?这是需要首先搞清楚的。

一个网页的大概流程包括:

如果你的性能瓶颈在「等待服务器响应」这一步,那么你怎么优化 JS、CSS 都没用。
所以再说一遍:在没有找到性能瓶颈之前,不要优化!
怎么优化?
等你找到了瓶颈所在,就可以「对症下药」了。

整体思路

5、Promise 是什么?

window.Promise 已经是 JS 的一个内置对象了。

  1. Promise 有规格文档吗?
  2. 你一般如何使用 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、什么是响应式页面?

前几年火的一个概念:响应式页面。

  1. 什么样的页面是响应式页面?
  2. 响应式页面用到哪些技术?
  3. 响应式页面和自适应页面有什么区别?

什么是响应式页面?
首先你要理解什么是「响应」。

「响应」就是「你动,我也动」。
「响应式页面」就是「随着设备属性(如宽高)的变化,网页也随着变化。」


Paste_Image.png

如上图,左边是 PC 上页面的样子,右边是手机上页面的样子。

响应式页面用到哪些技术?

响应式页面和自适应页面的区别

自适应页面(流体布局、fluid layout)指的是页面宽度不固定。跟响应式页面没有什么关系。
自适应页面强调「不写死宽度」;响应式页面强调「响应」。
自适应页面可以是响应式的,也可以不是响应式的。
响应式页面可以是自适应的,也可以是不自适应的(也就是定宽的)。

8、简述浏览器缓存是如何控制的

1. 情形1,无缓存
浏览器向服务器请求资源 a.jpg,服务器找到对应资源把内容返回给浏览器。当浏览器再次向服务器请求资源a.jpg时,服务器重新发送完整的数据文件给浏览器。

2. 情形2,有缓存无更新
浏览器第一次请求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常见的几个值:

优点:缓存控制功能更强大

缺点:假如浏览器再次请求资源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 已经有几个属性(方法)了:

Paste_Image.png
那么问题来了:valueOf / toString / constructor 是怎么来?我们并没有给 obj.valueOf 赋值呀。
要搞清楚 valueOf / toString / constructor 是怎么来的,就要用到 console.dir 了。 Paste_Image.png

上面这个图有点难懂,我手画一个示意图:

Paste_Image.png

我们发现 console.dir(obj) 打出来的结果是:

  1. obj 本身有一个属性 name(这是我们给它加的)

  2. obj 还有一个属性叫做 proto(它是一个对象)

  3. obj.proto 有很多属性,包括 valueOf、toString、constructor 等

  4. obj.proto 其实也有一个叫做 proto 的属性(console.log 没有显示),值为 null

现在回到我们的问题:obj 为什么会拥有 valueOf / toString / constructor 这几个属性?

答案:
这跟 proto 有关。当我们「读取」 obj.toString 时,JS 引擎会做下面的事情:

  1. 看看 obj 对象本身有没有 toString 属性。没有就走到下一步。
  2. 看看 obj.proto 对象有没有 toString 属性,发现 obj.proto 有 toString 属性,于是找到了
    所以 obj.toString 实际上就是第 2 步中找到的 obj.proto.toString。
    可以想象,
  3. 如果 obj.proto 没有,那么浏览器会继续查看 obj.proto.proto
  4. 如果 obj.proto.proto 也没有,那么浏览器会继续查看 obj.proto.proto.proto__
  5. 直到找到 toString 或者 proto 为 null。

上面的过程,就是「读」属性的「搜索过程」。而这个「搜索过程」,是连着由 proto 组成的链子一直走的。
这个链子,就叫做「原型链」。

共享原型链
现在我们有另一个对象

var obj2 = { name: 'obj2' }

Paste_Image.png
那么 obj.toString 和 obj2.toString 其实是同一个东西,也就是 obj2.proto.toString。

这有什么意义呢?

如果我们改写 obj2.proto.toString,那么 obj.toString 其实也会变!

这样 obj 和 obj2 就是具有某些相同行为的对象,这就是意义所在。

差异化
如果我们想让 obj.toString 和 obj2.toString 的行为不同怎么做呢?直接赋值就好了:

obj.toString = function(){ return '新的 toString 方法' }

Paste_Image.png
总结:
「读」属性时会沿着原型链搜索
「新增」属性时不会去看原型链。
上一篇 下一篇

猜你喜欢

热点阅读