《前端面试之道-JS篇》(中)

闭包

闭包的定义:函数 A 返回了一个函数 B,而且函数 B 中使用了函数 A 的变量,函数 B 就被称为闭包。javascript

闭包是指有权访问另外一个函数做用域中变量的函数.html

function A() {
  let a = 1
  function B() {
      console.log(a)
  }
  return B
}
复制代码

为何函数 B 还能引用到函数 A 中的变量?java

由于函数 A 中的变量这时候是存储在堆上的。如今的 JS 引擎能够经过逃逸分析辨别出哪些变量须要存储在堆上,哪些须要存储在栈上。面试

函数生命周期图示:编程

闭包做用:数组

  1. 读取函数内部的变量, 内部函数也能够引用外层的参数和变量
  2. 这些变量的值始终保持在内存中, 不会被垃圾回收机制回收 缘由:f1是f2的父函数,而f2被赋给了一个全局变量,这致使f2始终在内存中,而f2的存在依赖于f1,所以f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。

(目前浏览器引擎都基于V8,V8有gc回收机制,不用太担忧变量不会被回收)浏览器

  • 可用于setTimeout/setInterval、回调函数(callback)、事件句柄(event handle)
  • 利用闭包能够长久地保存变量又避免全局污染

使用闭包的注意点:缓存

  1. 因为闭包会使得函数中的变量都被保存在内存中,内存消耗很大,因此不能滥用闭包,不然会形成网页的性能问题,在IE中可能致使内存泄露。 解决方法: 在退出函数以前,将不使用的局部变量所有删除。服务器

  2. 闭包会在父函数外部,改变父函数内部变量的值。 若是你把父函数看成对象使用,把闭包看成它的公用方法,把内部变量看成它的私有属性,不要随便改变父函数内部变量的值闭包

面试题:循环中使用闭包解决 var 定义函数的问题?

for ( var i=0; i<5; i++) {
	setTimeout( function timer() {
		console.log( i );
	}, 1000 );
}
//setTimeout 是个异步函数,全部会先把循环所有执行完毕,这时候 i 就是 5 了,因此会输出6个 5。
复制代码

若是用箭头表示其先后的两次输出之间有 1 秒的时间间隔,而逗号表示其先后的两次输出之间的时间间隔能够忽略,代码实际运行的结果该如何描述?

5 -> 5, 5, 5, 5, 5
//循环执行过程当中,几乎同时设置了 5 个定时器,通常状况下,这些定时器都会在 1 秒以后触发,而循环完的输出是当即执行的。
复制代码

setTimeout注册的函数fn会交给浏览器的定时器模块来管理,延迟时间到了就将fn加入主进程执行队列,若是队列前面还有没有执行完的代码,则又须要花一点时间等待才能执行到fn,因此实际的延迟时间会比设置的长。

setInterval并无论上一次fn的执行结果,而是每隔100ms就将fn放入主线程队列,而两次fn之间具体间隔和JS执行状况有关。

循环中使用闭包解决 var 定义函数的问题解决办法:

  1. 使用闭包 直接使用匿名函数的当即执行模式。当即执行函数保证了每一个函数中的i是当时循环到的i值,即便循环结束,i 值在循环中已经以副本形式肯定下来了。
for (var i = 0; i < 5; i++) {
  (function(j) { //j = i
    setTimeout(function timer() {
      console.log(j);
    }, 1000);
  })(i);  //5 -> 0,1,2,3,4
}
复制代码
  1. 修改循环体
var output = function (i) {
    setTimeout(function() {
        console.log(new Date, i);
    }, 1000);
};

for (var i = 0; i < 5; i++) {
    output(i);  // 这里传过去的 i 值被复制了
}
复制代码
  1. 使用 setTimeout 的第三个参数 setTimeout(function[, delay, param1, param2, ...]) param1, param2, ...做为前面回调函数的附加参数。
