前端面试题(一)JS篇

内置类型

JS 中分为七种内置类型,七种内置类型又分为两大类型:基本类型和对象(Object)。html

基本类型有六种: nullundefinedbooleannumberstringsymbolc++

其中 JS 的数字类型是浮点类型的,没有整型。而且浮点类型基于 IEEE 754标准实现,在使用中会遇到某些 BugNaN 也属于 number 类型,而且 NaN 不等于自身。git

对于基本类型来讲,若是使用字面量的方式,那么这个变量只是个字面量,只有在必要的时候才会转换为对应的类型github

let a = 111 // 这只是字面量,不是 number 类型
a.toString() // 使用时候才会转换为对象类型
复制代码

对象(Object)是引用类型,在使用过程当中会遇到浅拷贝和深拷贝的问题。面试

let a = { name: 'FE' }
let b = a
b.name = 'EF'
console.log(a.name) // EF
复制代码

Typeof

typeof 对于基本类型,除了 null 均可以显示正确的类型正则表达式

typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof b // b 没有声明,可是还会显示 undefined
复制代码

typeof 对于对象,除了函数都会显示 object算法

typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'
复制代码

对于 null 来讲,虽然它是基本类型,可是会显示 object,这是一个存在好久了的 Bug编程

typeof null // 'object'
复制代码

PS:为何会出现这种状况呢?由于在 JS 的最第一版本中,使用的是 32 位系统,为了性能考虑使用低位存储了变量的类型信息,000 开头表明是对象,然而 null 表示为全零,因此将它错误的判断为 object 。虽然如今的内部类型判断代码已经改变了,可是对于这个 Bug 倒是一直流传下来。数组

若是咱们想得到一个变量的正确类型,能够经过 Object.prototype.toString.call(xx)。这样咱们就能够得到相似 [object Type] 的字符串。promise

let a
// 咱们也能够这样判断 undefined
a === undefined
// 可是 undefined 不是保留字,可以在低版本浏览器被赋值
let undefined = 1
// 这样判断就会出错
// 因此能够用下面的方式来判断,而且代码量更少
// 由于 void 后面随便跟上一个组成表达式
// 返回就是 undefined
a === void 0
复制代码

类型转换

转Boolean

在条件判断时,除了 undefinednullfalseNaN''0-0,其余全部值都转为 true,包括全部对象。

对象转基本类型

对象在转换基本类型时,首先会调用 valueOf 而后调用 toString。而且这两个方法你是能够重写的。

let a = {
    valueOf() {
    	return 0
    }
}
复制代码

固然你也能够重写 Symbol.toPrimitive ,该方法在转基本类型时调用优先级最高。

let a = {
  valueOf() {
    return 0;
  },
  toString() {
    return '1';
  },
  [Symbol.toPrimitive]() {
    return 2;
  }
}
1 + a // => 3
'1' + a // => '12'
复制代码

四则运算符

只有当加法运算时,其中一方是字符串类型,就会把另外一个也转为字符串类型。其余运算只要其中一方是数字,那么另外一方就转为数字。而且加法运算会触发三种类型转换:将值转换为原始值,转换为数字,转换为字符串。

1 + '1' // '11'
2 * '2' // 4
[1, 2] + [2, 1] // '1,22,1'
// [1, 2].toString() -> '1,2'
// [2, 1].toString() -> '2,1'
// '1,2' + '2,1' = '1,22,1'
复制代码

对于加号须要注意这个表达式 'a' + + 'b'

'a' + + 'b' // -> "aNaN"
// 由于 + 'b' -> NaN
// 你也许在一些代码中看到过 + '1' -> 1
复制代码

== 操做符

image

上图中的 toPrimitive 就是对象转基本类型。

这里来解析一道题目 [] == ![] // -> true ,下面是这个表达式为什么为 true 的步骤

// [] 转成 true,而后取反变成 false
[] == false
// 根据第 8 条得出
[] == ToNumber(false)
[] == 0
// 根据第 10 条得出
ToPrimitive([]) == 0
// [].toString() -> ''
'' == 0
// 根据第 6 条得出
0 == 0 // -> true
复制代码

比较运算符

  1. 若是是对象,就经过 toPrimitive 转换对象
  2. 若是是字符串,就经过 unicode 字符索引来比较

原型

prototype

每一个函数都有 prototype 属性,除了 Function.prototype.bind(),该属性指向原型。

每一个对象都有 __proto__ 属性,指向了建立该对象的构造函数的原型。其实这个属性指向了 [[prototype]],可是 [[prototype]] 是内部属性,咱们并不能访问到,因此使用 _proto_ 来访问。

对象能够经过 __proto__ 来寻找不属于该对象的属性,__proto__ 将对象链接起来组成了原型链。

若是你想更进一步的了解原型,能够仔细阅读 深度解析原型中的各个难点

