文章目录
1 手写 Promise 上
1.1 手写 Promise
面试答题方法论:
- 该技术要解决什么问题–why
- 该技术是怎么解决这个问题的–how
- 该技术有什么优点(对比其他技术)–pros (优点)
- 该技术有什么缺点–cons (缺点)
- 如何解决这些缺点–more
Promise 要解决什么问题–why
答:Promise 要解决的就是回调地狱的问题。回调过多导致代码复杂,不清晰。
如下图所示,该代码用 Node.js 实现了调整文件夹中图片的宽高比例。回调地狱就是不断的进行回调,这里进行了 5次。
回调地狱的出现可能是这个程序员水平不行造成的。
补充:重构上图代码。因此,出现回调地狱首先考虑的是不是因为自己的水平问题,代码没有写好。
该技术有什么优点(对比其他技术)–pros (优点)
答:
优点一,减少缩进,把函数里的函数变成 then 下面的 then。
fn1(xxx, function fn2(a){
fn3(yyy, function fn4(b){
// fn4 是函数里的函数
fn5(a+b, function fn6(){
// code
})
})
})
fn(xxx)
.then(fn2) // fn2 里面调用fn3
.then(fn4) // fn4 里面调用fn5
.then(fn6)
// 提问:fn5 怎么得到 a 和 b
// fn2 的输出作为 fn4 的输入
优点二,消灭if(err),错误处理单独放到一个函数里,如果不处理就一直等到往后抛。
fn(xxx)
.then(fn2, error1) // fn2 里面调用fn3
.then(fn4, error2) // fn4 里面调用fn5
.then(fn6, error3)
.then(null, errorAll)
// 最后一句可以写成 .catch
Promise/ A+ 标准文档:JavaScript 的Promise 的公开标准,有中文翻译,但可能不准确。
1.2 使用 chai
mocha 是提供 describe、it 和 漂亮输出的库。chai 是提供 assert 的库。
yarn --dev add ts-node mocha
局部安装- 创建目录 demo(名字随意)
yarn init -y
或npm init -y
yarn --dev add chai mocha
yarn --dev @types/chai @types/mocha
- 创建
test/index.ts
- mocha 只支持 JavaScript,不支持 TypeScript。因此,用
mocha -r ts-node/register test/index.ts
运行代码
describe 和 it 的含义,如下图所示。
出现上图错误,是默认在本地环境下运行找不到该 module 。因此要通过 --dev
安装到本地环境下。
1.3 异步测试
1.4 使用 sinon 测试函数
安装
yarn --dev add sinon sinon-chai
yarn --dev add @types/sinon @types/sinon-chai
1.5 完成 Promise A+ (上)
1. 6 then 做了什么
1.7 如何通过 pull request 完成作业
2 手写 Promise 下
3 async await 全解
4 EventHub
eventhub(也叫发布订阅模式、eventbus 等)
4.1 源码书写过程
面试解题思路:
- 确定API
- 添加测试用例
- 让测试用例通过
- 重构代码
Notice:#
表示对象的属性(class 中)。
运行 TypeScript 代码用 ts-node
注意:数组的 indexOf
对 IE 的兼容性不好,仅支持 Edge。
打注释方法 /**
+ enter
留坑给面试官问,别写的太完美了。
unknown
是一个安全的 any
,只要一旦确定就不能修改了。
4.2 改进 TypeScript
申明类型。
5 EventLoop
5.1 EventLoop的三个阶段和三个API
EventLoop 是 C++写的,是 Node.js 的概念不是浏览器的。
EventLoop 是循环的一个过程,表示不同状态。
与前端相关的就 3 个(timers、poll 和 check):
setImediate()
只有 Node.js 有。
5.2 宏任务和微任务
如果用 await
语法糖,那就将 await
转化为 Promise 的 then
的方式。
Promise 中加入队列看 then
不需要看 resolve
,resolve
是确定用那个函数。
5.3 总结(必看)
async await 全解
Promise 精讲
Promise 串行面试题
感觉不好
面试题解
感觉不好
await 基本用法
因为,官网的 await 在发布之前,就有第三方写过await 了。因此需要区分,就在 funciton 前面加 async 来以示区分。
await 和 Promise 要配合使用。
function ajax(){
return new Promise((resolve, reject) => {
reject({
response: {
status: 403
}
})
// resolve({
// date:{name: 'Jonathan Ben'}
// })
})
}
const error = (e) => {
console.log(e)
console.log('提示用户没有权限')
throw e
}
async function fn(){
const response = await ajax().then(null, error)
console.log(response)
}
fn()
await 在 for 里是串行的,在 forEach 里才是并行的,并行做网络请求(浏览器做的网络请求)。setTimeOut 是浏览器的接口。
async function runPromiseByQueue(myPromises) {
for (let i = 0; i < myPromises.length; i++) {
await myPromises[i]();
}
}
const createPromise = (time, id) => () =>
new Promise((resolve) =>
setTimeout(() => {
console.log("promise", id);
resolve();
}, time)
);
runPromiseByQueue([
createPromise(3000, 4),
createPromise(2000, 2),
createPromise(1000, 1)
]);
// promise 4
// promise 2
// promise 1
async function runPromiseByQueue(myPromises) {
myPromises.forEach(async (task) => {
await task();
});
}
const createPromise = (time, id) => () =>
new Promise((resolve) =>
setTimeout(() => {
console.log("promise", id);
resolve();
}, time)
);
runPromiseByQueue([
createPromise(3000, 4),
createPromise(2000, 2),
createPromise(1000, 1)
]);
// promise 1
// promise 2
// promise 4
问答
表达式前面的
,
不影响任何。
var a = 1
var b = (console.log(a), a) + 2
console.log('b' + b)
将同步的代码放到异步代码的前面,因为 await 和 Promise 后的代码会有传染性(同步变异步)。
题外话
一般情况下还是用Promise来写,在 ajax 错误处理部分可以是用 await 小技巧。
手写深拷贝
JSON 序列化
最简易的方法:JSON 序列化反序列化
let a = {
b: 1,
c: [1, 2, 3],
d: {d1: 'ddd1', d2: 'ddd2'}
}
let a2 = JSON.parse(JSON.stringify(a))
a2.b = 2
console.log(a.b)
a2.c[1] = 2222
console.log(a.c[1])
a2.d.d2 = 'ccccc'
console.log(a.d.d2)
缺点:对 JavaScript 来说,
JSON 不支持函数(直接忽略了)
let a = {
b: 1,
c: function (){return 1}
}
let a2 = JSON.parse(JSON.stringify(a))
console.log(a2)
不支持 undefined(和函数一样直接忽略了)
不支持正则(和函数一样直接忽略了)
不支持引用,JSON 不支持环装引用,只支持树状引用。
let a = {
b: 1,
c: function (){return 1}
}
a.self = a
let a2 = JSON.parse(JSON.stringify(a))
console.log(a2)
// TypeError: Converting circular structure to JSON
对 Date 支持不好,会把 Date 对象转为 String
let a = {
b: 1,
c: new Date()
}
let a2 = JSON.parse(JSON.stringify(a))
console.log(a2)
// { b: 1, c: '2021-03-29T15:54:34.691Z' }
递归深克隆
用最少的代码,来完成测试用例。
var a = Symbol()
var b = a
a === b // true
环检测(遇 BUG )
因为递归的对象是没有形成环的,所有会死循环。
解决 BUG,考虑爆栈。
函数调用栈会爆栈,如果对象很深。解决方式就是:把递归代码改为循环代码,存入数组中。也就是把纵向的,改为横向的。
拷贝 RegExp 和 Date
正则有 source 和 flags。
总结
存在的问题就是,每次复制一次之后,cache
变量没有清空。可以通过用面向对象来每次实例化,也就是把 cache
放到 constructor
来解决。
手写bind
bind
nodejs 是没有模块化的。
支持 new 的 bind
JavaScript 的 new 其实就是一个语法糖:
var fn = function(a){
this.a = a
}
new fn(a)
// 等价于上面的 fn,相当于JavaScript 帮你做这几步骤。
// var fn = function (a){
// 1. var temp = {}
// 2. temp.__proto__ = fn.prototype
// 3. fn.call(fn, a)
// 4. return this
// }
当指定 this,没有传。则 this 指向全局:
function fn(){
console.log(this)
}
fn.call(undefined)
// Window {0: global, window: Window, self: Window, document: document, name: "", location: Location, …}
通过 new 4步过程中的第二步: 临时变量.__proto__ === Fn.prototype
来鉴别是否用了 new:
var fn = function(){
console.log(this)
console.log(this.__proto__ === fn.prototype)
}
fn()
// Window {0: global, window: Window, self: Window, document: document, name: "", location: Location, …}
// false
fn.call({name: 'Jonathan Ben'})
// {name: "Jonathan Ben"}
// false
new fn()
// fn {}
// true
__proto__
是几个浏览器自己定义的,因为很火,导致后面官方不得不加到标准,但是官方内心还是坚持不推荐使用的。
this.__proto__ === Fn.prototype
最好的写法是:this instanceof Fn
和 Fn.prototype.isPrototypeOf(this)
(后者用的少)。
用 this.constructor === Fn
可能会间接继承。@@ 试验一下 @@
mdn 上的可以 new 的 bind 存在的问题:多了一层原型。
var ArrayPrototypeSlice = Array.prototype.slice;
// 将 mdn bind 改为 bind2,防止与原生 bind 冲突
Function.prototype.bind2 = function(otherThis) {
if (typeof this !== 'function') {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
}
var baseArgs= ArrayPrototypeSlice.call(arguments, 1),
baseArgsLength = baseArgs.length,
fToBind = this,
fNOP = function() {},
fBound = function() {
baseArgs.length = baseArgsLength; // reset to default base arguments
baseArgs.push.apply(baseArgs, arguments);
return fToBind.apply(
fNOP.prototype.isPrototypeOf(this) ? this : otherThis, baseArgs
);
};
if (this.prototype) {
// Function.prototype doesn't have a prototype property
fNOP.prototype = this.prototype;
}
fBound.prototype = new fNOP();
return fBound;
};
let fn = function (a){
this.a = a
}
fn.prototype.sayHi = function(){}
let o1 = new fn('fn1')
console.log(fn.prototype.isPrototypeOf(o1)) // true
let fn2 = fn.bind(undefined, 'fn2')
let o2 = new fn2()
console.log(o2 instanceof fn) // true
let fn3 = fn.bind2(undefined, 'fn3')
let o3 = new fn3()
console.log(o3.__proto__ === fn.prototype) // true
console.log(o3.__proto__.__proto__ === fn.prototype) // false
通过打印出对象看也是多了一层原型,和 JavaScript 的原生 bind 出现差别。
TDD (Test-Driven development,测试驱动开发)
三个版本:
- es6 语法版本
- 兼容版本(不能 new)
- 兼容版本(可以new)
函数全解上
函数与闭包
JavaScript 中,只有函数和方法,没有过程。
数学函数和编程的函数?
JavaScript 是默认支持闭包的。有些语言并不默认支持,比如 Ruby,需要吧 def
关键字改为 lambda
。
闭包只能位置变量这种状态,变量的值可以变化的。
对象和闭包都维持了变量的状态。
var obj = {
_i:0,
fn(){
console.log(this._i)
}
}
const handle = function (){
var i = 0
return function (){
console.log(i)
}
}
如果不支持对象,用闭包来代替。
function createPerson(name, age) {
return function (key) {
if (key === 'name') return name
if (key === 'age') return age
}
}
var person = createPerson('Jonathan ben', 25)
console.log(person('name'))
console.log(person('age'))
this 超级变态题
4 中申明函数的方式如图。
方方,自编题:
this,关注调用时候的传递的参数(call等)。
如果是点击的触发的话,那么一定就是 button
如果是
var fn = button.onclick()
fn()
// 等价于 fn.call(undefined)
那么 this 就是 window。
一般来说 this 只的是 vm 对象。但是如果特意指定的话,就说不定了。
变态面试题:
let length = 10
function fn(){
console.log(this.length)
}
let obj = {
length: 5,
method(fn){
fn()
// fn.call(undefined)
arguments[0]()
// arguments.0.call(arguments)
// fn.call(arguments)
}
}
obj.method(fn, 1)
// 3
// 2
考点一:let
不会绑定在 window
上
考点二:window.length
在浏览器指的是 <iframe></iframe>
的个数
考点三:method
的隐式对象确实是 obj
考点四:arguments
是实参。确定形参(函数定义时的参数)的是 fn.length
属性
自由变量:不是自己本身的变量。
递归、记忆化与 React 优化
console.time ('看时间')
// 代码
console.timeEnd('看时间')
这两个方法用于计时,可以算出一个操作所花费的准确时间。
迭代基本就是尾递归(迭代递归)。
JavaScript 是没有尾递归优化的,因此还是压栈(理论是是不用压栈的)。
所有的递归都可以变为循环
在 JavaScript 中,递归有时候很差,性能差可读性低。因此解决办法之一是:记忆化(Memorize)
代码执行了,可能 DOM 也没更新。因此 React 中用了 memo ,来记忆代码执行过了。
测试题
const memo = (fn) => {
let hashMap = new Map()
return function (key){
if(!hashMap.get(key)) {
hashMap.set(key, fn(key))
}
return hashMap.get(key)
}
}
const x2 = memo((x) => {
console.log('执行了一次')
return x * 2
})
// 第一次调用 x2(1)
console.log(x2(1)) // 打印出执行了,并且返回2
// 第二次调用 x2(1)
console.log(x2(1)) // 不打印执行,并且返回上次的结果2
// 第三次调用 x2(1)
console.log(x2(1)) // 不打印执行,并且返回上次的结果2
函数全解下
柯里化 Currying
函数式
JavaScript 中,柯里化里面一般都是用闭包多余对象。
== 不要用外面传的参数 ==
第一:形参 params 数组,递归是传的地址进去的,因此是同一个地址的数组。
第二:因为每次需要 push。第一次 newAddTwo(1)
,已经通过 push 改变了 params 里面有一个 1
了。然后第二次调用 newAddTwo(1)(2)
其实在第一个(1)
时候,就满足 params.length === fn.length
因此直接返回了结果 2。在通过 2(2)
显然会报不是函数的错误。
正常版本:
const addTwo = (a, b) => a + b
const currying = (fn, params = []) =>
(arg) => {
const newParams = params.concat(arg)
return newParams.length === fn.length
? fn(...newParams)
: currying(fn, newParams)
}
const newAddTwo = currying(addTwo)
console.log(newAddTwo(1)(5))
优化版:
const addTwo = (a, b) => a + b
const currying = (fn, params = []) =>
(...args) => {
return params.length + args.length === fn.length
? fn(...params, ...args)
: currying(fn, [...params, ...args])
}
const newAddTwo = currying(addTwo)
console.log(newAddTwo(1)(5))
console.log(newAddTwo(1, 3))
三元运算符用 return:需要将return放在三元运算符最前面。不是放在后面 return 两次。
let fn = () => {
return 1 ? 22 : 33
}
fn()
高阶函数
var bind = Function.prototype.bind
var f1 = function (){
console.log('this')
console.log(this)
console.log('arguments')
console.log(arguments)
console.log('-------')
}
var newF1 = f1.bind({name: 'Jonathan Ben'}, 1, 2, 3)
var newF2 = bind.call(f1,{name: 'Jonathan Ben'}, 1, 2, 3)
newF1()
newF2()
// 假定
// obj.method(a, b, c, d)
// obj.method.call(obj, a, b, c, d)
// 设 obj = f1
// 设 method = bind
// 代入
// f1.bind(a, b, c, d)
// f1.bind.call(f1, a, b, c, d)
// 代入参数
// f1.bind({name: 'Jonathan Ben'}, 1, 2, 3)
// f1.bind.call(f1, {name: 'Jonathan Ben'}, 1, 2, 3)
// f1.bind === Function.prototype.bind
// var bind = Function.prototype.bind
// 所以 f1.bind 就是 bind
// bind.call(f1, {name: 'Jonathan Ben'}, 1, 2, 3)
// bind.call 接收一个函数 fn, this, 其他参数
// 返回一个新的函数,会调用 fn,并传入 this 和其他参数
var apply = Function.prototype.apply
var f1 = function (){
console.log('this')
console.log(this)
console.log('arguments')
console.log(arguments)
console.log('-------')
}
f1.apply({name: 'Jonathan Ben'}, [1, 2, 3])
apply.call(f1,{name: 'Jonathan Ben'}, [1, 2, 3])
// 假定
// obj.method(a, b, c, d)
// obj.method.call(obj, a, b, c, d)
// 设 obj = f1
// 设 method = apply
// 代入
// f1.apply(a, b, c, d)
// f1.apply.call(f1, a, b, c, d)
// 代入参数
// f1.apply({name: 'Jonathan Ben'}, [1, 2, 3]) // 理解这个。。
// f1.apply.call(f1, {name: 'Jonathan Ben'}, [1, 2, 3])
// f1.apply === Function.prototype.apply
// var apply = Function.prototype.apply
// 所以 f1.apply 就是 apply
// apply.call(f1, {name: 'Jonathan Ben'}, [1, 2, 3])
// apply.call 接收一个函数 fn, this, 数组
// 返回一个新的函数,会调用 fn,并传入 this 和数组
var call = Function.prototype.call
var f1 = function (){
console.log('this')
console.log(this)
console.log('arguments')
console.log(arguments)
console.log('-------')
}
f1.call({name: 'Jonathan Ben'}, 1, 2, 3)
call.call(f1,{name: 'Jonathan Ben'}, 1, 2, 3)
// 假定
// obj.method(a, b, c, d)
// obj.method.call(obj, a, b, c, d)
// 设 obj = f1
// 设 method = call
// 代入
// f1.call(a, b, c, d)
// f1.call.call(f1, a, b, c, d)
// 代入参数
// f1.call({name: 'Jonathan Ben'}, 1, 2, 3) // 理解这个。。
// f1.call.call(f1, {name: 'Jonathan Ben'}, 1, 2, 3)
// f1.call === Function.prototype.call
// var call = Function.prototype.call
// 所以 f1.call 就是 call
// call.call(f1, {name: 'Jonathan Ben'}, 1, 2, 3)
// call.call 接收一个函数 fn, this, 其他参数
// 返回一个新的函数,会调用 fn,并传入 this 和其他参数
bind.call 接收一个 函数,然后也返回一个函数。apply.call 接收一个函数。call.call 接收一个函数。因此都是高阶函数。
var array = [1, 5, 2, 3, 4]
var sort = Array.prototype.sort
console.log(array.sort((a, b) => a - b))
console.log(sort.call(array, (a, b) => a - b))
var array = [1, 5, 2, 3, 4]
var map = Array.prototype.map
console.log(array.map(item => item * 2))
console.log(map.call(array, item => item * 2))
函数组合 JavaScript 程序员用的比较少。
使用 pipe 来进行函数组合
单参数在高级的函数值是很重要的。
继承和组合
类知识简介
类和继承都是为了解决代码重复。
直接谷歌 tsconfig.json
复制代码。
需要定义类自己的方法:
class Person{
// 类自己的方法需要用 =
mySayHi = () => {}
}
继承与组合
JavaScript 和 Java 中的继承是单继承。C++中的继承是多继承。
因此,组合的最大缺点就是太灵活了。
组合的思想:你要什么东西,我就复制给你。
for… in 遍历不到 class 的方法。
组合是优于继承的。
复习一下组合
Vue 的源码中使用了组合
组合更占内存吗
面向对象虽然省了函数的内存(共有函数),但是原型链创建的对象省不了。
自由组合,虽然函数多了内存,但是都是复制过来的,因此原型链这部分的对象省了些。
因此,两种方式对内存的开销应该是差不多的。