for ( var i=1; i<=5; i++) {
	setTimeout( function timer(j) {
		console.log( j );
	}, i*1000, i);
}
复制代码
  1. 使用let定义 i ,由于let会建立一个块级做用域,循环中不会共享一个 i 值。
for ( let i=1; i<=5; i++) {
	setTimeout( function timer() {
		console.log( i );
	}, i*1000 );
}
复制代码
  1. (补充)代码执行时,当即输出 0,以后每隔 1 秒依次输出 1,2,3,4,循环结束后在大概第 5 秒的时候输出 5。 zhuanlan.zhihu.com/p/25855075 Promise异步解决方案
const tasks = []; // 这里存放异步操做的 Promise
const output = (i) => new Promise((resolve) => {
    setTimeout(() => {
        console.log(new Date, i);
        resolve();
    }, 1000 * i);
});

// 生成所有的异步操做
for (var i = 0; i < 5; i++) {
    tasks.push(output(i));
}

// 异步操做完成以后,输出最后的 i
Promise.all(tasks).then(() => {
    setTimeout(() => {
        console.log(new Date, i);
    }, 1000);
});
复制代码

如何使用 ES7 中的 async await 特性来让这段代码变的更简洁?

// 模拟其余语言中的 sleep,实际上能够是任何异步操做
const sleep = (timeountMS) => new Promise((resolve) => {
    setTimeout(resolve, timeountMS);
});

(async () => {  // 声明即执行的 async 函数表达式
    for (var i = 0; i < 5; i++) {
        await sleep(1000);
        console.log(new Date, i);
    }

    await sleep(1000);
    console.log(new Date, i);
})();
复制代码

深浅拷贝

  1. 值类型和引用类型:

值类型:String(字符串),Number(数值),Boolean(布尔值),Undefined,Null 引用类型:Array(数组),Object(对象),Function(函数)

  1. 值类型的变量会保存在栈内存中; 引用类型的变量名保存在栈内存中,其实是一个存放在栈内存的指针,这个指针指向堆内存中的地址,变量值存放在堆内存中。

栈(stack)为自动分配的内存空间,它由系统自动释放;而堆(heap)则是动态分配的内存,大小不定也不会自动释放。 内存中的栈区域存放变量以及指向堆区域存储位置的指针,内容存放在堆中。

  1. 基本类型的比较是值的比较,引用类型的比较是引用的比较。
var aa = 1;
var bb = 1;
console.log(aa === bb);//true
//比较的时候最好使用严格等,由于 == 会进行类型转换

var a = [1,2,3];
var b = [1,2,3];
console.log(a === b); // false
//虽然变量 a 和变量 b 都是表示一个内容为 1,2,3 的数组,可是其在内存中的位置不同,也就是说变量 a 和变量 b 指向的不是同一个对象,因此他们是不相等的。
复制代码
  1. 赋值:

基本类型的赋值是传值: 在内存中新开辟一段栈内存,而后再将值赋值到新的栈中。因此基本类型赋值的两个变量是两个独立相互不影响的变量。

引用类型的赋值是传址: 只是改变指针的指向,也就是说引用类型的赋值是对象保存在栈中的地址的赋值,这样的话两个变量就指向同一个对象,所以二者之间操做互相有影响。

浅拷贝:

浅拷贝: 从新在堆中建立内存,拷贝先后对象的基本数据类型互不影响。只拷贝一层,不能对对象中的子对象进行拷贝。

赋值和浅拷贝的区别:

var obj1 = {    //原始数据
        'name' : 'zhangsan',
        'age' :  '18',
        'language' : [1,[2,3],[4,5]],
    };

    var obj2 = obj1;  //赋值操做


    var obj3 = shallowCopy(obj1);  //浅拷贝
    function shallowCopy(src) {
        var dst = {};
        for (var prop in src) {
            if (src.hasOwnProperty(prop)) {
                dst[prop] = src[prop];
            }
        }
        return dst;
    }

    obj2.name = "lisi";
    obj3.age = "20";

    obj2.language[1] = ["二","三"];
    obj3.language[2] = ["四","五"];

    console.log(obj1);  
    //obj1 = {
    // 'name' : 'lisi',
    // 'age' : '18',
    // 'language' : [1,["二","三"],["四","五"]],
    //};

    console.log(obj2);
    //obj2 = {
    // 'name' : 'lisi',
    // 'age' : '18',
    // 'language' : [1,["二","三"],["四","五"]],
    //};

    console.log(obj3);
    //obj3 = {
    // 'name' : 'zhangsan',
    // 'age' : '20',
    // 'language' : [1,["二","三"],["四","五"]],
    //};
复制代码

改变 obj2 的 name 属性和 obj3 的 age 属性,能够看到,改变赋值获得的对象 obj2 同时也会改变原始值 obj1,而改变浅拷贝获得的的 obj3 则不会改变原始对象 obj1。

说明赋值获得的对象 obj2 只是将指针改变,其引用的仍然是同一个对象,而浅拷贝获得的的 obj3 则是从新建立了新对象。

改变了赋值获得的对象 obj2 和浅拷贝获得的 obj3 中的 language 属性的第二个值和第三个值(language 是一个数组,也就是引用类型)。 结果可见,不管是修改赋值获得的对象 obj2 和浅拷贝获得的 obj3 都会改变原始数据。

浅拷贝只复制一层对象的属性,并不包括对象里面的为引用类型的数据。因此就会出现改变浅拷贝获得的 obj3 中的引用类型时,会使原始数据获得改变。

总结:

浅拷贝实现方案:

  1. 经过Object.assign(target,source1,source2,source3);

Object.assign 是ES6新添加的接口,用于将全部可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。

let a = {
    age: 1
}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // 1
复制代码
  1. 经过 Array.prototype.slice(begin, end)

slice() 方法返回一个新的数组对象,这一对象是一个由 begin 和 end(不包括end)决定的原数组的浅拷贝。

  1. 经过Array.prototype.concat()

concat() 方法用于合并多个数组,并返回一个新的数组,和slice方法相似。 var new_array = old_array.concat(value1[, value2[, ...[, valueN]]])

  1. ES6中的展开运算符(...)

经过展开运算符 (...) , 以更加简洁的形式将一个对象的可枚举属性拷贝至另外一个对象。 须要转换编译器才能将对象展开运算符应用在生产环境中, 如 Babel

Babel是一个普遍使用的转码器,能够将ES6代码转为ES5代码,从而在现有环境执行。

替代函数的apply方法: 再也不须要apply方法,将数组转为函数的参数了

// ES5 的写法
function f(x, y, z) {
  // ...
}
var args = [0, 1, 2];
f.apply(null, args);

// ES6的写法
let args = [0, 1, 2];
f(...args);
复制代码

使用展开运算符复制数组/合并数组/......

//复制数组
const a1 = [1, 2];
const a2 = [...a1]; // 写法一
const [...a2] = a1; // 写法二

//合并数组
const arr1 = ['a', 'b'];
const arr2 = ['c'];
const arr3 = ['d', 'e'];

arr1.concat(arr2, arr3); // ES5 的合并数组
[...arr1, ...arr2, ...arr3] // ES6 的合并数组
复制代码

深拷贝:

深拷贝: 对对象以及对象中的全部子对象进行递归拷贝,拷贝先后的两个对象互不影响。

如何实现深拷贝:递归调用刚刚的浅拷贝,把全部属于对象的属性类型都遍历赋给另外一个对象。

能够看下源码

JSON.parse(JSON.stringify(object)) :一般状况下,复杂数据都是能够序列化的,因此这个函数能够解决大部分问题,而且该函数是内置函数中处理深拷贝性能最快的。