new

  1. 新生成了一个对象
  2. 连接到原型
  3. 绑定 this
  4. 返回新对象

在调用 new 的过程当中会发生以上四件事情,咱们也能够试着来本身实现一个 new

function create() {
    // 建立一个空的对象
    let obj = new Object()
    // 得到构造函数
    let Con = [].shift.call(arguments)
    // 连接到原型
    obj.__proto__ = Con.prototype
    // 绑定 this,执行构造函数
    let result = Con.apply(obj, arguments)
    // 确保 new 出来的是个对象
    return typeof result === 'object' ? result : obj
}
复制代码

对于实例对象来讲,都是经过 new 产生的,不管是 function Foo() 仍是 let a = { b : 1 }

对于建立一个对象来讲,更推荐使用字面量的方式建立对象(不管性能上仍是可读性)。由于你使用 new Object() 的方式建立对象须要经过做用域链一层层找到 Object,可是你使用字面量的方式就没这个问题。

function Foo() {}
// function 就是个语法糖
// 内部等同于 new Function()
let a = { b: 1 }
// 这个字面量内部也是使用了 new Object()
复制代码

对于 new 来讲,还须要注意下运算符优先级。

function Foo() {
    return this;
}
Foo.getName = function () {
    console.log('1');
};
Foo.prototype.getName = function () {
    console.log('2');
};

new Foo.getName();   // -> 1
new Foo().getName(); // -> 2 
复制代码

image

从上图能够看出,new Foo() 的优先级大于 new Foo ,因此对于上述代码来讲能够这样划分执行顺序

new (Foo.getName());   
(new Foo()).getName();
复制代码

对于第一个函数来讲,先执行了 Foo.getName() ,因此结果为 1;对于后者来讲,先执行 new Foo() 产生了一个实例,而后经过原型链找到了 Foo 上的 getName 函数,因此结果为 2。

instanceof

instanceof 能够正确的判断对象的类型,由于内部机制是经过判断对象的原型链中是否是能找到类型的 prototype

咱们也能够试着实现一下 instanceof

function instanceof(left, right) {
    // 得到类型的原型
    let prototype = right.prototype
    // 得到对象的原型
    left = left.__proto__
    // 判断对象的类型是否等于类型的原型
    while (true) {
    	if (left === null)
    		return false
    	if (prototype === left)
    		return true
    	left = left.__proto__
    }
}
复制代码

this

this 是不少人会混淆的概念,可是其实他一点都不难,你只须要记住几个规则就能够了。

function foo() {
	console.log(this.a)
}
var a = 1
foo()

var obj = {
	a: 2,
	foo: foo
}
obj.foo()

// 以上二者状况 `this` 只依赖于调用函数前的对象,优先级是第二个状况大于第一个状况

// 如下状况是优先级最高的,`this` 只会绑定在 `c` 上,不会被任何方式修改 `this` 指向
var c = new foo()
c.a = 3
console.log(c.a)

// 还有种就是利用 call,apply,bind 改变 this,这个优先级仅次于 new
复制代码

以上几种状况明白了,不少代码中的 this 应该就没什么问题了,下面让咱们看看箭头函数中的 this

function a() {
    return () => {
        return () => {
        	console.log(this)
        }
    }
}
console.log(a()()())
复制代码

箭头函数实际上是没有 this 的,这个函数中的 this 只取决于他外面的第一个不是箭头函数的函数的 this。在这个例子中,由于调用 a 符合前面代码中的第一个状况,因此 thiswindow。而且 this 一旦绑定了上下文,就不会被任何代码改变。

执行上下文

当执行 JS 代码时,会产生三种执行上下文

  • 全局执行上下文
  • 函数执行上下文
  • eval 执行上下文

每一个执行上下文中都有三个重要的属性

  • 变量对象(VO),包含变量、函数声明和函数的形参,该属性只能在全局上下文中访问
  • 做用域链(JS 采用词法做用域,也就是说变量的做用域是在定义时就决定了)
  • this
var a = 10
function foo(i) {
  var b = 20
}
foo()
复制代码

对于上述代码,执行栈中有两个上下文:全局上下文和函数 foo 上下文。

stack = [
    globalContext,
    fooContext
]
复制代码

对于全局上下文来讲,VO 大概是这样的

globalContext.VO === globe
globalContext.VO = {
    a: undefined,
	foo: <Function>, } 复制代码

对于函数 foo 来讲,VO 不能访问,只能访问到活动对象(AO)

fooContext.VO === foo.AO
fooContext.AO {
    i: undefined,
	b: undefined,
    arguments: <> } // arguments 是函数独有的对象(箭头函数没有) // 该对象是一个伪数组,有 `length` 属性且能够经过下标访问元素 // 该对象中的 `callee` 属性表明函数自己 // `caller` 属性表明函数的调用者 复制代码

对于做用域链,能够把它理解成包含自身变量对象和上级变量对象的列表,经过 [[Scope]] 属性查找上级变量

