一场面试题目整理

465 阅读6分钟

1.call,apply,bind他们有什么区别

var foo = {
	value: 1
}
function bar () {
	console.log(this.value)
}
// 如果不对this进行绑定执行bar() 会返回undefined
bar.call(foo) // 1

共同点:

都可以改变函数执行的上下文环境;bind返回对应函数, 便于稍后调用; apply, call则是立即调用。

不同点:

fun.call(xh,"a","b");

fun.apply(xh,["a","b"]);

fun.bind(xh,"a","b")();

原理:

  1. 将函数设为对象的属性
  2. 执行这个函数
  3. 删除这个函数

2.常见的继承有哪些?组合式继承中,子函数继承父函数的prototype怎么继承?

function Parent2(){
    this.name = "parent2";
    this.colors = ["red","blue","yellow"];
}
function Child2(){
    Parent2.call(this);
    this.type = "child2";
}
Child2.prototype = new Parent2()复制代码
var s1 = new Child2();
s1.colors.push("black");
var s2 = new Child2();

s1.colors; // (4) ["red", "blue", "yellow", "balck"]
s2.colors; // (3) ["red", "blue", "yellow"]
我回答的是通过new来创建实现继承的,面试官又说那就是通过拷贝内存创建和父函数内存一样的地址实现的,那是怎么才能拷贝一样的地址呢。

我了解的面试官可能想要考啥深度克隆和浅克隆,但是不知道对不对。我回答object.assign

  • Object.assign

JSON.parse(JSON.stringify(obj)): 性能最快

  • 具有循环引用的对象时,报错
  • 当值为函数、undefined、或symbol时,无法拷贝
  • $.extend

4.描述一下作用域,作用域链是怎么生成的?

简单来讲,作用域(scope)就是变量访问规则的有效范围

  •  全局作用域

var foo = 'foo'; console.log(window.foo); // => 'foo' 

 函数作用域function doSomething () { var thing = '吃早餐'; } console.log(thing); // Uncaught ReferenceError: thing is not defined

  • ES6的块级作用域

很多书上都有一句话,javascript没有块级作用域的概念。所谓块级作用域,就是{}包裹的区域。但是在ES6出来以后,这句话并不那么正确了。因为可以用 let 或者 const 声明一个块级作用域的变量或常量。

比如:

for (let i = 0; i < 10; i++) {
    // ...
}
console.log(i); // Uncaught ReferenceError: i is not defined

默认使用 const,只有当确实需要改变变量的值的时候才使用let。因为大部分的变量的值在初始化之后不应再改变,而预料之外的变量的修改是很多bug的源头。

var a = 123;
function fn1 () {
    console.log(a);
}
function fn2 () {
    var a = 456;
    fn1();
}
fn2();   // 123

通俗地讲,当声明一个函数时,局部作用域一级一级向上包起来,就是作用域链。

  • 当执行函数时,总是先从函数内部找寻局部变量
  • 如果内部找不到(函数的局部作用域没有),则会向创建函数的作用域(声明函数的作用域)寻找,依次向上
  • 由两部分组成:
    • [[scope]]属性: 指向父级变量对象和作用域链,也就是包含了父级的[[scope]]AO
    • AO: 自身活动对象

如此 [[scopr]]包含[[scope]],便自上而下形成一条 链式作用域

5.闭包

6.[]==0?

两个不同类型的简单类型数据通过 == 进行比较时,都会转换为数值类型再进行比较。

[]会转化成0, 0==0返回true

7.用splice如何删除数组

8.reduce方法内部是怎么实现的

reduce(callback,initiaValue)会传入两个变量,回调函数(callback)和初始值(initiaValue)。

假设函数有个传入参数,prev和next,index和array。 Prev和next是你必须要了解的。

当没有传入初始值时,prev是从数组中第一个元素开始的,next是第二个函数。

但是当传入初始值(initiaValue)后,第一个prev将是initivalValue,next将是数组中的第一个元素。

// 实现的原理

Array.prototype.myReduce = function(fn, initialValue) {
    if (this.length === 0) {
        if (initialValue === undefined) {
            console.error("reduce of empty array with no initialValue")
        } else {
            return initialValue
        }
    } else {
        var prev = initialValue !== undefined ? initialValue : this[0]
        var startIndex = initialValue !== undefined ? 0 : 1
        for (var i = startIndex; i < this.length; i++) {
            prev = fn(prev, this[i])
        }
        return prev
    }
}
var ret = arr.myReduce(function(prev, curr) {
    return prev + curr
}, 5)     3q

