内置类型
JS 中分为七种内置类型,七种内置类型又分为两大类型:基本类型和对象(Object)。
基本类型有六种: null
,undefined
,boolean
,number
,string
,symbol
。
其中 JS 的数字类型是浮点类型的,没有整型。并且浮点类型基于 IEEE 754标准实现,在使用中会遇到某些 Bug。NaN
也属于 number
类型,并且 NaN
不等于自身。
对于基本类型来说,如果使用字面量的方式,那么这个变量只是个字面量,只有在必要的时候才会转换为对应的类型
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]
的字符串。
let a// 我们也可以这样判断 undefineda === undefined// 但是 undefined 保留字,能够在低版本浏览器被赋值let undefined = 1// 这样判断就会出错// 所以可以用下面的方式来判断,并且代码量更少// 因为 void 后面随便跟上一个组成表达式// 返回就是 undefineda === void 0
#类型转换
#转Boolean
除了 undefined
, null
, false
, NaN
, ''
, 0
, -0
,其他所有值都转为 true
,包括所有对象。
#对象转基本类型
对象在转换基本类型时,首先会调用 valueOf
然后调用 toString
。并且这两个方法你是可以重写的。
let a = { valueOf() { return 0 }}
#四则运算符
只有当加法运算时,其中一方是字符串类型,就会把另一个也转为字符串类型。其他运算只要其中一方是数字,那么另一方就转为数字。并且加法运算会触发三种类型转换:将值转换为原始值,转换为数字,转换为字符串。
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
#==
操作符
上图中的 toPrimitive
就是对象转基本类型。
一般推荐使用 ===
判断两个值,但是你如果想知道一个值是不是 null
,你可以通过 xx == null
来比较。
这里来解析一道题目 [] == ![] // -> true
,下面是这个表达式为何为 true
的步骤
// [] 转成 true,然后取反变成 false[] == false// 根据第 8 条得出[] == ToNumber(false)[] == 0// 根据第 10 条得出ToPrimitive([]) == 0// [].toString() -> '''' == 0// 根据第 6 条得出0 == 0 // -> true
#比较运算符
如果是对象,就通过
toPrimitive
转换对象如果是字符串,就通过
unicode
字符索引来比较
#原型
每个函数都有 prototype
属性,除了 Function.prototype.bind()
,该属性指向原型。
每个对象都有 __proto__
属性,指向了创建该对象的构造函数的原型。其实这个属性指向了 [[prototype]]
,但是 [[prototype]]
是内部属性,我们并不能访问到,所以使用 _proto_
来访问。
对象可以通过 __proto__
来寻找不属于该对象的属性,__proto__
将对象连接起来组成了原型链。
如果你想更进一步的了解原型,可以仔细阅读 深度解析原型中的各个难点。
#new
新生成了一个对象
链接到原型
绑定 this
返回新对象
在调用 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(); // -> 1new Foo().getName(); // -> 2
从上图可以看出,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 = 2foo()var obj = { a: 2, foo: foo}obj.foo()// 以上两者情况 `this` 只依赖于调用函数前的对象,优先级是第二个情况大于第一个情况// 以下情况是优先级最高的,`this` 只会绑定在 `c` 上,不会被任何方式修改 `this` 指向var c = new foo()c.a = 3console.log(c.a)// 还有种就是利用 call,apply,bind 改变 this,这个优先级仅次于 new
以上几种情况明白了,很多代码中的 this
应该就没什么问题了,下面让我们看看箭头函数中的 this
function a() { return () => { return () => { console.log(this) } }}console.log(a()()())
箭头函数其实是没有 this
的,这个函数中的 this
只取决于他外面的第一个不是箭头函数的函数的 this
。在这个例子中,因为调用 a
符合前面代码中的第一个情况,所以 this
是 window
。并且 this
一旦绑定了上下文,就不会被任何代码改变。
#执行上下文
当执行 JS 代码时,会产生三种执行上下文
全局执行上下文
函数执行上下文
eval 执行上下文
每个执行上下文中都有三个重要的属性
变量对象(VO),包含变量、函数声明和函数的形参,该属性只能在全局上下文中访问
作用域链(JS 采用词法作用域,也就是说变量的作用域是在定义时就决定了)
this
var a = 10function 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.AOfooContext.AO { i: undefined, b: undefined, arguments: <>}// arguments 是函数独有的对象(箭头函数没有)// 该对象是一个伪数组,有 `length` 属性且可以通过下标访问元素// 该对象中的 `callee` 属性代表函数本身// `caller` 属性代表函数的调用者
对于作用域链,可以把它理解成包含自身变量对象和上级变量对象的列表,通过 [[Scope]]
属性查找上级变量
fooContext.[[Scope]] = [ globalContext.VO]fooContext.Scope = fooContext.[[Scope]] + fooContext.VOfooContext.Scope = [ fooContext.VO, globalContext.VO]
接下来让我们看一个老生常谈的例子,var
b() // call bconsole.log(a) // undefinedvar a = 'Hello world'function b() { console.log('call b')}
想必以上的输出大家肯定都已经明白了,这是因为函数和变量提升的原因。通常提升的解释是说将声明的代码移动到了顶部,这其实没有什么错误,便于大家理解。但是更准确的解释应该是:在生成执行上下文时,会有两个阶段。第一个阶段是创建的阶段(具体步骤是创建 VO),JS 解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为 undefined,所以在第二个阶段,也就是代码执行阶段,我们可以直接提前使用。
在提升的过程中,相同的函数会覆盖上一个函数,并且函数优先于变量提升
b() // call b secondfunction b() { console.log('call b fist')}function b() { console.log('call b second')}var b = 'Hello world'
var
会产生很多错误,所以在 ES6中引入了 let
。let
不能在声明前使用,但是这并不是常说的 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
来说
第三种就是使用 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( i ); }, i*1000 ); } i++ { let ii = i } i++ { let ii = i } ...}
#深浅拷贝
let a = { age: 1}let b = a a.age = 2console.log(b.age) // 2
从上述例子中我们可以发现,如果给一个变量赋值一个对象,那么两者的值会是同一个引用,其中一方改变,另一方也会相应改变。
通常在开发中我们不希望出现这样的问题,我们可以使用浅拷贝来解决这个问题。
#浅拷贝
首先可以通过 Object.assign
来解决这个问题。
let a = { age: 1}let b = Object.assign({}, a)a.age = 2console.log(b.age) // 1
当然我们也可以通过展开运算符(…)来解决
let a = { age: 1}let b = {...a}a.age = 2console.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
不能序列化函数
不能解决循环引用的对象
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.clet newObj = JSON.parse(JSON.stringify(obj))console.log(newObj)
如果你有这么一个循环引用对象,你会发现你不能通过该方法深拷贝
在遇到函数或者 undefined
的时候,该对象也不能正常的序列化
let a = { age: undefined, jobs: function() {}, name: 'yck'}let b = JSON.parse(JSON.stringify(a))console.log(b) // {name: "yck"}
你会发现在上述情况中,该方法会忽略掉函数和 undefined
。
但是在通常情况下,复杂数据都是可以序列化的,所以这个函数可以解决大部分问题,并且该函数是内置函数中处理深拷贝性能最快的。当然如果你的数据中含有以上三种情况下,可以使用 loadash 的深拷贝函数。
如果你所需拷贝的对象含有内置类型并且不包含函数,可以使用 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 和循环引用对象const clone = await structuralClone(obj);
#模块化
在有 Babel 的情况下,我们可以直接使用 ES6 的模块化
// file a.jsexport function a() {}export function b() {}// file b.jsexport default function() {}import {a, b} from './a.js'import XXX from './b.js'
#CommonJS
CommonJs
是 Node 独有的规范,浏览器中使用就需要用到 Browserify
解析了。
// a.jsmodule.exports = { a: 1}// orexports.a = 1// b.jsvar module = require('./a.js')module.a // -> log 1
在上述代码中,module.exports
和 exports
很容易混淆,让我们来看看大致内部实现
var module = require('./a.js')module.a// 这里其实就是包装了一层立即执行函数,这样就不会污染全局变量了,// 重要的是 module 这里,module 是 Node 独有的一个变量module.exports = { a: 1}// 基本实现var module = { exports: {} // exports 就是个空对象}// 这个是为什么 exports 和 module.exports 用法相似的原因var exports = module.exportsvar load = function (module) { // 导出的东西 var a = 1 module.exports = a return module.exports};
再来说说 module.exports
和 exports
,用法其实是相似的,但是不能对 exports
直接赋值,不会有任何效果。
对于 CommonJS
和 ES6 中的模块化的两者区别是:
前者支持动态导入,也就是
require(${path}/xx.js)
,后者目前不支持,但是已有提案前者是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用导入会对渲染有很大影响
前者在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是后者采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化
后者会编译成
require/exports
来执行的
#AMD
AMD 是由 RequireJS
提出的
// AMDdefine(['./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()})
#防抖
你是否在日常开发中遇到一个问题,在滚动事件中需要做个复杂计算或者实现一个按钮的防二次点击操作。
这些需求都可以通过函数防抖动来实现。尤其是第一个需求,如果在频繁的事件回调中做复杂计算,很有可能导致页面卡顿,不如将多次计算合并为一次计算,只在一个精确点做操作。因为防抖动的轮子很多,这里也不重新自己造个轮子了,直接使用 underscore 的源码来解释防抖动。
/** * underscore 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行 * * @param {function} func 回调函数 * @param {number} wait 表示时间窗口的间隔 * @param {boolean} immediate 设置为ture时,是否立即调用函数 * @return {function} 返回客户调用函数 */_.debounce = function(func, wait, immediate) { var timeout, args, context, timestamp, result; var later = function() { // 现在和上一次时间戳比较 var last = _.now() - timestamp; // 如果当前间隔时间少于设定时间且大于0就重新设置定时器 if (last < wait && last >= 0) { timeout = setTimeout(later, wait - last); } else { // 否则的话就是时间到了执行回调函数 timeout = null; if (!immediate) { result = func.apply(context, args); if (!timeout) context = args = null; } } }; return function() { context = this; args = arguments; // 获得时间戳 timestamp = _.now(); // 如果定时器不存在且立即执行函数 var callNow = immediate && !timeout; // 如果定时器不存在就创建一个 if (!timeout) timeout = setTimeout(later, wait); if (callNow) { // 如果需要立即执行函数的话 通过 apply 执行 result = func.apply(context, args); context = args = null; } return result; }; };
整体函数实现的不难,总结一下。
对于按钮防点击来说的实现:一旦我开始一个定时器,只要我定时器还在,不管你怎么点击都不会执行回调函数。一旦定时器结束并设置为
null
,就可以再次点击了。对于延时执行函数来说的实现:每次调用防抖动函数都会判断本次调用和之前的时间间隔,如果小于需要的时间间隔,就会重新创建一个定时器,并且定时器的延时为设定时间减去之前的时间间隔。一旦时间到了,就会执行相应的回调函数。
#节流
防抖动和节流本质是不一样的。防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行。
/** * 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()
你会惊奇地发现出现了报错
因为在 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 区别
首先说下前两者的区别。
call
和 apply
都是为了解决改变 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
状态,可以通过函数 resolve
和 reject
,将状态转变为 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.3function 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
的完整测试
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、FlapMap 和 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
FlapMap
和 map
的作用几乎是相同的,但是对于多维数组来说,会将原数组降维。可以将 FlapMap
看成是 map
+ flatten
,目前该函数在浏览器中还不支持。
[1, [2], 3].flatMap((v) => v + 1)// -> [2, 3, 4]
如果想将一个多维数组彻底的降维,可以这样实现
const flattenDeep = (arr) => Array.isArray(arr) ? arr.reduce( (a, b) => [...flattenDeep(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(sync()); // -> 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 = 0var 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
内部实现了generators
,generators
会保留堆栈中东西,所以这时候a = 0
被保存了下来因为
await
是异步操作,所以会先执行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 valuelet 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)
那么如何得到这个二进制的呢,我们可以来演算下
小数算二进制和整数不同。乘法计算时,只计算小数位,整数位用作每一位的二进制,并且得到的第一位为最高位。所以我们得出 0.1 = 2^-4 * 1.10011(0011)
,那么 0.2
的演算也基本如上所示,只需要去掉第一步乘法,所以得出 0.2 = 2^-3 * 1.10011(0011)
。
回来继续说 IEEE 754 双精度。六十四位中符号位占一位,整数位占十一位,其余五十二位都为小数位。因为 0.1
和 0.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 相同字符串 |
| | 匹配 | 前后任意字符 |
\ | 转义 |
* | 只匹配出现 -1 次以上 * 前的字符 |
+ | 只匹配出现 0 次以上 + 前的字符 |
? | ? 之前字符可选 |
#修饰语
修饰语 | 作用 |
---|---|
i | 忽略大小写 |
g | 全局搜索 |
m | 多行 |
#字符简写
简写 | 作用 |
---|---|
\w | 匹配字母数字或下划线或汉字 |
\W | 和上面相反 |
\s | 匹配任意的空白符 |
\S | 和上面相反 |
\d | 匹配数字 |
\D | 和上面相反 |
\b | 匹配单词的开始或结束 |
\B | 和上面相反 |