fooContext.[[Scope]] = [
    globalContext.VO
]
fooContext.Scope = fooContext.[[Scope]] + fooContext.VO
fooContext.Scope = [
    fooContext.VO,
    globalContext.VO
]
复制代码

接下来让咱们看一个老生常谈的例子,var

b() // call b
console.log(a) // undefined

var a = 'Hello world'

function b() {
	console.log('call b')
}
复制代码

想必以上的输出你们确定都已经明白了,这是由于函数和变量提高的缘由。一般提高的解释是说将声明的代码移动到了顶部,这其实没有什么错误,便于你们理解。可是更准确的解释应该是:在生成执行上下文时,会有两个阶段。第一个阶段是建立的阶段(具体步骤是建立 VO),JS 解释器会找出须要提高的变量和函数,而且给他们提早在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明而且赋值为 undefined,因此在第二个阶段,也就是代码执行阶段,咱们能够直接提早使用。

在提高的过程当中,相同的函数会覆盖上一个函数,而且函数优先于变量提高

b() // call b second

function b() {
	console.log('call b fist')
}
function b() {
	console.log('call b second')
}
var b = 'Hello world'
复制代码

var 会产生不少错误,因此在 ES6中引入了 letlet 不能在声明前使用,可是这并非常说的 let 不会提高,let 提高了声明但没有赋值,由于临时死区致使了并不能在声明前使用。

对于非匿名的当即执行函数须要注意如下一点

var foo = 1
(function foo() {
    foo = 10
    console.log(foo)
}()) // -> ƒ foo() { foo = 10 ; console.log(foo) }
复制代码

由于当 JS 解释器在遇到非匿名的当即执行函数时,会建立一个辅助的特定对象,而后将函数名称做为这个对象的属性,所以函数内部才能够访问到 foo,可是这个值又是只读的,因此对它的赋值并不生效,因此打印的结果仍是这个函数,而且外部的值也没有发生更改。

specialObject = {};
 
Scope = specialObject + Scope;
 
foo = new FunctionExpression;
foo.[[Scope]] = Scope;
specialObject.foo = foo; // {DontDelete}, {ReadOnly}
 
delete Scope[0]; // remove specialObject from the front of scope chain
复制代码

闭包

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

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

你是否会疑惑,为何函数 A 已经弹出调用栈了,为何函数 B 还能引用到函数 A 中的变量。由于函数 A 中的变量这时候是存储在堆上的。如今的 JS 引擎能够经过逃逸分析辨别出哪些变量须要存储在堆上,哪些须要存储在栈上。

经典面试题,循环中使用闭包解决 var 定义函数的问题

for ( var i=1; i<=5; i++) {
	setTimeout( function timer() {
		console.log( i );
	}, i*1000 );
}
复制代码

首先由于 setTimeout 是个异步函数,全部会先把循环所有执行完毕,这时候 i 就是 6 了,因此会输出一堆 6。

解决办法两种,第一种使用闭包

for (var i = 1; i <= 5; i++) {
  (function(j) {
    setTimeout(function timer() {
      console.log(j);
    }, j * 1000);
  })(i);
}
复制代码

第二种就是使用 setTimeout 的第三个参数

for ( var i=1; i<=5; i++) {
	setTimeout( function timer(j) {
		console.log( j );
	}, i*1000, i);
}
复制代码

第三种就是使用 let 定义 i

for ( let i=1; i<=5; i++) {
	setTimeout( function timer() {
		console.log( i );
	}, i*1000 );
}
复制代码

由于对于 let 来讲,他会建立一个块级做用域,至关于

{ // 造成块级做用域
  let i = 0
  {
    let ii = i
    setTimeout( function timer() {
        console.log( ii );
    }, i*1000 );
  }
  i++
  {
    let ii = i
  }
  i++
  {
    let ii = i
  }
  ...
}
复制代码

深浅拷贝

let a = {
    age: 1
}
let b = a
a.age = 2
console.log(b.age) // 2
复制代码

从上述例子中咱们能够发现,若是给一个变量赋值一个对象,那么二者的值会是同一个引用,其中一方改变,另外一方也会相应改变。

一般在开发中咱们不但愿出现这样的问题,咱们可使用浅拷贝来解决这个问题。

浅拷贝

首先能够经过 Object.assign 来解决这个问题。

let a = {
    age: 1
}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // 1
复制代码

固然咱们也能够经过展开运算符(…)来解决

let a = {
    age: 1
}
let b = {...a}
a.age = 2
console.log(b.age) // 1
复制代码

一般浅拷贝就能解决大部分问题了,可是当咱们遇到以下状况就须要使用到深拷贝了

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

浅拷贝只解决了第一层的问题,若是接下去的值中还有对象的话,那么就又回到刚开始的话题了,二者享有相同的引用。要解决这个问题,咱们须要引入深拷贝。