JSON.stringify() 方法是将JavaScript值(对象或者数组)转换为JSON字符串"{"a":1,"b":2}"。 JSON.parse() 方法用来解析JSON字符串,构造出JSON对象age:"23" name:"lisa"。

let a = {
    age: 1,
    jobs: {
        first: 'FE'
    }
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE
复制代码

局限性:

  • 忽略 undefined 和 symbol
  • 不能序列化函数
  • 不能解决循环引用的对象

在遇到函数、 undefined 或者 symbol 的时候,该对象不能正常的序列化, 此时可使用 lodash 的深拷贝函数

若是你所需拷贝的对象含有内置类型而且不包含函数,可使用 MessageChannel

function structuralClone(obj) {
  return new Promise(resolve => {
    const {port1, port2} = new MessageChannel();
    port2.onmessage = ev => resolve(ev.data);
    port1.postMessage(obj);
  });
}

var obj = {a: 1, b: {
    c: b
}}
// 注意该方法是异步的
// 能够处理 undefined 和循环引用对象
(async () => {
  const clone = await structuralClone(obj)
})()
复制代码

本身实现一个深拷贝:

//经过对须要拷贝的对象的属性进行递归遍历,若是对象的属性不是基本类型时,就继续递归,知道遍历到对象属性为基本类型,而后将属性和属性值赋给新对象.
function copy(obj) {
  if (!obj || typeof obj !== 'object') {
    return
  }
  var newObj = obj.constructor === Array ? [] : {}
  for (var key in obj) {
    if (obj.hasOwnProperty(key)) {
      if (typeof obj[key] === 'object' && obj[key]) {
        newObj[key] = copy(obj[key])
      } else {
        newObj[key] = obj[key]
      }
    }
  }
  return newObj
}

var old = {a: 'old', b: {c: 'old'}}
var newObj = copy(old)
newObj.b.c = 'new'
console.log(old) // { a: 'old', b: { c: 'old' } }
console.log(newObj) // { a: 'old', b: { c: 'new' } }
复制代码

模块化

原始写法:使用"当即执行函数"(Immediately-Invoked Function Expression,IIFE),能够达到不暴露私有成员的目的。

var module1 = (function(){
&emsp;&emsp;&emsp;&emsp;var _count = 0;
&emsp;&emsp;&emsp;&emsp;var m1 = function(){
&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;//...
&emsp;&emsp;&emsp;&emsp;};
&emsp;&emsp;&emsp;&emsp;var m2 = function(){
&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;//...
&emsp;&emsp;&emsp;&emsp;};
&emsp;&emsp;&emsp;&emsp;return {
&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;m1 : m1,
&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;m2 : m2
&emsp;&emsp;&emsp;&emsp;};
&emsp;&emsp;})();
复制代码

在有 Babel 的状况下,能够直接使用 ES6 的模块化

// file a.js
export function a() {}
export function b() {}
// file b.js
export default function() {}

import {a, b} from './a.js'
import XXX from './b.js'
复制代码

CommonJS

CommonJs 由 BravoJS 提出,是 Node 独有的规范,浏览器中使用就须要用Browserify 解析。

// a.js
module.exports = {
    a: 1
}
// or
exports.a = 1
// 不能对 exports 直接赋值

// b.js
var module = require('./a.js')
module.a // -> log 1
复制代码

CommonJS 和 ES6 中的模块化的二者区别:

  1. 前者支持动态导入,也就是 require(${path}/xx.js),后者目前不支持,可是已有提案
  2. 前者是同步导入,由于用于服务端,文件都在本地,同步导入即便卡住主线程影响也不大。然后者是异步导入,由于用于浏览器,须要下载文件,若是也采用同步导入会对渲染有很大影响
  3. 前者在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,因此若是想更新值,必须从新导入一次。可是后者采用实时绑定的方式,导入导出的值都指向同一个内存地址,因此导入值会跟随导出值变化
  4. 后者会编译成 require/exports 来执行的

局限: 由于调用模块提供的方法须要等待模块加载完成,对于浏览器来讲,模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器会处于"假死"状态。因此CommonJS不适用于浏览器环境。

所以,浏览器端的模块,不能采用"同步加载"(synchronous),只能采用"异步加载"(asynchronous)。这就是AMD规范诞生的背景。

AMD

AMD(异步模块定义,Asynchronous Module Definition) 是由 RequireJS 提出的。 CMD 是由 SeaJS 提出的。

AMD采用异步方式加载模块,模块的加载不影响它后面语句的运行。全部依赖这个模块的语句,都定义在一个回调函数中,等到加载完成以后,这个回调函数才会运行。

// CommonJS
var math = require('math');
math.add(2, 3);

//AMD
//require()第一个参数[module],是一个数组,里面的成员就是要加载的模块;第二个参数callback,则是加载成功以后的回调函数。
require(['math'], function (math) {
&emsp;&emsp;&emsp;&emsp;math.add(2, 3);
&emsp;&emsp;});
复制代码

目前,主要有两个Javascript库实现了AMD规范:require.jscurl.js

http://www.ruanyifeng.com/blog/2012/11/require_js.html介绍require.js,进一步讲解AMD的用法,以及如何将模块化编程投入实战。

require.js的诞生,解决了两个问题:

  1. 实现js文件的异步加载,避免网页失去响应;
  2. 管理模块之间的依赖性,便于代码的编写和维护。
//require.js的异步加载
<script src="js/require.js" defer async="true" ></script>
//加载本身的代码文件。data-main属性的做用是,指定网页程序的主模块
<script src="js/require.js" data-main="js/main"></script>
复制代码

主模块的写法:

require(['moduleA', 'moduleB', 'moduleC'], function (moduleA, moduleB, moduleC){
&emsp;&emsp;&emsp;&emsp;// some code here
&emsp;&emsp;});
复制代码

require()函数接受两个参数。第一个参数是一个数组,表示所依赖的模块,上例就是['moduleA', 'moduleB', 'moduleC'],即主模块依赖这三个模块;第二个参数是一个回调函数,当前面指定的模块都加载成功后,它将被调用。加载的模块会以参数形式传入该函数,从而在回调函数内部就可使用这些模块。

防抖和节流

防抖

PS:防抖和节流的做用都是防止函数屡次调用。

区别在于,假设一个用户一直触发这个函数,且每次触发函数的间隔小于wait,防抖只会调用一次,而节流会每隔必定时间(参数wait)调用函数。

袖珍版防抖实现,只能在最后调用:

// func是用户传入须要防抖的函数
// wait是等待时间
const debounce = (func, wait = 50) => {
  // 缓存一个定时器id
  let timer = 0
  // 这里返回的函数是每次用户实际调用的防抖函数
  // 若是已经设定过定时器了就清空上一次的定时器
  // 开始一个新的定时器,延迟执行用户传入的方法
  return function(...args) {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      func.apply(this, args)
    }, wait)
  }
}
// 不难看出若是用户调用该函数的间隔小于wait,上一次的时间还未到就被清除了,并不会执行函数
复制代码