console.log(ret)
//reduce
Array.prototype.reduce = function (callback, pre) {    for (let i = 0; i < this.length; i++) {        if (typeof pre == undefined) {            pre = callback(this[i + 1], this[i], i, this);            i++;        } else {            pre = callback(pre, this[i], i, this)        }    }    return pre}let rd = [1, 2, 3].reduce((a, b, index, arr) => {    return a + b}, 0)

10.map

ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。

const m = new Map();
const o = {p: 'Hello World'};

m.set(o, 'content')
m.get(o) // "content"

m.has(o) // true
m.delete(o) // true
m.has(o) // false

11.盒子水平居中,transform和margin的区别

transform 属于合成属性(composite property),对合成属性进行 transition/animation 动画将会创建一个合成层(composite layer),这使得被动画元素在一个独立的层中进行动画。通常情况下,浏览器会将一个层的内容先绘制进一个位图中,然后再作为纹理(texture)上传到 GPU,只要该层的内容不发生改变,就没必要进行重绘(repaint),浏览器会通过重新复合(recomposite)来形成一个新的帧。

top/left属于布局属性,该属性的变化会导致重排(reflow/relayout),所谓重排即指对这些节点以及受这些节点影响的其它节点,进行CSS计算->布局->重绘过程,浏览器需要为整个层进行重绘并重新上传到 GPU,造成了极大的性能开销。

对布局属性进行动画,浏览器需要为每一帧进行重绘并上传到 GPU 中
对合成属性进行动画,浏览器会为元素创建一个独立的复合层,当元素内容没有发生改变,该层就不会被重绘,浏览器会通过重新复合来创建动画帧

11.事件模型,怎么阻止冒泡,preventdefult会不会退去所有元素的冒泡?

w3c的方法是e.stopPropagation(),IE则是使用e.cancelBubble = true

原文链接:caibaojian.com/javascript-…

取消默认事件

w3c的方法是e.preventDefault(),IE则是使用e.returnValue = false;·

preventDefault它是事件对象(Event)的一个方法,作用是取消一个目标元素的默认行为。既然是说默认行为,当然是元素必须有默认行为才能被取消,如果元素本身就没有默认行为,调用当然就无效了。什么元素有默认行为呢?如链接<a>,提交按钮<input type=”submit”>等。当Event 对象的 cancelable为false时,表示没有默认行为,这时即使有默认行为,调用preventDefault也是不会起作用的。

var a = document.getElementById("testA");
a.onclick =function(e){
if(e.preventDefault){
e.preventDefault();
}else{
window.event.returnValue == false;
}
}

function cancelHandler(event) {var e = window.event || event;if(document.all) {e.returnValue = false;}else {e.preventDefault();}}

12.绑定事件有什么

13.http协议,url到服务器返回数据都经历了什么,http1的keep alive和http2有什么区别

 KeepAlive带来的好处是可以减少tcp连接的开销.websocket不同,它本身就规定了是正真的、双工的长连接,两边都必须要维持住连接的状态。
HTTP/2中,HTTP / 2.0 中引入了多路复用,能够让多个请求使用同一个 TCP 链接,极大的加快了网页的加载速度。并且还支持 Header 压缩,进一步的减少了请求的数据大小,可以一次性发很多请求。

14.promise原理,用法,promise all原理

  • 可以把 Promise 看成一个状态机。初始是 pending 状态,可以通过函数 resolvereject,将状态转变为 resolved或者 rejected 状态,状态一旦改变就不能再次变化。
  • then 函数会返回一个 Promise 实例,并且该返回值是一个新的实例而不是之前的实例。因为 Promise 规范规定除了 pending 状态,其他状态是不可以改变的,如果返回的是一个相同实例的话,多个 then 调用就失去意义了。
  • 对于 then来说,本质上可以把它看成是 flatMap

http://blog.poetries.top/FE-Interview-Questions/advance/#_16-promise-%E5%AE%9E%E7%8E%B0

Promise.all的返回值是一个Promise实例

Promise.all = function (promise) {
    return new Promise((resolve, reject) => {
        let index = 0
        let result = []
        if (promise.length === 0) {
            resolve(result)
        } else {
            function processValue(i, data) {
                result[i] = data
                if (++index === promise.length) {
                    resolve(result)
                }
            }
            for (let i = 0; i < promise.length; i++) {
                Promise.resolve(promise[i]).then((data) => {
                    processValue(i, data)
                }, (err) => {
                    reject(err)
                    return
                })
            }
        }
    })
}

在实际应用中,如果可以从几个接口获取相同的数据,哪个接口数据先到就先用哪个,就可以使用Promise.race(),所需时间等于其中最快的那个接口。下面是代码:

const race = function (iterable) {  
return new Promise(function (resolve, reject) {  
  for (const i in iterable) {      
const v = iterable[i]    
  if (typeof v === 'object' && typeof v.then === 'function') {  
      v.then(resolve, reject)     
 } else {        resolve(v)     
 }    }  })}const p1 = new Promise(function (resolve) { 
setTimeout(resolve, 200, 1) })
const p2 = new Promise(function (resolve) { s
etTimeout(resolve, 100, 2) })
race([p1, p2]).then(function (res) { console.log(res) }) // 2

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

上面代码中,不管promise最后的状态,在执行完thencatch指定的回调函数以后,都会执行finally方法指定的回调函数。

下面是一个例子,服务器使用 Promise 处理请求,然后使用finally方法关掉服务器。

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
finally方法的回调函数不接受任何参数,这意味着没有办法知道,
前面的 Promise 状态到底是fulfilled还是rejected。这表明,
finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。
 

15.微任务和宏任务的执行机制,微任务和宏任务是如何切换的,什么时候执行微任务什么时候执行宏任务,执行的时机是什么?

  • cmd-markdown-logo
  • 执行完宏任务必须执行微任务,而不是一次性把所有微任务执行完。

    宏任务中包括了 script ,浏览器会先执行一个宏任务,接下来有异步代码的话就先执行微任务.

    所以正确的一次 Event loop 顺序是这样的

    • 执行同步代码,这属于宏任务
    • 执行栈为空,查询是否有微任务需要执行
    • 执行所有微任务
    • 必要的话渲染 UI
    • 然后开始下一轮 Event loop,执行宏任务中的异步代码
    微任务:
    • process.nextTick
    • promise
    • Object.observe
    • MutationObserver
    宏任务
    • script
    • setTimeout
    • setInterval
    • setImmediate
    • I/O
    • UI rendering

    Promise.resolve().then(()=>{
      console.log('Promise1')  
      setTimeout(()=>{
        console.log('setTimeout2')
      },0)
    })
    
    setTimeout(()=>{
      console.log('setTimeout1')
      Promise.resolve().then(()=>{
        console.log('Promise2')    
      })
    },0)
    执行结果为:Promise1,setTimeout1,Promise2,setTimeout2
    

    16.generator函数使用过程

    传统的编程语言,早有异步编程的解决方案(其实是多任务的解决方案)。其中有一种叫做"协程"(coroutine),意思是多个线程互相协作,完成异步任务。

    协程有点像函数,又有点像线程。它的运行流程大致如下。

    • 第一步,协程A开始执行。
    • 第二步,协程A执行到一半,进入暂停,执行权转移到协程B
    • 第三步,(一段时间后)协程B交还执行权。
    • 第四步,协程A恢复执行。

    上面流程的协程A,就是异步任务,因为它分成两段(或多段)执行。

    举例来说,读取文件的协程写法如下。

    function* asyncJob() {
      // ...其他代码
      var f = yield readFile(fileA);
      // ...其他代码
    }

    创建了这个函数的句柄,并没有实际执行,需要进一步调用next()

    加上 * 的函数执行后拥有了 next函数,也就是说函数执行后返回了一个对象。每次调用 next函数可以继续执行被暂停的代码。以下是 Generator函数的简单实现

    function* helloGenerator() {
           yield "hello";
           yield "generator";
           return;
       }
       var h = helloGenerator();
       console.log(h.next());//{ value: 'hello', done: false }
       console.log(h.next());//{ value: 'generator', done: false }
       console.log(h.next());//{ value: 'undefined', done: true }
    

    (1)创建了h对象,指向helloGenerator的句柄,
    
    (2)第一次调用nex(),执行到"yield hello",暂缓执行,并返回了"hello"
    
    (3)第二次调用next(),继续上一次的执行,执行到"yield generator",暂缓执行,并返回了"generator"。
    
    (4)第三次调用next(),直接执行return,并返回done:true,表明结束。
    
    经过上面的分析,yield实际就是暂缓执行的标示,每执行一次next(),相当于指针移动到下一个yield位置。
    
    总结一下,Generator函数是ES6提供的一种异步编程解决方案。通过yield标识位和next()方法调用,实现函数的分段执行。
    

    原文链接:blog.poetries.top/FE-Intervie…

    await的特点

    1. 简洁

    由示例可知,使用Async/Await明显节约了不少代码。我们不需要写.then,不需要写匿名函数处理Promise的resolve值,也不需要定义多余的data变量,还避免了嵌套代码。这些小的优点会迅速累计起来,这在之后的代码示例中会更加明显。

    function timeout(ms) {
      return new Promise((resolve) => {
        setTimeout(resolve, ms);
      });
    }
    
    async function asyncPrint(value, ms) {
      await timeout(ms);
      console.log(value);
    }
    
    asyncPrint('hello world', 50);

    2. 错误处理

    Async/Await让try/catch可以同时处理同步和异步错误。在下面的promise示例中我们需要使用.catch,这样错误处理代码非常冗余。并且,在我们的实际生产代码会更加复杂。·promise处理异常需要在catch中捕捉错误异常,而await只在asnic中处理就行

    const makeRequest = async () => { await callAPromise() await callAPromise() await callAPromise() await callAPromise() await callAPromise() throw new Error("oops"); } makeRequest() .catch(err => { console.log(err); // output // Error: oops at makeRequest (index.js:7:9) })

    18.数据劫持

    Object.defineProperty(obj,prop,descriptor)
    obj:目标对象
    prop:需要定义的属性或方法的名称
    descriptor:目标属性所拥有的特性
    可供定义的特性列表value:属性的值writable:如果为false,属性的值就不能被重写。
    get: 一旦目标属性被访问就会调回此方法,并将此方法的运算结果返回用户。
    set:一旦目标属性被赋值,就会调回此方法。
    configurable:如果为false,
    则任何尝试删除目标属性或修改属性性以下特性(writable, configurable, enumerable)的行为将被无效化。
    enumerable:是否能在for...in循环中遍历出来或在Object.keys中列举出来。

    Object.defineProperty主要是通过set和get来让属性值发生变化

    var data = {
        name:'lhl'
    }
    
    Object.keys(data).forEach(function(key){
        Object.defineProperty(data,key,{
            get:function(){
                console.log('get');
            },
            set:function(){
                console.log('监听到数据发生了变化');
            }
        })
    });
    data.name //控制台会打印出 “get”
    data.name = 'hxx' //控制台会打印出 "监听到数据发生了变化"
    使用 Object.defineProperty() 多数要配合 Object.keys() 和遍历,于是多了一层嵌套
    Object.keys(obj).forEach(key => {
      Object.defineProperty(obj, key, {
        // ...
      })
    }
    如果是这一类嵌套对象,那就必须逐层遍历,直到把每个对象的每个属性都调用 Object.defineProperty() 为止。 Vue 的源码中就能找到这样的逻辑 (叫做 walk 方法)。
    

    在数据劫持这个问题上,Proxy 可以被认为是 Object.defineProperty() 的升级版。外界对某个对象的访问,都必须经过这层拦截。因此它是针对 整个对象,而不是 对象的某个属性,所以也就不需要对 keys 进行遍历。支持数组,Proxy 不需要对数组的方法进行重载,省去了众多 hack,减少代码量等于减少了维护成本。

    let obj = {
      name: 'Eason',
      age: 30
    }
    let handler = {
      get (target, key, receiver) {
        console.log('get', key)
        return Reflect.get(target, key, receiver)
      },
      set (target, key, value, receiver) {
        console.log('set', key, value)
        return Reflect.set(target, key, value, receiver)
      }
    }
    let proxy = new Proxy(obj, handler)
    proxy.name = 'Zoe' // set name Zoe
    proxy.age = 18 // set age 18

    let arr = [1,2,3]
    let proxy = new Proxy(arr, {
        get (target, key, receiver) {
            console.log('get', key)
            return Reflect.get(target, key, receiver)
        },
        set (target, key, value, receiver) {
            console.log('set', key, value)
            return Reflect.set(target, key, value, receiver)
        }
    })
    proxy.push(4)
    // 能够打印出很多内容
    // get push     (寻找 proxy.push 方法)
    // get length   (获取当前的 length)
    // set 3 4      (设置 proxy[3] = 4)
    // set length 4 (设置 proxy.length = 4)

    19.js的编译过程

    网上也没有查到,面试官说是AU,GO

    20.高阶函数

    map():reduce():filter();filter也是一个常用的操作,它用于把Array的某些元素过滤掉,然后返回剩下的元素。和map()类似,Array的filter()也接收一个函数。和map()不同的是,filter()把传入的函数依次作用于每个元素,然后根据返回值是true还是false决定保留还是丢弃该元素。sort排序算法

    函数作为参数或者函数返回函数
    array.reduce(function(total, currentValue, currentIndex, arr), initialValue)
    total:必需。初始值, 或者计算结束后的返回值。currentValue:可选。当前元素的索引.
    arr:可选。当前元素所属的数组对象。initialValue:可选。传递给函数的初始值

    function pow(x) {
        return x * x;
    }
    
    var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
    arr.map(pow); // [1, 4, 9, 16, 25, 36, 49, 64, 81]
    var arr=[1,2,3]
    arr.reduce((pre,cur)=>{
    return pre+cur
    })//6
    //例如,在一个Array中,删掉偶数,只保留奇数,可以这么写:
    var arr = [1, 2, 4, 5, 6, 9, 10, 15];var r = arr.filter(function (x) {
        return x % 2 !== 0;
    });
    r; // [1, 5, 9, 15]
    var arr = [10, 20, 1, 2];
    arr.sort((a,b)=>{
    return a-b})// [1, 2, 10, 20]
    
    

    20.函数柯里化

    函数柯里化是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

    function add(){
    var args=Array.prototype.slice.call(arguments);
    // 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
    var adder=function(){
    args.push(...arguments)
    return adder;
    }
    // 利用toString隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
    adder.toString=function(){
    return adder.reduce((a,b)=>{
    return a+b}
    )
    }
    return adder;
    }
    add(1)(2)(3) // 6add(1, 2, 3)(4) // 10add(1)(2)(3)(4)(5) // 15
    
    

    21.import,require,link的区别

    • node编程中最重要的思想就是模块化,importrequire都是被模块化所使用。

      遵循规范
      • require 是 AMD规范引入方式
      • import是es6的一个语法标准,如果要兼容浏览器的话必须转化成es5的语法
      调用时间
      • require是运行时调用,所以require理论上可以运用在代码的任何地方
      • import是编译时调用,所以必须放在文件开头
      本质
      • require是赋值过程,其实require的结果就是对象、数字、字符串、函数等,再把require的结果赋值给某个变量
      • import是解构过程,但是目前所有的引擎都还没有实现import,我们在node中使用babel支持ES6,也仅仅是将ES6转码为ES5再执行,import语法会被转码为require

    22.pwa,amt了解么

    PWA并不是单指某一项技术,你更可以把它理解成是一种思想和概念,目的就是对标原生app,将Web网站通过一系列的Web技术去优化它,提升其安全性,性能,流畅性,用户体验等各方面指标,最后达到用户就像在用app一样的感觉。

    PWA中包含的核心功能及特性如下:
    1. Web App Manifest
    2. Service Worker
    3. Cache API 缓存
    4. Push&Notification 推送与通知
    5. Background Sync 后台同步
    6. 响应式设计

    23.缓存机制(强缓存,协商缓存,本地缓存)

    segmentfault.com/a/119000001…

    24.介绍一下WebSocket

    WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

    在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

    现在,很多网站为了实现推送技术,所用的技术都是 Ajax 轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。

    HTML5 定义的 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。

    25.前端安全

    主要有两种xss,CSRF攻击

    xss:

    在项目开发中,评论是个常见的功能,如果直接把评论的内容保存到数据库,那么显示的时候就可能被攻击。

    当前用户的登录凭证存储于服务器的 session 中,而在浏览器中是以 cookie 的形式存储的。如果攻击者能获取到用户登录凭证的 Cookie,甚至可以绕开登录流程,直接设置这个 Cookie 值,来访问用户的账号。

    防御:

    httpOnly: 在 cookie 中设置 HttpOnly 属性后,js脚本将无法读取到 cookie 信息。对一些字符进行转义:& --> &amp; < --> &lt; > -->&gt; " --> &quot; ' --> &#x27; / --> &#x2F;

    JS中可以使用JavascriptEncode。需要对“\”对特殊字符转义,输出的变量的必须在引号内部。

    csrf:跨站点请求伪造(Cross-Site Request Forgeries),也被称为 one-click attack 或者 session riding。冒充用户发起请求(在用户不知情的情况下), 完成一些违背用户意愿的事情(如修改用户信息,删初评论等)。

    案例:

    1. 比如某网站的转账操作
    2. 受害者张三给李四转账100

    防御

    1. 验证码;强制用户必须与应用进行交互,才能完成最终请求。此种方式能很好的遏制 csrf,但是用户体验比较差。
    2. 尽量使用 post ,限制 get 使用;上一个例子可见,get 太容易被拿来做 csrf 攻击,但是 post 也并不是万无一失,攻击者只需要构造一个form就可以。
    3. Referer check;请求来源限制,此种方法成本最低,但是并不能保证 100% 有效,因为服务器并不是什么时候都能取到 Referer,而且低版本的浏览器存在伪造 Referer 的风险。
    4. token;token 验证的 CSRF 防御机制是公认最合适的方案

    28.nodejs用过什么?nodeJs事件怎么循环的

    • timers: 这个阶段执行定时器队列中的回调如 setTimeout()setInterval()
    • I/O callbacks: 这个阶段执行几乎所有的回调。但是不包括close事件,定时器和setImmediate()的回调。
    • idle, prepare: 这个阶段仅在内部使用,可以不必理会。
    • poll: 等待新的I/O事件,node在一些特殊情况下会阻塞在这里。
    • check: setImmediate()的回调会在这个阶段执行。
    • close callbacks: 例如socket.on('close', ...)这种close事件的回调。

    29.koa和express框架的区别

  • Express 是一个Node.js的基础框架,主要基于 Connect 中间件,并且自身封装了路由(需要配合bodyParser)、视图处理等功能,使用人数众多,弊端是callback回调方式。
  • Koa 是一个比Express更精简,使用node新特性的中间件框架。其提供的是一个架子,而几乎所有的功能都需要由第三方中间件完成,比如koa-router, koa-view等。
    Koa 利用 co 作为底层运行框架,利用 Generator 的特性,实现“无回调”的异步处理,路由处理 Express 是自身集成的,而 Koa 需要引入中间件。
  • var koa = require('koa')
    var route = require('koa-route') //中间件
    var app = koa()
    app.use(route.get('/', function *(){
      this.body = 'Hello World'
    }))

    面试官概括说koa中间件要自己加,express自集成了好多中间件。

    30.koa-body的原理,服务器的渲染

    这个没有查到

    31.描述react的生命周期

    第一个是组件初始化(initialization)阶段

    第二个是组件的挂载(Mounting)阶段

    此阶段分为componentWillMount,render,componentDidMount三个时期。

    • componentWillMount:

    在组件挂载到DOM前调用,且只会被调用一次,在这边调用this.setState不会引起组件重新渲染,也可以把写在这边的内容提前到constructor()中,所以项目中很少用。

    • render:

    根据组件的props和state(无两者的重传递和重赋值,论值是否有变化,都可以引起组件重新render) ,return 一个React元素(描述组件,即UI),不负责组件实际渲染工作,之后由React自身根据此元素去渲染出页面DOM。render是纯函数(Pure function:函数的返回结果只依赖于它的参数;函数执行过程里面没有副作用),不能在里面执行this.setState,会有改变组件状态的副作用。

    • componentDidMount:

    组件挂载到DOM后调用,且只会被调用一次

    第三个是组件的更新(update)阶段

    在componentWillReceiveProps方法中,将props转换成自己的state

    在该函数(componentWillReceiveProps)中调用 this.setState() 将不会引起第二次渲染。

    此阶段分为componentWillReceiveProps,shouldComponentUpdate,componentWillUpdate,render,componentDidUpdate

    卸载阶段

    此阶段只有一个生命周期方法:componentWillUnmount

    • componentWillUnmount

    此方法在组件被卸载前调用,可以在这里执行一些清理工作,比如清楚组件中使用的定时器,清楚componentDidMount中手动创建的DOM元素等,以避免引起内存泄漏。

    新引入了两个新的生命周期函数:getDerivedStateFromProps,getSnapshotBeforeUpdate

    getDerivedStateFromProps本来(React v16.3中)是只在创建和更新(由父组件引发部分),也就是不是不由父组件引发,那么getDerivedStateFromProps也不会被调用,如自身setState引发或者forceUpdate引发。
    getSnapshotBeforeUpdate()
     被调用于render之后,可以读取但无法使用DOM的时候。它使您的组件可以在可能更改之前从DOM捕获一些信息(例如滚动位置)。此生命周期返回的任何值都将作为参数传递给componentDidUpdate()。

    32.描述一下diff算法,diff算法是怎么对比两个节点的

    clipboard.png

  • 从上至下,从左往右遍历对象,树进行深度递归,这一步会给每个节点添加索引,便于最后渲染差异。判断新旧节点的tagName是否相同,如果不相同代表节点被替换了。如果没有更改tagName,就需要判断是否有子集,有的话进行第二部算法。
  • 一旦节点有子集,就去判断子元素是否有不同。要进行判断是否有节点移除,在新的列表中判断是否有新的节点加入,还需要判断节点是否有移动。通过key来判断节点是否发生移动。
  • 判断完差异后,把差异记录下来。对比完两颗树以后,通过差异去局部更新DOM,实现性能的最优化。
  • 33.react怎么进行路由的动态加载

    根据官方文档中的 Code Splitting 部分,实现动态加载需要 webpackbabel-plugin-systax-dynamic-importreact-loadable
    修改后:

    <Route exact path="/settings"
      component={Loadable({
        loader: () => import(/* webpackChunkName: "Settings" */ './Settings.js'),
        loading:Loading                            
      })}
    />

    34.redux的原理,redux为什么能进行跨组件传值

    segmentfault.com/a/119000001…

    juejin.cn/post/684490…

    react-redux是一个轻量级的封装库,核心方法只有两个:

    • Provider
    • connect
    核心是观察者模式,观察者模式又叫发布-订阅模式,它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖它的对象都将得到通知

    //observes.js
    import React, { Component, PropTypes } from 'react';
    import pick from 'lodash/object/pick';
    
    const contextTypes = {
        observeStores: PropTypes.func.isRequired
    };
    
    export default function connect(...storeKeys) {
    
        return function (DecoratedComponent) {
    
            const wrappedDisplayName = DecoratedComponent.name;
    
            return class extends Component {
                static displayName = `ReduxObserves(${wrappedDisplayName})`;
                static contextTypes = contextTypes;
    
                constructor(props, context) {
                    super(props, context);
                    this.handleChange = this.handleChange.bind(this);
                    this.unobserve = this.context.observeStores(storeKeys , this.handleChange); //订阅store数据
                }
                
                handleChange(stateFromStores) {
                    this.currentStateFromStores = pick(stateFromStores, storeKeys);
                    this.updateState(stateFromStores);
                }
                
                updateState(stateFromStores, props) {
                    stateFromStores = stateFromStores[storeKeys[0]];
                    const state = stateFromStores;
                    this.setState(state);//通过setState进行组件更新
                }
    
                componentWillUnmount() {
                    this.unobserve();//退订
                }
    
                render() {
                    return (
                        <DecoratedComponent {...this.props}
                            {...this.state} />
                    );
                }
            };
        };
    }

    35.react的组件是怎么通讯的

    36.hooks

    juejin.cn/post/684490…

    • 其实 HOCVue 中的 mixins 作用是一致的,并且在早期 React 也是使用 mixins 的方式。但是在使用 class 的方式创建组件以后,mixins 的方式就不能使用了,并且其实 mixins 也是存在一些问题的,比如

    37.描述一下contex是怎么用的,组建通信

    blog.poetries.top/FE-Intervie…

    38.react中怎么引用样式

    三种方法引入样式,{{}},style等于一样变量的方式引入,引入一个css的文件

    39.描述下虚拟dom

    github.com/HolyZheng/h…

    在第一步算法中我们需要判断新旧节点的 tagName 是否相同,如果不相同的话就代表节点被替换了。如果没有更改 tagName 的话,就需要判断是否有子元素,有的话就进行第二步算法。在第二步算法中,我们需要判断原本的列表中是否有节点被移除,在新的列表中需要判断是否有新的节点加入,还需要判断节点是否有移动。

    40.vue的生命周期

    41.vuex

    42.在那个生命周期不可以发送ajax请求

    43.vue-router的原理,怎么监听hash,histoty的变化

    vue-router 默认模式是 hash 模式 —— 使用 URL 的 hash 来模拟一个完整的 URL,当 URL 改变时,页面不会去重新加载

    hash(#)是URL 的锚点,代表的是网页中的一个位置,单单改变#后的部分(/#/..),浏览器只会加载相应位置的内容,不会重新加载网页,也就是说 #是用来指导浏览器动作的,对服务器端完全无用,HTTP请求中不包括#;同时每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,就可以回到上一个位置;所以说Hash模式通过锚点值的改变,根据不同的值,渲染指定DOM位置的不同数据。通过hashChange事件来监听URL的变化。

    window.addEventListener('hashchange',()=>{//具体逻辑})

    HTML5 History API提供了一种功能,能让开发人员在不刷新整个页面的情况下修改站点的URL,就是利用 history.pushState API 来完成 URL 跳转而无须重新加载页面;

    由于hash模式会在url中自带#,如果不想要很丑的 hash,我们可以用路由的 history 模式,只需要在配置路由规则时,加入"mode: 'history'",这种模式充分利用 history.pushState 和hostoty.replaceState改变URL,用户后退的时候会触发popState事件

    history.pushSate(stateObject,title,URL)//新增历史记录
    history.replaceState(stateObject,title,URl)//替换当前的历史记录

    当用户点击按钮回退会触发popStae事件

    window.addEventListner('popState',e=>{
    })

    两种模式的对比:

    hash模式只可以改变#后面的内容,history模式可以通过api设置任意的同源URL

    History模式可以通过api添加任意的数据到历史记录中,hash模式只能更改哈希值

    hash模式无需后端配置兼容性比较好。history模式在用户手动输入地址或者刷新页面的时候发起uRL请求,后端需要配置index.html页面匹配不到资源的时候。

    44.mvvm描述

    45.vue双向绑定原理

    46.vuex

    原理运用了单例模式

    let Vue;class Store{    constructor(options){        this.vm = new Vue({ // new Vue 会创建vue的实例 将状态变成响应式的 如果数据更新 则试图刷新            data:{state:options.state}        });        this.state = this.vm.state        this.mutations = options.mutations;        this.actions = options.actions;    }    commit = (eventName)=>{        this.mutations[eventName](this.state)    }    dispatch = (eventName) =>{        this.actions[eventName](this);    }}const install = (_Vue)=>{    Vue = _Vue;    Vue.mixin({        beforeCreate(){            if(this.$options && this.$options.store){                this.$store = this.$options.store            }else{                this.$store = this.$parent && this.$parent.$store            }        }    })}export default {    Store,    install}

    47.vue-cli实现原理,vue-cli底层原理是怎么创建项目的,怎么把项目创建起来。

    面试官说是通过命令行的配置,从远程仓库拉下来的

    48,v-if,v-show的区别

    49.v-if和v-for的优先级

    面试官说v-if的优先级要高于v-for,因为在循环列表时候要判断数据是否存在

    50.keep-alive是怎么缓存的

    缓存动态组件:

    使用keep-alive可以将所有路径匹配到的路由组件都缓存起来,包括路由组件里面的组件,keep-alive大多数使用场景就是这种。

    <keep-alive>

    <router-view></router-view> 

     </keep-alive>

    新增属性:

    • include:匹配的 路由/组件 会被缓存
    • exclude:匹配的 路由/组件 不会被缓存 <keep-alive include="a,b"> 
     <keep-alive include="a,b">   
       <component :is="view"></component>   
     </keep-alive>       
     <!-- 正则表达式 (使用 `v-bind`) -->   
     <keep-alive :include="/a|b/">     
     <component :is="view"></component>   
     </keep-alive>       
     <!-- 数组 (使用 `v-bind`) -->  
      <keep-alive :include="['a', 'b']">     
         <component :is="view"></component>   
     </keep-alive>

    51.vue的循环列表为什么要加key

    主要是为了更快的进行diff算法

    52.vue监听字段的变化

    发布订阅者模式

    把函数push到一个数组里面,然后循环数据调用函数

    var arr=[];
    let a=()=>{console.log(a)}
    arr.push(a)//订阅a函数
    arr.push(a)//又订阅a函数
    arr.forEach(fn=>fn())//发布所有
    //订阅类
    class Dep{
    constructor(){
        this.subs=[]
    }
    //订阅函数
    addSub(sub){
    this.subs.push(sub)
    }
    //发布函数
    notify(){
    this.subs.filter(item=>typeof item !=='string').forEach(sub=>sub.update())}
    }

    前端路由

    segmentfault.com/a/119000001…

    美团的面试题:

    1.var a = ?;

    a == 1 && a == 2 && a == 3
     	

    用toString的方法

    a={
    i:1,
    toString():function(){
        return a.i++
    }
    }
    a==1 && a==2 && a==3 //true
    

    let a = {
      i: 1,
      valueOf () {
        return a.i++
      }
    }
    a==1 && a==2 && a==3 //true

    let a={
    fg:(function*(){
    yield 1;
    yield 2;
    yield 3;
    })(),
    valueof(){
    this.fg()next().value
    }
    }
    a==1 && a==2 && a==3 //true

    Object.defineProperty(window, 'a', {
        get: function() {
              return this.value = this.value ? (this.value += 1) : 1;
        }
    });
    a==1 && a==2 && a==3//true

    3.查出是杨辉三角,写出(m,n)的结果


    function findNum(n,m){  if(m>n){    return false;  }  if(m==1 || m==n){    return 1;  }	  if(m==2 || m==n-1){    return n-1;  }  return findNum(n-1,m) + findNum(n-1,m-1);}console.log(findNum(7,5))   //15

    4.var arrArrs = [{ name: 1, lableName: '小明' }, { name: 2, lableName: '小李' }, { name: 1, lableName: '小王' }, { name: 3, lableName: '校招' }, { name: 4, lableName: '小云' }];要求把最后的结果合并成为[1,“小明”,“小王”],[2,"小李"],[3,"校招"]

    let map = {};
    for(let i in arrArrs) {
      console.log(i, arrArrs[i]['lableName']);
      map[arrArrs[i]['name']] = 
        Array.isArray(map[arrArrs[i]['name']]) ? 
        [...map[arrArrs[i]['name']], arrArrs[i]['lableName']]:
        [arrArrs[i]['name'], arrArrs[i]['lableName']]
    }
    console.log(map);