面试答题方法论:
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 的公开标准,有中文翻译,但可能不准确。
mocha 是提供 describe、it 和 漂亮输出的库。chai 是提供 assert 的库。
yarn --dev add ts-node mocha
局部安装yarn init -y
或 npm init -y
yarn --dev add chai mocha
yarn --dev @types/chai @types/mocha
test/index.ts
mocha -r ts-node/register test/index.ts
运行代码describe 和 it 的含义,如下图所示。
出现上图错误,是默认在本地环境下运行找不到该 module 。因此要通过 --dev
安装到本地环境下。
安装
yarn --dev add sinon sinon-chai
yarn --dev add @types/sinon @types/sinon-chai
eventhub(也叫发布订阅模式、eventbus 等)
面试解题思路:
运行 TypeScript 代码用 ts-node
注意:数组的 indexOf
对 IE 的兼容性不好,仅支持 Edge。
打注释方法 /**
+ enter
留坑给面试官问,别写的太完美了。
unknown
是一个安全的 any
,只要一旦确定就不能修改了。
申明类型。
EventLoop 是 C++写的,是 Node.js 的概念不是浏览器的。
EventLoop 是循环的一个过程,表示不同状态。
与前端相关的就 3 个(timers、poll 和 check):
setImediate()
只有 Node.js 有。
如果用 await
语法糖,那就将 await
转化为 Promise 的 then
的方式。
Promise 中加入队列看 then
不需要看 resolve
,resolve
是确定用那个函数。
感觉不好
感觉不好
因为,官网的 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 小技巧。
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
因为递归的对象是没有形成环的,所有会死循环。
函数调用栈会爆栈,如果对象很深。解决方式就是:把递归代码改为循环代码,存入数组中。也就是把纵向的,改为横向的。
存在的问题就是,每次复制一次之后,cache
变量没有清空。可以通过用面向对象来每次实例化,也就是把 cache
放到 constructor
来解决。
nodejs 是没有模块化的。
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,测试驱动开发)
三个版本:
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'))
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
在浏览器指的是 的个数
考点三:method
的隐式对象确实是 obj
考点四:arguments
是实参。确定形参(函数定义时的参数)的是 fn.length
属性
自由变量:不是自己本身的变量。
console.time ('看时间')
// 代码
console.timeEnd('看时间')
这两个方法用于计时,可以算出一个操作所花费的准确时间。
迭代基本就是尾递归(迭代递归)。
JavaScript 是没有尾递归优化的,因此还是压栈(理论是是不用压栈的)。
所有的递归都可以变为循环
在 JavaScript 中,递归有时候很差,性能差可读性低。因此解决办法之一是:记忆化(Memorize)
代码执行了,可能 DOM 也没更新。因此 React 中用了 memo ,来记忆代码执行过了。
新的问题 onClick 每次都会重新创建。
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
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))
单参数在高级的函数值是很重要的。
类和继承都是为了解决代码重复。
需要定义类自己的方法:
class Person{
// 类自己的方法需要用 =
mySayHi = () => {}
}
JavaScript 和 Java 中的继承是单继承。C++中的继承是多继承。
因此,组合的最大缺点就是太灵活了。
组合的思想:你要什么东西,我就复制给你。
for… in 遍历不到 class 的方法。
组合是优于继承的。
面向对象虽然省了函数的内存(共有函数),但是原型链创建的对象省不了。
自由组合,虽然函数多了内存,但是都是复制过来的,因此原型链这部分的对象省了些。
因此,两种方式对内存的开销应该是差不多的。