这是一个简单版的防抖,可是有缺陷,这个防抖只能在最后调用。通常的防抖会有immediate选项,表示是否当即调用。这二者的区别,举个栗子来讲:

  • 例如在搜索引擎搜索问题的时候,咱们固然是但愿用户输入完最后一个字才调用查询接口,这个时候适用延迟执行的防抖函数,它老是在一连串(间隔小于wait的)函数触发以后调用。

  • 例如用户给interviewMap点star的时候,咱们但愿用户点第一下的时候就去调用接口,而且成功以后改变star按钮的样子,用户就能够立马获得反馈是否star成功了,这个状况适用当即执行的防抖函数,它老是在第一次调用,而且下一次调用必须与前一次调用的时间间隔大于wait才会触发。

带有当即执行的防抖函数:

// 这个是用来获取当前时间戳的
function now() {
  return +new Date()
}
/** * 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行 * * @param {function} func 回调函数 * @param {number} wait 表示时间窗口的间隔 * @param {boolean} immediate 设置为ture时,是否当即调用函数 * @return {function} 返回客户调用函数 */
function debounce (func, wait = 50, immediate = true) {
  let timer, context, args

  // 延迟执行函数
  const later = () => setTimeout(() => {
    // 延迟函数执行完毕,清空缓存的定时器序号
    timer = null
    // 延迟执行的状况下,函数会在延迟函数中执行
    // 使用到以前缓存的参数和上下文
    if (!immediate) {
      func.apply(context, args)
      context = args = null
    }
  }, wait)

  // 这里返回的函数是每次实际调用的函数
  return function(...params) {
    // 若是没有建立延迟执行函数(later),就建立一个
    if (!timer) {
      timer = later()
      // 若是是当即执行,调用函数
      // 不然缓存参数和调用上下文
      if (immediate) {
        func.apply(this, params)
      } else {
        context = this
        args = params
      }
    // 若是已有延迟执行函数(later),调用的时候清除原来的并从新设定一个
    // 这样作延迟函数会从新计时
    } else {
      clearTimeout(timer)
      timer = later()
    }
  }
}
复制代码