深拷贝

这个问题一般能够经过 JSON.parse(JSON.stringify(object)) 来解决。

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
  • 不能序列化函数
  • 不能解决循环引用的对象
let obj = {
  a: 1,
  b: {
    c: 2,
    d: 3,
  },
}
obj.c = obj.b
obj.e = obj.a
obj.b.c = obj.c
obj.b.d = obj.b
obj.b.e = obj.b.c
let newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj)
复制代码

若是你有这么一个循环引用对象,你会发现你不能经过该方法深拷贝

image

在遇到函数、 undefined 或者 symbol 的时候,该对象也不能正常的序列化

let a = {
    age: undefined,
    sex: Symbol('male'),
    jobs: function() {},
    name: 'yck'
}
let b = JSON.parse(JSON.stringify(a))
console.log(b) // {name: "yck"}
复制代码

你会发如今上述状况中,该方法会忽略掉函数和 undefined

可是在一般状况下,复杂数据都是能够序列化的,因此这个函数能够解决大部分问题,而且该函数是内置函数中处理深拷贝性能最快的。固然若是你的数据中含有以上三种状况下,可使用 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)
})()
复制代码

模块化

在有 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 是 Node 独有的规范,浏览器中使用就须要用到 Browserify 解析了。

// a.js
module.exports = {
    a: 1
}
// or
exports.a = 1

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

在上述代码中,module.exportsexports 很容易混淆,让咱们来看看大体内部实现

var module = require('./a.js')
module.a
// 这里其实就是包装了一层当即执行函数,这样就不会污染全局变量了,
// 重要的是 module 这里,module 是 Node 独有的一个变量
module.exports = {
    a: 1
}
// 基本实现
var module = {
  exports: {} // exports 就是个空对象
}
// 这个是为何 exports 和 module.exports 用法类似的缘由
var exports = module.exports
var load = function (module) {
    // 导出的东西
    var a = 1
    module.exports = a
    return module.exports
};
复制代码

再来讲说 module.exportsexports,用法实际上是类似的,可是不能对 exports 直接赋值,不会有任何效果。

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

  • 前者支持动态导入,也就是 require(${path}/xx.js),后者目前不支持,可是已有提案

  • 前者是同步导入,由于用于服务端,文件都在本地,同步导入即便卡住主线程影响也不大。然后者是异步导入,由于用于浏览器,须要下载文件,若是也采用同步导入会对渲染有很大影响

  • 前者在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,因此若是想更新值,必须从新导入一次。可是后者采用实时绑定的方式,导入导出的值都指向同一个内存地址,因此导入值会跟随导出值变化

  • 后者会编译成 require/exports 来执行的

AMD

AMD 是由 RequireJS 提出的

// AMD
define(['./a', './b'], function(a, b) {
    a.do()
    b.do()
})
define(function(require, exports, module) {   
    var a = require('./a')  
    a.doSomething()   
    var b = require('./b')
    b.doSomething()
})

复制代码

防抖

你是否在平常开发中遇到一个问题,在滚动事件中须要作个复杂计算或者实现一个按钮的防二次点击操做。

这些需求均可以经过函数防抖动来实现。尤为是第一个需求,若是在频繁的事件回调中作复杂计算,颇有可能致使页面卡顿,不如将屡次计算合并为一次计算,只在一个精确点作操做。

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()
    }
  }
}
复制代码

总体函数实现的不难,总结一下。

  • 对于按钮防点击来讲的实现:若是函数是当即执行的,就当即调用,若是函数是延迟执行的,就缓存上下文和参数,放到延迟函数中去执行。一旦我开始一个定时器,只要我定时器还在,你每次点击我都从新计时。一旦你点累了,定时器时间到,定时器重置为 null,就能够再次点击了。
  • 对于延时执行函数来讲的实现:清除定时器ID,若是是延迟调用就调用函数

节流

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

