「前端进阶」方方-前端体系课阶段二:前端精进(JavaScript 专精)

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 -ynpm 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 不需要看 resolveresolve 是确定用那个函数。

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 FnFn.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 ,来记忆代码执行过了。

新的问题 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

函数全解下

柯里化 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 的源码中使用了组合

在这里插入图片描述

组合更占内存吗

面向对象虽然省了函数的内存(共有函数),但是原型链创建的对象省不了。

自由组合,虽然函数多了内存,但是都是复制过来的,因此原型链这部分的对象省了些。

因此,两种方式对内存的开销应该是差不多的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值