节流

防抖动是将屡次执行变为最后一次执行,节流是将屡次执行变成每隔一段时间执行。

/** * underscore 节流函数,返回函数连续调用时,func 执行频率限定为 次 / wait * * @param {function} func 回调函数 * @param {number} wait 表示时间窗口的间隔 * @param {object} options 若是想忽略开始函数的的调用,传入{leading: false}。 * 若是想忽略结尾函数的调用,传入{trailing: false} * 二者不能共存,不然函数不能执行 * @return {function} 返回客户调用函数 */
_.throttle = function(func, wait, options) {
    var context, args, result;
    var timeout = null;
    // 以前的时间戳
    var previous = 0;
    // 若是 options 没传则设为空对象
    if (!options) options = {};
    // 定时器回调函数
    var later = function() {
      // 若是设置了 leading,就将 previous 设为 0
      // 用于下面函数的第一个 if 判断
      previous = options.leading === false ? 0 : _.now();
      // 置空一是为了防止内存泄漏,二是为了下面的定时器判断
      timeout = null;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    };
    return function() {
      // 得到当前时间戳
      var now = _.now();
      // 首次进入前者确定为 true
	  // 若是须要第一次不执行函数
	  // 就将上次时间戳设为当前的
      // 这样在接下来计算 remaining 的值时会大于0
      if (!previous && options.leading === false) previous = now;
      // 计算剩余时间
      var remaining = wait - (now - previous);
      context = this;
      args = arguments;
      // 若是当前调用已经大于上次调用时间 + wait
      // 或者用户手动调了时间
 	  // 若是设置了 trailing,只会进入这个条件
	  // 若是没有设置 leading,那么第一次会进入这个条件
	  // 还有一点,你可能会以为开启了定时器那么应该不会进入这个 if 条件了
	  // 其实仍是会进入的,由于定时器的延时
	  // 并非准确的时间,极可能你设置了2秒
	  // 可是他须要2.2秒才触发,这时候就会进入这个条件
      if (remaining <= 0 || remaining > wait) {
        // 若是存在定时器就清理掉不然会调用二次回调
        if (timeout) {
          clearTimeout(timeout);
          timeout = null;
        }
        previous = now;
        result = func.apply(context, args);
        if (!timeout) context = args = null;
      } else if (!timeout && options.trailing !== false) {
        // 判断是否设置了定时器和 trailing
	    // 没有的话就开启一个定时器
        // 而且不能不能同时设置 leading 和 trailing
        timeout = setTimeout(later, remaining);
      }
      return result;
    };
  };
复制代码
相关文章
相关标签/搜索