/** * 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;
    };
  };
复制代码

继承

在 ES5 中,咱们可使用以下方式解决继承的问题

function Super() {}
Super.prototype.getNumber = function() {
  return 1
}

function Sub() {}
let s = new Sub()
Sub.prototype = Object.create(Super.prototype, {
  constructor: {
    value: Sub,
    enumerable: false,
    writable: true,
    configurable: true
  }
})
复制代码

以上继承实现思路就是将子类的原型设置为父类的原型

在 ES6 中,咱们能够经过 class 语法轻松解决这个问题

class MyDate extends Date {
  test() {
    return this.getTime()
  }
}
let myDate = new MyDate()
myDate.test()
复制代码

可是 ES6 不是全部浏览器都兼容,因此咱们须要使用 Babel 来编译这段代码。

若是你使用编译过得代码调用 myDate.test() 你会惊奇地发现出现了报错

image

由于在 JS 底层有限制,若是不是由 Date 构造出来的实例的话,是不能调用 Date 里的函数的。因此这也侧面的说明了:ES6 中的 class 继承与 ES5 中的通常继承写法是不一样的

既然底层限制了实例必须由 Date 构造出来,那么咱们能够改变下思路实现继承

function MyData() {

}
MyData.prototype.test = function () {
  return this.getTime()
}
let d = new Date()
Object.setPrototypeOf(d, MyData.prototype)
Object.setPrototypeOf(MyData.prototype, Date.prototype)
复制代码

以上继承实现思路:先建立父类实例 => 改变实例原先的 _proto__ 转而链接到子类的 prototype => 子类的 prototype__proto__ 改成父类的 prototype

经过以上方法实现的继承就能够完美解决 JS 底层的这个限制。

call, apply, bind 区别

首先说下前二者的区别。

callapply 都是为了解决改变 this 的指向。做用都是相同的,只是传参的方式不一样。

除了第一个参数外,call 能够接收一个参数列表,apply 只接受一个参数数组。

let a = {
    value: 1
}
function getValue(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value)
}
getValue.call(a, 'yck', '24')
getValue.apply(a, ['yck', '24'])
复制代码

模拟实现 call 和 apply

能够从如下几点来考虑如何实现

  • 不传入第一个参数,那么默认为 window
  • 改变了 this 指向,让新的对象能够执行该函数。那么思路是否能够变成给新的对象添加一个函数,而后在执行完之后删除?
Function.prototype.myCall = function (context) {
  var context = context || window
  // 给 context 添加一个属性
  // getValue.call(a, 'yck', '24') => a.fn = getValue
  context.fn = this
  // 将 context 后面的参数取出来
  var args = [...arguments].slice(1)
  // getValue.call(a, 'yck', '24') => a.fn('yck', '24')
  var result = context.fn(...args)
  // 删除 fn
  delete context.fn
  return result
}
复制代码

以上就是 call 的思路,apply 的实现也相似

Function.prototype.myApply = function (context) {
  var context = context || window
  context.fn = this

  var result
  // 须要判断是否存储第二个参数
  // 若是存在,就将第二个参数展开
  if (arguments[1]) {
    result = context.fn(...arguments[1])
  } else {
    result = context.fn()
  }

  delete context.fn
  return result
}
复制代码

bind 和其余两个方法做用也是一致的,只是该方法会返回一个函数。而且咱们能够经过 bind 实现柯里化。

一样的,也来模拟实现下 bind

Function.prototype.myBind = function (context) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
  var _this = this
  var args = [...arguments].slice(1)
  // 返回一个函数
  return function F() {
    // 由于返回了一个函数,咱们能够 new F(),因此须要判断
    if (this instanceof F) {
      return new _this(...args, ...arguments)
    }
    return _this.apply(context, args.concat(...arguments))
  }
}
复制代码

Promise 实现

Promise 是 ES6 新增的语法,解决了回调地狱的问题。

能够把 Promise 当作一个状态机。初始是 pending 状态,能够经过函数 resolvereject ,将状态转变为 resolved 或者 rejected 状态,状态一旦改变就不能再次变化。

then 函数会返回一个 Promise 实例,而且该返回值是一个新的实例而不是以前的实例。由于 Promise 规范规定除了 pending 状态,其余状态是不能够改变的,若是返回的是一个相同实例的话,多个 then 调用就失去意义了。

对于 then 来讲,本质上能够把它当作是 flatMap

// 三种状态
const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";
// promise 接收一个函数参数,该函数会当即执行
function MyPromise(fn) {
  let _this = this;
  _this.currentState = PENDING;
  _this.value = undefined;
  // 用于保存 then 中的回调,只有当 promise
  // 状态为 pending 时才会缓存,而且每一个实例至多缓存一个
  _this.resolvedCallbacks = [];
  _this.rejectedCallbacks = [];

  _this.resolve = function (value) {
    if (value instanceof MyPromise) {
      // 若是 value 是个 Promise,递归执行
      return value.then(_this.resolve, _this.reject)
    }
    setTimeout(() => { // 异步执行,保证执行顺序
      if (_this.currentState === PENDING) {
        _this.currentState = RESOLVED;
        _this.value = value;
        _this.resolvedCallbacks.forEach(cb => cb());
      }
    })
  };

  _this.reject = function (reason) {
    setTimeout(() => { // 异步执行,保证执行顺序
      if (_this.currentState === PENDING) {
        _this.currentState = REJECTED;
        _this.value = reason;
        _this.rejectedCallbacks.forEach(cb => cb());
      }
    })
  }
  // 用于解决如下问题
  // new Promise(() => throw Error('error))
  try {
    fn(_this.resolve, _this.reject);
  } catch (e) {
    _this.reject(e);
  }
}

MyPromise.prototype.then = function (onResolved, onRejected) {
  var self = this;
  // 规范 2.2.7,then 必须返回一个新的 promise
  var promise2;
  // 规范 2.2.onResolved 和 onRejected 都为可选参数
  // 若是类型不是函数须要忽略,同时也实现了透传
  // Promise.resolve(4).then().then((value) => console.log(value))
  onResolved = typeof onResolved === 'function' ? onResolved : v => v;
  onRejected = typeof onRejected === 'function' ? onRejected : r => throw r;

  if (self.currentState === RESOLVED) {
    return (promise2 = new MyPromise(function (resolve, reject) {
      // 规范 2.2.4,保证 onFulfilled,onRjected 异步执行
      // 因此用了 setTimeout 包裹下
      setTimeout(function () {
        try {
          var x = onResolved(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (reason) {
          reject(reason);
        }
      });
    }));
  }

  if (self.currentState === REJECTED) {
    return (promise2 = new MyPromise(function (resolve, reject) {
      setTimeout(function () {
        // 异步执行onRejected
        try {
          var x = onRejected(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (reason) {
          reject(reason);
        }
      });
    }));
  }

  if (self.currentState === PENDING) {
    return (promise2 = new MyPromise(function (resolve, reject) {
      self.resolvedCallbacks.push(function () {
        // 考虑到可能会有报错,因此使用 try/catch 包裹
        try {
          var x = onResolved(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (r) {
          reject(r);
        }
      });

      self.rejectedCallbacks.push(function () {
        try {
          var x = onRejected(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (r) {
          reject(r);
        }
      });
    }));
  }
};
// 规范 2.3
function resolutionProcedure(promise2, x, resolve, reject) {
  // 规范 2.3.1,x 不能和 promise2 相同,避免循环引用
  if (promise2 === x) {
    return reject(new TypeError("Error"));
  }
  // 规范 2.3.2
  // 若是 x 为 Promise,状态为 pending 须要继续等待不然执行
  if (x instanceof MyPromise) {
    if (x.currentState === PENDING) {
      x.then(function (value) {
        // 再次调用该函数是为了确认 x resolve 的
        // 参数是什么类型,若是是基本类型就再次 resolve
        // 把值传给下个 then
        resolutionProcedure(promise2, value, resolve, reject);
      }, reject);
    } else {
      x.then(resolve, reject);
    }
    return;
  }
  // 规范 2.3.3.3.3
  // reject 或者 resolve 其中一个执行过得话,忽略其余的
  let called = false;
  // 规范 2.3.3,判断 x 是否为对象或者函数
  if (x !== null && (typeof x === "object" || typeof x === "function")) {
    // 规范 2.3.3.2,若是不能取出 then,就 reject
    try {
      // 规范 2.3.3.1
      let then = x.then;
      // 若是 then 是函数,调用 x.then
      if (typeof then === "function") {
        // 规范 2.3.3.3
        then.call(
          x,
          y => {
            if (called) return;
            called = true;
            // 规范 2.3.3.3.1
            resolutionProcedure(promise2, y, resolve, reject);
          },
          e => {
            if (called) return;
            called = true;
            reject(e);
          }
        );
      } else {
        // 规范 2.3.3.4
        resolve(x);
      }
    } catch (e) {
      if (called) return;
      called = true;
      reject(e);
    }
  } else {
    // 规范 2.3.4,x 为基本类型
    resolve(x);
  }
}
复制代码

以上就是根据 Promise / A+ 规范来实现的代码,能够经过 promises-aplus-tests 的完整测试

image

Generator 实现

Generator 是 ES6 中新增的语法,和 Promise 同样,均可以用来异步编程

// 使用 * 表示这是一个 Generator 函数
// 内部能够经过 yield 暂停代码
// 经过调用 next 恢复执行
function* test() {
  let a = 1 + 2;
  yield 2;
  yield 3;
}
let b = test();
console.log(b.next()); // > { value: 2, done: false }
console.log(b.next()); // > { value: 3, done: false }
console.log(b.next()); // > { value: undefined, done: true }
复制代码

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

// cb 也就是编译过的 test 函数
function generator(cb) {
  return (function() {
    var object = {
      next: 0,
      stop: function() {}
    };

    return {
      next: function() {
        var ret = cb(object);
        if (ret === undefined) return { value: undefined, done: true };
        return {
          value: ret,
          done: false
        };
      }
    };
  })();
}
// 若是你使用 babel 编译后能够发现 test 函数变成了这样
function test() {
  var a;
  return generator(function(_context) {
    while (1) {
      switch ((_context.prev = _context.next)) {
        // 能够发现经过 yield 将代码分割成几块
        // 每次执行 next 函数就执行一块代码
        // 而且代表下次须要执行哪块代码
        case 0:
          a = 1 + 2;
          _context.next = 4;
          return 2;
        case 4:
          _context.next = 6;
          return 3;
		// 执行完毕
        case 6:
        case "end":
          return _context.stop();
      }
    }
  });
}
复制代码

Map、FlatMap 和 Reduce

Map 做用是生成一个新数组,遍历原数组,将每一个元素拿出来作一些变换而后 append 到新的数组中。

[1, 2, 3].map((v) => v + 1)
// -> [2, 3, 4]
复制代码

Map 有三个参数,分别是当前索引元素,索引,原数组

['1','2','3'].map(parseInt)
// parseInt('1', 0) -> 1
// parseInt('2', 1) -> NaN
// parseInt('3', 2) -> NaN
复制代码

FlatMapmap 的做用几乎是相同的,可是对于多维数组来讲,会将原数组降维。能够将 FlatMap 当作是 map + flatten ,目前该函数在浏览器中还不支持。

[1, [2], 3].flatMap((v) => v + 1)
// -> [2, 3, 4]
复制代码

若是想将一个多维数组完全的降维,能够这样实现

const flattenDeep = (arr) => Array.isArray(arr)
  ? arr.reduce( (a, b) => [...a, ...flattenDeep(b)] , [])
  : [arr]

flattenDeep([1, [[2], [3, [4]], 5]])
复制代码

Reduce 做用是数组中的值组合起来,最终获得一个值

function a() {
    console.log(1);
}

function b() {
    console.log(2);
}

[a, b].reduce((a, b) => a(b()))
// -> 2 1
复制代码

async 和 await

一个函数若是加上 async ,那么该函数就会返回一个 Promise

async function test() {
  return "1";
}
console.log(test()); // -> Promise {<resolved>: "1"}
复制代码

能够把 async 当作将函数返回值使用 Promise.resolve() 包裹了下。

await 只能在 async 函数中使用

function sleep() {
  return new Promise(resolve => {
    setTimeout(() => {
      console.log('finish')
      resolve("sleep");
    }, 2000);
  });
}
async function test() {
  let value = await sleep();
  console.log("object");
}
test()
复制代码

上面代码会先打印 finish 而后再打印 object 。由于 await 会等待 sleep 函数 resolve ,因此即便后面是同步代码,也不会先去执行同步代码再来执行异步代码。

async 和 await 相比直接使用 Promise 来讲,优点在于处理 then 的调用链,可以更清晰准确的写出代码。缺点在于滥用 await 可能会致使性能问题,由于 await 会阻塞代码,也许以后的异步代码并不依赖于前者,但仍然须要等待前者完成,致使代码失去了并发性。

下面来看一个使用 await 的代码。

var a = 0
var b = async () => {
  a = a + await 10
  console.log('2', a) // -> '2' 10
  a = (await 10) + a
  console.log('3', a) // -> '3' 20
}
b()
a++
console.log('1', a) // -> '1' 1
复制代码

对于以上代码你可能会有疑惑,这里说明下原理

  • 首先函数 b 先执行,在执行到 await 10 以前变量 a 仍是 0,由于在 await 内部实现了 generatorsgenerators 会保留堆栈中东西,因此这时候 a = 0 被保存了下来
  • 由于 await 是异步操做,遇到await就会当即返回一个pending状态的Promise对象,暂时返回执行代码的控制权,使得函数外的代码得以继续执行,因此会先执行 console.log('1', a)
  • 这时候同步代码执行完毕,开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 10
  • 而后后面就是常规执行代码了

Proxy

Proxy 是 ES6 中新增的功能,能够用来自定义对象中的操做

let p = new Proxy(target, handler);
// `target` 表明须要添加代理的对象
// `handler` 用来自定义对象中的操做
复制代码

能够很方便的使用 Proxy 来实现一个数据绑定和监听

let onWatch = (obj, setBind, getLogger) => {
  let handler = {
    get(target, property, receiver) {
      getLogger(target, property)
      return Reflect.get(target, property, receiver);
    },
    set(target, property, value, receiver) {
      setBind(value);
      return Reflect.set(target, property, value);
    }
  };
  return new Proxy(obj, handler);
};

let obj = { a: 1 }
let value
let p = onWatch(obj, (v) => {
  value = v
}, (target, property) => {
  console.log(`Get '${property}' = ${target[property]}`);
})
p.a = 2 // bind `value` to `2`
p.a // -> Get 'a' = 2
复制代码

为何 0.1 + 0.2 != 0.3

由于 JS 采用 IEEE 754 双精度版本(64位),而且只要采用 IEEE 754 的语言都有该问题。

咱们都知道计算机表示十进制是采用二进制表示的,因此 0.1 在二进制表示为

// (0011) 表示循环
0.1 = 2^-4 * 1.10011(0011)
复制代码

那么如何获得这个二进制的呢,咱们能够来演算下

image

小数算二进制和整数不一样。乘法计算时,只计算小数位,整数位用做每一位的二进制,而且获得的第一位为最高位。因此咱们得出 0.1 = 2^-4 * 1.10011(0011),那么 0.2 的演算也基本如上所示,只须要去掉第一步乘法,因此得出 0.2 = 2^-3 * 1.10011(0011)

回来继续说 IEEE 754 双精度。六十四位中符号位占一位,整数位占十一位,其他五十二位都为小数位。由于 0.10.2 都是无限循环的二进制了,因此在小数位末尾处须要判断是否进位(就和十进制的四舍五入同样)。

因此 2^-4 * 1.10011...001 进位后就变成了 2^-4 * 1.10011(0011 * 12次)010 。那么把这两个二进制加起来会得出 2^-2 * 1.0011(0011 * 11次)0100 , 这个值算成十进制就是 0.30000000000000004

下面说一下原生解决办法,以下代码所示

parseFloat((0.1 + 0.2).toFixed(10))
复制代码

正则表达式

元字符

元字符 做用
. 匹配任意字符除了换行符和回车符
[] 匹配方括号内的任意字符。好比 [0-9] 就能够用来匹配任意数字
^ ^9,这样使用表明匹配以 9 开头。[^9],这样使用表明不匹配方括号内除了 9 的字符
{1, 2} 匹配 1 到 2 位字符
(yck) 只匹配和 yck 相同字符串
| 匹配 | 先后任意字符
\ 转义
* 只匹配出现 0 次及以上 * 前的字符
+ 只匹配出现 1 次及以上 + 前的字符
? ? 以前字符可选

修饰语

修饰语 做用
i 忽略大小写
g 全局搜索
m 多行

字符简写

简写 做用
\w 匹配字母数字或下划线
\W 和上面相反
\s 匹配任意的空白符
\S 和上面相反
\d 匹配数字
\D 和上面相反
\b 匹配单词的开始或结束
\B 和上面相反

V8 下的垃圾回收机制

V8 实现了准确式 GC,GC 算法采用了分代式垃圾回收机制。所以,V8 将内存(堆)分为新生代和老生代两部分。

新生代算法

新生代中的对象通常存活时间较短,使用 Scavenge GC 算法。

在新生代空间中,内存空间分为两部分,分别为 From 空间和 To 空间。在这两个空间中,一定有一个空间是使用的,另外一个空间是空闲的。新分配的对象会被放入 From 空间中,当 From 空间被占满时,新生代 GC 就会启动了。算法会检查 From 空间中存活的对象并复制到 To 空间中,若是有失活的对象就会销毁。当复制完成后将 From 空间和 To 空间互换,这样 GC 就结束了。

老生代算法

老生代中的对象通常存活时间较长且数量也多,使用了两个算法,分别是标记清除算法和标记压缩算法。

在讲算法前,先来讲下什么状况下对象会出如今老生代空间中:

  • 新生代中的对象是否已经经历过一次 Scavenge 算法,若是经历过的话,会将对象重新生代空间移到老生代空间中。
  • To 空间的对象占比大小超过 25 %。在这种状况下,为了避免影响到内存分配,会将对象重新生代空间移到老生代空间中。

老生代中的空间很复杂,有以下几个空间

enum AllocationSpace {
  // TODO(v8:7464): Actually map this space's memory as read-only.
  RO_SPACE,    // 不变的对象空间
  NEW_SPACE,   // 新生代用于 GC 复制算法的空间
  OLD_SPACE,   // 老生代常驻对象空间
  CODE_SPACE,  // 老生代代码对象空间
  MAP_SPACE,   // 老生代 map 对象
  LO_SPACE,    // 老生代大空间对象
  NEW_LO_SPACE,  // 新生代大空间对象

  FIRST_SPACE = RO_SPACE,
  LAST_SPACE = NEW_LO_SPACE,
  FIRST_GROWABLE_PAGED_SPACE = OLD_SPACE,
  LAST_GROWABLE_PAGED_SPACE = MAP_SPACE
};
复制代码

在老生代中,如下状况会先启动标记清除算法:

  • 某一个空间没有分块的时候
  • 空间中被对象超过必定限制
  • 空间不能保证新生代中的对象移动到老生代中

在这个阶段中,会遍历堆中全部的对象,而后标记活的对象,在标记完成后,销毁全部没有被标记的对象。在标记大型对内存时,可能须要几百毫秒才能完成一次标记。这就会致使一些性能上的问题。为了解决这个问题,2011 年,V8 从 stop-the-world 标记切换到增量标志。在增量标记期间,GC 将标记工做分解为更小的模块,可让 JS 应用逻辑在模块间隙执行一会,从而不至于让应用出现停顿状况。但在 2018 年,GC 技术又有了一个重大突破,这项技术名为并发标记。该技术可让 GC 扫描和标记对象时,同时容许 JS 运行,你能够点击 该博客 详细阅读。

清除对象后会形成堆内存出现碎片的状况,当碎片超过必定限制后会启动压缩算法。在压缩过程当中,将活的对象像一端移动,直到全部对象都移动完成而后清理掉不须要的内存。

相关文章
相关标签/搜索