我明白了,玩转前端面试JS篇

简介: 前端面试 无非就是 CSS + JS + 框架 + 工具 + 源码 + 算法 + 职业规划 + 实战,这篇文章以及接下来的文章也是围绕这些内容依次展开。说到JS,它非常的强大,除了在页面中运行js,还有在服务器中运行的node.js,以js的构建工具等等,但是我在这篇文章中并不会去说那些js的扩展,比如多端应用、服务器端框架部分、小程序等等东西,还是说说通用的以及基础的部分吧。首当其冲的是 作用域、闭包、面向对象的this指向以及多种创建方式呀继承方式呀、ES6的语法以及promise和async呀await呀,还有与HTML有关的JS DOM呀,和网络有关的 HTTP、NodeJS呀

前言

前端面试 无非就是 CSS + JS + 框架 + 工具 + 源码 + 算法 + 职业规划 + 实战,这篇文章以及接下来的文章也是围绕这些内容依次展开。

说到JS,它非常的强大,除了在页面中运行js,还有在服务器中运行的node.js,以js的构建工具等等,但是我在这篇文章中并不会去说那些js的扩展,比如多端应用、服务器端框架部分、小程序等等东西,还是说说通用的以及基础的部分吧。

首当其冲的是 作用域、闭包、面向对象的this指向以及多种创建方式呀继承方式呀、ES6的语法以及promise和async呀await呀,还有与HTML有关的JS DOM呀,和网络有关的 HTTP、NodeJS呀,和JS构建工具有关的Webpack呀,还有较为常用的JS超集TypeScript等等。

常见且容易忽视的作用域

作用域就一句话理解:它是变量的可用范围,用于防止不同范围的变量之间互相干扰。

js底层只有全局作用域和函数作用域。es6中的块儿级作用域其实还是函数作用域,那是个自调用的闭包。
全局作用域:非任意函数的外部范围
全局变量:存储到全局作用域中的变量
函数作用域:某个函数的内部范围,也叫做某个函数内的作用范围,也就是在函数的{}中形成的作用域
局部变量:存储到函数作用域中的变量,不光是函数的{}中创建的变量,还有函数的()中声明的变量

全局变量的好处与坏处与局部变量相对。全局变量的好处是可重复使用,局部变量与其相反。全局变量的坏处是容易造成全局污染,局部变量与其相反。

除函数的{}中形成的作用域之外,其它非函数的{}都不是作用域,块级作用域实际上是用匿名函数自调来模拟的。
而且js中是没有块级作用域的,那种看起来像强类型语言中的块级作用域的是需要配合let以及const来同时使用,如果没有它们,是无法拦住内部的变量超出{}的范围的。

作用域链

它是一个抽象的概念,指的是一个查找变量的路径。
一个函数在定义时就会创建好一条作用域链。在闭包中,内层函数会在被变量接收时,会创建好一条作用域链。
当前函数内作用域 --> 当前函数外部作用域 --> 全局作用域。
既然称之为链,自然就会有节点了,节点就像是域,它存储了这个域中包含的数据。
逻辑上就像是一个链表,当你拿到链表的某一个节点时,就能往上或者往下追溯,从而找到其它节点的存储的数据了。
就像浏览器的源码调试栏中的作用域这一栏,里面会有全局 和 本地的作用域,全局 是指 window对象,本地 是指 函数作用域下的 变量。

全局作用域、函数作用域、作用域链它们全都是对象。函数作用域是一个临时的对象,函数调用完毕之后,这个对象就会被回收、释放掉,闭包的使用就可能会使得这些临时对象不会被回收掉,从而容易内存泄露从而引发内存溢出。

注意
在js中给未声明的变量赋值,不会报错,而是自动挂载到window上了,也就是创建了一个全局变量并赋值。
变量提升只会把声明提前,变量提升不会超过当前函数作用域,在严格模式下不声明的赋值会报错。
js中只有两种局部变量,一个是在函数中用var声明的变量,一个是函数()中声明的形参。

函数调用过程
先创建函数作用域对象,然后保存局部变量,之后依次执行函数内的代码逻辑,最后函数执行完毕,正常销毁作用域对象。

function也是对象
js声明的function 等于 new Function,所以函数也是对象。

老说长谈的闭包

闭包虽然可能会导致内存泄露从而引发内存溢出,但它能解决全局变量极易被污染的问题。

全局变量和局部变量的优缺点对立,而闭包能够同时兼备它俩的优点。

闭包是即重用变量又保护变量不被污染的一种编程方式。

三步成闭包

  1. 外层函数包裹内层函数和要保护的变量
  2. 外层函数内部返回内层函数对象
  3. 调用外层函数,用变量接收返回的内层函数对象

原理:外层函数调用后,外层函数的作用域对象 被返回的内层函数的作用域链引用着,无法释放,就形成了闭包对象。
释放闭包:将接收内层函数的变量赋值为null时,该闭包就会被释放。

在闭包中,接收内层函数对象的方式除了常规的那种,还有以下两种:

  1. 接收内层函数也可以在内部接收,内部接收直接赋值给window,比如在外部函数被调用时,在外层函数的内部就把内部函数赋值给了window。
  2. 不光可以在外层函数内部返回内层函数对象,还可以将内层函数对象包裹成对象或者数组,然后再返回。
function mother () {
   var a = 0;
   window.fn = function () { a++; console.log(a)}
}
mother() // 也能形成一个闭包

之所以闭包会导致内存泄漏,是因为函数只有在被调用的时候才会执行函数体中的内容,而函数体中内容被执行的时候会创建作用域对象,当返回内层函数时,外层函数创建的作用域对象并不会销毁掉,于是只要内层函数被引用着,这个作用域对象就一直存在。
那么如果一直执行外层函数,就会不停创建新的作用域对象,然后返回内层函数还一直被引用着,那么内存就会不停的上涨,最终内存溢出。

如果你想查看内层函数有哪些作用域对象,可以通过以下代码查看。

function outerFn () {
    var value = 999;
    return function () {
        console.log(value);
    };
}

const innerFn = outerFn();
console.dir(innerFn); // 查看这个函数对象的[[Scopes]]隐藏属性

解闭包题

管理员创建用户,并把权限分配给用户,用户可以使用分配到的权限。
管理员可以创建多个用户,用户被销毁后,分配给用户的权限也会被回收掉。
解决闭包问题需要找到以下三种东西。

  1. 外层函数 // 管理员
  2. 外层函数的局部变量 // 权限(钥匙)
  3. 内层函数(可能有多个) // 用户

js中的面向对象

面向对象编程便于大量数据的管理维护。

老说长谈的面向对象

封装:根据设计,集中去的保存某类事物的数据以及功能,将零散的数据和功能分门别类的进行适合的整理,从而便于维护和管理。
继承:复用封装好的功能代码,减少不必要的冗余代码,提升代码的质量以及美感。
多态:根据预定接口,对该接口进行不同的功能实现,使用的时候通过切换不同的对象,从而实现不同的功能效果。

封装对象

第一种
使用对象字面量{},虽然它有{},但不会生成作用域,所以如果你在对象字面量中定义属性和函数时,在这个函数里面不能直接通过属性名的方式访问定义的属性。
因为对象字面量中的属性是不会纳入到对象字面量中的函数的作用域链中。
如果要在对象字面量中的函数中访问对象字面量的属性,可以通过 对象字面量名.属性名的方式来使用,不过这种方式过于紧耦合,也可以通过this.属性名的方式来使用。
对象函数的作用域只有当前函数内和全局。

第二种
使用 new Object(),通过对象名.属性名的方式来赋值和使用
Js中所有对象底层都是关联数组。关联数组是指,下标是自定义字符串的数组。Js中没有堆、栈,js的成员几乎都是放在内存的自由存储区域中,在ES6中,也有部分成员放到静态存储区域中,比如

为啥说js对象底层都是关联数组,原因是以下五点

  1. 数据结构类似
  2. 都是通过[] 和 . 来访问属性
  3. 强制赋值,都不会报错
  4. 访问不存在的属性,都不会报错
  5. 都可以用for in 的方式访问。

第三种
通过定义构造函数,然后new 构造函数(),在构造函数中{}中通过this.属性名的方式赋值,在外部通过对象名.属性名的方式来使用。
使用构造函数可以反复创建多个相同结构的对象。
new构造函数创建对象做了4件事:

  1. 创建空对象
  2. 调用构造函数,让新对象继承。还会自动设置新对象的__proto__指向构造函数的原型对象。
  3. 将构造函数中的this指向新对象
  4. 将新对象赋值给外边那个变量(如果构造函数中没有返回有效的数据,则直接返回this,这个this就是新对象,如果返回了有效数据,则直接返回有效数据给外边那个变量)。

继承

js中继承都是通过原型对象来实现的,所有子对象共用的属性、函数就可以放到原型对象中。多个子对象都要使用相同的功能以及属性时,可以使用继承。

原型对象是在定义构造函数的时候,默认创建的一个空对象。构造函数中都会自带一个prototype属性。

通过对象. 的方式来访问成员时,如果在当前对象中找不到,就会在当前对象的.__proto__中去找,一层一层的网上,再找不到就会报xx不存在。

方法或者函数中的this,一般都是谁调用,这个this就指向谁。

例如:

  1. obj.fn() this指向obj。
  2. new Fun() this指向new创建的新对象。
  3. 原型对象中共同方法里的this会指向将来调用这个共同方法的那个子对象,例如 1 中this指向obj。
  4. use strict 中fn()、匿名函数自调、回调函数中的this指向undefined,这三种种情况在非严格模式下时this指向window。
  5. dom事件绑定函数(绑定的函数非箭头函数)时,this指向当前触发这个事件的dom对象。
  6. 箭头函数中的this指向该函数最近的那个作用域的this。

内置类型
每一个类型都有构造函数和原型对象
内置类型有11种,ES6加了一种Symbol(唯一字符串)
String, Number, Boolean,Array, Date, RegExp, Math(已经是一个对象了),Error,Function,Object,global(全局作用域对象,在浏览器中是window对象)

原型链
自己的__proto__指向构造函数的prototype,一层一层指向Object.prototype。
现在谷歌浏览器都会将__proto__ 显示成[[Prototype]],但是并不能使用,双中括号属于内部属性。

多态

重载:在后端中常见,允许存在相同函数名但参数签名不同的多个函数存在,也就是根据你传入的参数自动去寻找对应的某个函数,在前端中不行。
重写:继承父对象的成员后,觉得不好用或者不想用,就可以在子对象中重写同名的成员,可以是属性也可以是方法。

就像是数组的toString 和 对象的toString,它们都实现了toString的功能接口,数组的toString就是重写了对象的toString。还有Date的toString也是重写了对象的toString方法。
如果你自定义的构造函数也想重写对象的toString方法,可以通过构造函数.prototype.toString=函数的方式来重写。

箭头函数

箭头函数就是使用bind实现的。
使用时注意,被bind永久绑定的this,是不能被call再次替换。
替换this的三种情况:

  1. 临时替换一次this,使用call
  2. 临时替换一次 this, 使用apply
  3. 永久替换 this,使用 bind

创建对象的多种方式

js中创建对象很灵活,有印象了解原理即可。

1.new Object,缺点是初始化成员的步骤过多。

2.对象字面量创建,虽然很方便,但缺点是创建多个字面量对象时,代码会很冗余。

3.工厂函数的方式,是对new Object的进一步的业务封装。
例如:function createObj(){var o = new Object(); return o;}; var obj = createObj();
缺点是没法根据对象的原型对象来判断对象的类型。

4.构造函数的方式,虽然可以复用代码创建多个对象,但是缺点是如果在构造函数中有成员是fn,就会导致重复创建fn,就会浪费内存。
function P1(tag) { this.tag = tag; this.tip=function(){alert(this.tag)};

5.原型对象的方式,定义空的构造函数,然后给构造函数的原型对象初始化成员,这样一来和new Object的缺点类似了,初始化成员步骤过多。
function P1(tag) {}; P1.prototype.tag = tag; P1.prototype.tip=function(){alert(this.tag)

6.混合模式,就是4和5的方式结合起来,可以设计的很好,也可能很糟糕,但这么做是为了规避它们各自的缺点,但还是会有缺点,是不符合面向对象封装的思想。
它不是集中去的保存某类事物的数据以及功能,有点零散。
function P1(tag) { this.tag = tag;}; P1.prototype.tip=function(){alert(this.tag)};

7.动态混合,算是4和5的方式结合起来,同时也为了弥补6中缺点。
function P1(tag) { this.tag = tag; if([undefined].include(P1.prototype.tip) {P1.prototype.tip=function(){alert(this.tag)}}
这样就不零散了,只是通过构造函数创建对象时,除了第一次的if有意义,其它时候的if就多余了。

8.寄生构造函数,构造函数A中借用构造函数B来创建对象b,然后定制化的添加对象b的成员,最后返回对象b。
缺点是在复杂的情况下会导致代码可读性差。

9.ES6的Class,这种方式比较传统,后端很多年前就有了。其实它就是4和5结合起来呀,只不过是用了class {}包裹4和5,然后换成后端传统的那种写法。
class P1{ constructor(tag) {this.tag=tag;} tip() {alert(this.tag)}}

10.闭包构造函数,它不用new和this了,它将2和4的方式与闭包结合起来了,非常的方便,但闭包容易内存泄露从而引发内存溢出。
function P1(tag) {var p1= {};p.tag=tag;p1.top=function {alert(this.tag)}; return p1}; var obj = P1('hello world')

继承的多种方式

js中的继承也很灵活,有印象了解原理即可。

1.原型链继承,通过原型链的方式实现继承,function P1(tag) {this.tag=tag}; function P2(){};P2.prototype=new P1('hello');var p = new P2()

2.构造函数继承,function P1(tag) {this.tag=tag}; function P2(){P1.Call(this, 'hello');} var p = new P2();

3.实例继承,构造函数A中返回构造函数B创建的对象b,同时可以在构造函数A中初始化其成员。
function B() {};function A(tag){var b=new B(); b.tag=tag; return b;}; var a = new A('hello');

4.拷贝继承,分为深拷贝和浅拷贝,浅拷贝只拷贝父类对象中的属性,深拷贝会递归的拷贝父类对象及以祖类对象中的属性。
function P1(tag) {this.tag=tag}; function P2(){var p1=new P1();for(var keyName in p1 ){ P2.prototype[keyName]=p1[keyName]}}; var p = new P2();

5.组合继承,是1和2结合起来,一部分通过原型继承,一部分通过构造函数继承。

function A(tag){this.tag=tag};
A.prototype.tip=function(){alert(this.tag)};
function B(){A.call(this, 'hello')};
B.prototype = new A();
B.prototype.constructor = B;
var b = new B();

6.寄生组合继承,是将1和2结合闭包来使用,除了像5之外,它会在闭包中对父子继承之前通过一个新的构造函数来借用父类构造函数的prototype。
闭包自调的时候完成继承,借用了父类构造函数prototype。

function A(tag){this.tag=tag};
A.prototype.tip=function(){alert(this.tag)};
function B(){A.call(this, 'hello')};
(function(){
    var C = function (){};
    C.prototype=A.prototype;
    B.prototype=new C();
})()
var b = new B();

7.ES6的Class extends继承,这种方式比较传统,原理上是5中的组合继承。
class A { constructor(tag) {this.tag=tag;}};class B extends A {constructor() {super('hello')}}

对象克隆

对象克隆一般分为浅克隆和深克隆。
浅克隆就是创建一个空对象,然后把另外一个要被克隆的对象遍历一遍,最后对空对象进行key/value的赋值即可。
浅克隆还可以使用Object.assign(target,source)来进行,它的原理也是简单遍历一遍被克隆的对象source然后赋值给target。
深克隆就是需要递归的遍历被克隆的对象,然后进行key/value的赋值,但是需要有特性的判断,比例如你遍历到数组、正则、null甚至一些特殊的对象,甚至还有边缘的处理等等。
最简单的深克隆是JSON.stringify()、JSON.parse(),但无法克隆函数以及值为undefined的属性。
记得似乎chrome 97 以及 node17 之后也支持了一个深克隆的api structuredClone,直接调用这个api即可,它是深克隆的。

ES6

如果系统的学习ES6可以去看阮一峰老师的网站,我说的其实都是通过我学习之后理解呀总结呀觉得很常用的内容。

简单知识点

模板字符串

${}
它里面能放 变量、算术计算表单式、三目表达式、对象属性、创建对象、调用函数、数组元素、有返回值的合法的任意js表达式。
不能放没有返回值的js表单式,什么if else for while 都不行。

let

var 中的问题,声明提前(变量提升),var 会打乱程序执行的顺序。
let 很好的解决了这个问题,不会被声明提前,保证程序顺序执行。同时也让let所在的程序块形成了一个"块级作用域"(匿名的函数作用域),从这个块儿内的变量不会去影响块儿外的变量。

let 的本质:底层相当于匿名函数自调噢,也就是用匿名的函数作用域了。所以它不能像var声明变量一样可以在window上通过window.变量名的方式访问或修改。

let 声明的变量转成匿名函数自调时是这样的,同时它不支持重复声明的。

let a = 2;
let b = 3;

// 转换之后是这样的
(function(a, b){})(2, 3)

let 和 const原理上一样,只是const比let多两点限制。
const 声明的变量必须赋初始值,并且不能直接修改这个变量的值(简单值)及引用(数组引用、对象引用)噢,但可以修改、删除、新增引用里的属性值。

再讲箭头函数

箭头函数就是使用bind实现的,永久绑定当前最近的那个作用域的this,并且是不能被call再次替换。

普通函数是有arguments,但箭头函数是没有arguments的。

最好不要在构造函数、对象方法、原型对象方法、DOM事件处理函数 这些里面使用箭头函数,会影响this的走向。
箭头函数不支持new 同时 也没有 prototype属性。

for of

遍历的操作很常见,比如 for 索引的方式遍历、forEeach api(数组原型、数组、伪数组)、for in、for of。

普通的for,过于原始,索引一般的是数字类型,可以通过封装 类forEach的方法来简化。
forEach api,这是基于普通的for进行封装的,经常配合箭头函数来使用,不能遍历对象。
如果是伪数组可以用Array.prototype.slice.call({0: 'a', 1: 'b', length: 2}, 0)来转换为数组,从而能够使用forEach。
for in,可以用于遍历普通对象,也就是自定义下标的对象。遍历的时候可以拿到自定义下标。然后通过对象名[下标]的方式访问。
for of,不能遍历普通对象,但是可以让它实现一个迭代器接口来支持被遍历,可以看我写的这篇文章我的代码实现之前端迭代器
它只能从头遍历到尾,同时只会取value,不能直接拿到下标。

一般来说,遍历普通对象,用for in,如果是非普通对象,可以用for of,有时候forEach 和 for 索引的方式用的也很方便,看业务需求和个人风格吧。

参数与展开运算符

参数默认值、参数聚合(剩余参数)、参数展开。

参数默认值就是给函数的形参赋默认值

箭头函数中没有arguments,如果想实现arguments一样的效果可以这样(...list) => {},这个list就是像argument一样简单的把参数聚合到一起了,
并且它是把参数都放到一个数组中,而不是像arguments那样是伪数组。
除了全部参数聚合,还可以局部参数聚合,比如这样(name, age, ...otherList) => {},这个otherList就是将前两个参数之外的参数聚合起来了。

通过展开运算符将参数展开
通过...可以将一个对象或者数组拆散开来从而实现浅克隆以及多合并,也可以在函数调用时候通过...展开一个数组中的参数依次赋值进去。比如~~

const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const arr3 = [...arr1, ...arr2, 7, 8, 9]; // 1 2 3 4 5 6 7 8 9

const obj1 = {name: 'm2', info: 'student'};
const obj2 = {sex: '男'};
const obj3 = {...obj1, ...obj2, address: '欧罗巴'};

function say(info1, info2, info3) {
  console.log(`1:${info1}, 2:${info2}, 3:${info3}`);
}
const infoList = ['one', 'two', 'three'];
say(...infoList);

Math.max(...arr3) // 9

通过解构来提效

解构就是按需的拿到一个数据集合中的你需要的那个数据,就像sql语句按需获取部分的数据。

解构有三种方式: 对 数组解构、对 对象解构、对函数的参数解构。

数组解构是通过元素的顺序来解构的。
对象解构是通过对象的keyName来解构的。
参数解构是对入参进行解构,如果入参是数组,就像数组解构一样,如果入参是对象,就像对象解构一样。

// 数组解构
const arr1 = [1, 2, 3];
const [one, two, three] = arr1;
console.log(`1:${one}, 2:${two}, 3:${three}`);

const arr2 = [4, 5, 6];
const [, five, ] = arr1;
console.log(five); // 5

// 对象解构
const obj1 = {name: 'm2', info: 'student', sex: '男'};
const { name, info } = obj1;
console.log(`name:${name}, info:${info}`);

// 参数解构
const arr2 = [4, 5, 6];
function say([info1, info2, info3]) {
  console.log(`1:${info1}, 2:${info2}, 3:${info3}`);
}
say(arr2);

const obj2 = {name: 'm2', info: 'student', sex: '男', address: '欧罗巴'};
function say2({ name, info, sex}) {
  console.log(`1:${info1}, 2:${info2}, 3:${info3}`);
}
say2(obj2)

解构失败不报错,会给变量赋值为undefined。

class 与 它的继承原理

class中的定义的函数会放到原型对象上,但是constractor中赋值的属性和class {}中普通定义的属性都会放到new 之后的实例对象上。

class M2 {
  name='m2'

  constractor () {
    this.age = 20
  }

  say () {
    console.log(`${this.name} ${this.age}`)
  }
}

多个实例对象共用的属性,不再是放到原型对象上,而是作为class 的静态属性来存放,直接通过class名.属性的方式。定义时是通过 static来修饰的。他并没存放到原型对象去,而是直接放到自己身上,所以才能通过类名.xx的方式使用。

class M2 {
  static name='m2'
}

继承 extends 和 调用父类 构造函数 super

继承是通过extends关键字来进行的,如果需要调用父类构造函数来初始化成员,也可以使用super来调。

class Parent {
  constractor (name) {
    this.name = name
  }
}

class M2 extends Parent {
  constractor (name, age) {
    super(name)
    this.age = age
  }

  parint () {
    consolog(`${this.name} ${this.age}岁(狗头)`)
  }
}

const m2 = new M2 ('码二', '1000')
m2.parint(); // 码二 1000岁(狗头)

继承是为了更方便的扩展,但是可能会造成子类职责变重,因为一下子就把父类的功能全弄过来了,有时并不需要那么多,那么可以通过组合或聚合的方式来按需拿自己需要的功能。

面向对象小知识点
先有子类型,再通过子类型抽象出父类型

Promise 以及 它的扩展

由于js单线程的特性,所以就有了异步函数,由于异步函数之间互不干扰,如果想要让一个异步函数的执行结果传给另一个异步函数,那么就需要不停的做函数嵌套。这样的套娃使得代码比较难看也不宜维护。于是Promise横空出世了。
promise 用来解决这种函数嵌套与代码难看的“回调地狱”问题。

使用promise 会优先将要调用的函数进行收集,收集完毕后,再依次执行,然后执行的结果会按需的有序的传递下去。

为了更准确的控制promise收集的这些函数的执行过程,所以它用到了三个状态
pending 任务挂起
fulfilled 执行成功
rejected 执行出错

Promise.resolve().then().catch()

new Promise((resolve, rejected) => {
  setTimeout(() => {
    if (Math.random() > 0.5) {
      resolve()
    } else {
      rejected()
    }
  },2000)
}).then().catch()

async 和 await 简化promise

它们基于promise,是promise的语法糖,它使用generator生成器来实现的。
主要功能是为了消除嵌套,虽然promise将函数的嵌套降到了几乎仅嵌套一层的地步,但是似乎还可以再优化,所以async 和 await 出现了,使得把代码变成了同步的写法。
其实还是收集异步函数,然后依次执行的底层逻辑。

Promise.resolve()
.then(() => setTimeout(function () {console.log('action 1')}, 500))
.then(() => setTimeout(function () {console.log('action 2')}, 3800))
.catch((e) => console.log('action error', e))

// 将promise的写法换成 async await
(async () => {
  try {
    await (()=> setTimeout(function () {console.log('action 1')}, 500))()
    await (()=> setTimeout(function () {console.log('action 2')}, 3800))()
  } catch(e) {
    console.log('action error', e)
  }
})()

Promise 状态机

之所以Promise能够实现这个效果这样的一个状态管理机制。
不过这样的一个状态机制也比较常见。
那么可以简单的实现一下promise,从中体会其中的大概原理。

1.状态机

Promise中保存了state 和 value 以及 then里的回调。

const PENDING = 0; // 挂起的状态
const FULFILLED = 1; // 执行成功的状态
const REJECTED = 2; // 执行失败的状态

// 定义一个Promise函数,以他为构造函数,用来创建promise对象
function Promise () {

    let state = PENDING; // 存储当前promise的状态

    let value = null; // 存储执行成功后的返回值 or 执行失败后的异常信息

    const handlers = []; // 存储promise.then时传入的每一个回调函数(任务)
}

2.参与状态变迁的两个底层函数

这两个底层函数是fulfill和reject


const PENDING = 0; // 挂起的状态
const FULFILLED = 1; // 执行成功的状态
const REJECTED = 2; // 执行失败的状态

// 定义一个Promise函数,以他为构造函数,用来创建promise对象
function Promise () {

    let state = PENDING; // 存储当前promise的状态

    let value = null; // 存储执行成功后的返回值 or 执行失败后的异常信息

    const handlers = []; // 存储promise.then时传入的每一个回调函数(任务)

    // 执行成功后,将状态变更为执行成功的状态,同时将执行结果返回值进行保存,存到value变量中
    function fulfill (result) {
        state = FULFILLED
        value = result
    }

    // 执行失败后,将状态变更为执行失败的状态,同时将执行结果返回值进行保存,存到value变量中
    function reject (error) {
        state = REJECTED
        value = error
    }
}

3.对外开放一个resolve方法

// 定义一个Promise函数,以他为构造函数,用来创建promise对象
function Promise () {

    let state = PENDING; // 存储当前promise的状态

    let value = null; // 存储执行成功后的返回值 or 执行失败后的异常信息

    const handlers = []; // 存储promise.then时传入的每一个回调函数(任务)

    // 执行成功后,将状态变更为执行成功的状态,同时将执行结果返回值进行保存,存到value变量中
    function fulfill (result) {
        state = FULFILLED
        value = result
    }

    // 执行失败后,将状态变更为执行失败的状态,同时将执行结果返回值进行保存,存到value变量中
    function reject (error) {
        state = REJECTED
        value = error
    }

    // 如果当前任务成功执行了,那么使用者就会去调用resolve(返回值)
    function resolve(result) {
        try {
            let then = getThen(result) // 拿到任务返回值中的新任务(回调函数) 或者 新的promise对象,这是因为.then(()=>XX)中的函数执行时,可能会返回一个新函数或者promise对象
            // 如果有拿到新任务或者新任务对象
            if (then) {
                // 执行新的任务函数,并把任务对象绑定到这个函数上,与此同时将resolve 和 reject 作为参数传递给这个新任务函数
                doResolve(then.bind(result), resolve, reject)
                return // 结束
            }

            // 如果没有新的任务,那就直接成功
            fulfill(result)
        } catch (error) {
           // 执行过程中报错了,那就直接失败
           reject(error) 
        }
    }
}

4.实现getThen和doResolve函数

获取then中返回的新promise对象的then,如果有就会执行then

// 定义一个Promise函数,以他为构造函数,用来创建promise对象
function Promise () {

    let state = PENDING; // 存储当前promise的状态

    let value = null; // 存储执行成功后的返回值 or 执行失败后的异常信息

    const handlers = []; // 存储promise.then时传入的每一个回调函数(任务)

    // 执行成功后,将状态变更为执行成功的状态,同时将执行结果返回值进行保存,存到value变量中
    function fulfill (result) {
        state = FULFILLED
        value = result
    }

    // 执行失败后,将状态变更为执行失败的状态,同时将执行结果返回值进行保存,存到value变量中
    function reject (error) {
        state = REJECTED
        value = error
    }

    // 如果当前任务成功执行了,那么使用者就会去调用resolve(返回值)
    function resolve(result) {
        try {
            let then = getThen(result) // 拿到任务返回值中新的promise对象的then(新任务),这是因为之前的.then(()=>XX)中的函数执行时,可能会返回一个新promise对象
            // 如果有拿到新任务
            if (then) {
                // 执行新的任务函数,并把任务对象绑定到这个函数上,与此同时将resolve 和 reject 作为参数传递给这个新任务函数
                doResolve(then.bind(result), resolve, reject)
                return // 结束
            }

            // 如果没有新的任务或者只是返回一个普通的函数,那就让当前的任务直接成功即可。
            fulfill(result)
        } catch (error) {
           // 执行过程中报错了,那就直接失败
           reject(error) 
        }
    }

    // 拿到新的promise对象中的then
    function getThen (value) {
        const t = typeof value
        if (value && ['object', 'function'].includes(t)) {
            // 如果value 是一个promise对象,那就返回这个promise对象的then
            const then = value.then
            if ('function' === typeof then) {
                return then
            }
            // 如果value是一个普通的函数,走下面的null逻辑,因为普通函数不用对状态进行干预了。
        }
        // value 为空,就返回null
        return null;
    }

    // 执行任务函数promise的then函数
    function doResolve (fn, onFulfilled, onRejected) {
        let done = false // 默认状态为暂未执行成功,也是加个锁
        try {
            // 调用任务函数
            fn(
                // 第一个回调
                function (value) {
                    if(done) return; // 异步任务执行完毕,就没必要往下走了。因为任务函数中的两个函数有一个已经执行完了
                    done = true;
                    onFulfilled(value)
                }, 
                // 第二个回调
                function (reason) {
                    if (done) return; // 异步任务执行完毕,就没必要往下走了。因为任务函数中的两个函数有一个已经执行完了
                    done = true;
                    onRejected(reason)
                }
            )
        } catch (error) {
            if(done) return
            done = true
            onRejected(error)
        }
    }
}

5.定义handle函数结合handlers来根据promise对象的状态来进行处理任务

根据promise的运行状态来决定是收集then中的回调,还是直接执行then中的回调(这个回调其实做了一层封装,是一个对象,对象中有两个函数,成功和失败的回调)


// 定义一个Promise函数,以他为构造函数,用来创建promise对象
function Promise () {

    let state = PENDING; // 存储当前promise的状态

    let value = null; // 存储执行成功后的返回值 or 执行失败后的异常信息

    const handlers = []; // 存储promise.then时传入的每一个回调函数(任务)

    // 执行成功后,将状态变更为执行成功的状态,同时将执行结果返回值进行保存,存到value变量中
    function fulfill (result) {
        state = FULFILLED
        value = result
    }

    // 执行失败后,将状态变更为执行失败的状态,同时将执行结果返回值进行保存,存到value变量中
    function reject (error) {
        state = REJECTED
        value = error
    }

    // 如果当前任务成功执行了,那么使用者就会去调用resolve(返回值)
    function resolve(result) {
        try {
            let then = getThen(result) // 拿到任务返回值中新的promise对象的then(新任务),这是因为之前的.then(()=>XX)中的函数执行时,可能会返回一个新promise对象
            // 如果有拿到新任务
            if (then) {
                // 执行新的任务函数,并把任务对象绑定到这个函数上,与此同时将resolve 和 reject 作为参数传递给这个新任务函数
                doResolve(then.bind(result), resolve, reject)
                return // 结束
            }

            // 如果没有新的任务或者只是返回一个普通的函数,那就让当前的任务直接成功即可。
            fulfill(result)
        } catch (error) {
           // 执行过程中报错了,那就直接失败
           reject(error) 
        }
    }

    // 拿到新的promise对象中的then
    function getThen (value) {
        const t = typeof value
        if (value && ['object', 'function'].includes(t)) {
            // 如果value 是一个promise对象,那就返回这个promise对象的then
            const then = value.then
            if ('function' === typeof then) {
                return then
            }
            // 如果value是一个普通的函数,走下面的null逻辑,因为普通函数不用对状态进行干预了。
        }
        // value 为空,就返回null
        return null;
    }

    // 执行任务函数promise的then函数
    function doResolve (fn, onFulfilled, onRejected) {
        let done = false // 默认状态为暂未执行成功,也是加个锁
        try {
            // 调用任务函数
            fn(
                // 第一个回调
                function (value) {
                    if(done) return; // 异步任务执行完毕,就没必要往下走了。因为任务函数中的两个函数有一个已经执行完了
                    done = true;
                    onFulfilled(value)
                }, 
                // 第二个回调
                function (reason) {
                    if (done) return; // 异步任务执行完毕,就没必要往下走了。因为任务函数中的两个函数有一个已经执行完了
                    done = true;
                    onRejected(reason)
                }
            )
        } catch (error) {
            if(done) return
            done = true
            onRejected(error)
        }
    }

    // 向外暴露的then收集的任务都会被二次封装,才会放入handlers或者直接执行掉
    function handle (handler) {
        if (state === PENDING) {
            handlers.push(handler)
            return
        }

        if (state === FULFILLED && 'function' === typeof handler.onFulfilled) {
            handler.onFulfilled(value)
        }

        if (state === REJECTED && 'function' === typeof handler.onRejected) {
            handler.onRejected(value)
        }
    }
}

6.实现done并且向外暴露then

异步的执行handle,其实每一个then都会返回新的promise对象,会根据前一个promise状态来对then进行不同的处理。如果状态未pending,就收集then中的回调,如果状态为 成功或者失败,那么就直接执行then中的回调。

// 定义一个Promise函数,以他为构造函数,用来创建promise对象
function Promise () {

    let state = PENDING; // 存储当前promise的状态

    let value = null; // 存储执行成功后的返回值 or 执行失败后的异常信息

    const handlers = []; // 存储promise.then时传入的每一个回调函数(任务)

    // 执行成功后,将状态变更为执行成功的状态,同时将执行结果返回值进行保存,存到value变量中
    function fulfill (result) {
        state = FULFILLED
        value = result
    }

    // 执行失败后,将状态变更为执行失败的状态,同时将执行结果返回值进行保存,存到value变量中
    function reject (error) {
        state = REJECTED
        value = error
    }

    // 如果当前任务成功执行了,那么使用者就会去调用resolve(返回值)
    function resolve(result) {
        try {
            let then = getThen(result) // 拿到任务返回值中新的promise对象的then(新任务),这是因为之前的.then(()=>XX)中的函数执行时,可能会返回一个新promise对象
            // 如果有拿到新任务
            if (then) {
                // 执行新的任务函数,并把任务对象绑定到这个函数上,与此同时将resolve 和 reject 作为参数传递给这个新任务函数
                doResolve(then.bind(result), resolve, reject)
                return // 结束
            }

            // 如果没有新的任务或者只是返回一个普通的函数,那就让当前的任务直接成功即可。
            fulfill(result)
        } catch (error) {
           // 执行过程中报错了,那就直接失败
           reject(error) 
        }
    }

    // 拿到新的promise对象中的then
    function getThen (value) {
        const t = typeof value
        if (value && ['object', 'function'].includes(t)) {
            // 如果value 是一个promise对象,那就返回这个promise对象的then
            const then = value.then
            if ('function' === typeof then) {
                return then
            }
            // 如果value是一个普通的函数,走下面的null逻辑,因为普通函数不用对状态进行干预了。
        }
        // value 为空,就返回null
        return null;
    }

    // 执行任务函数promise的then函数
    function doResolve (fn, onFulfilled, onRejected) {
        let done = false // 默认状态为暂未执行成功,也是加个锁
        try {
            // 调用任务函数
            fn(
                // 第一个回调
                function (value) {
                    if(done) return; // 异步任务执行完毕,就没必要往下走了。因为任务函数中的两个函数有一个已经执行完了
                    done = true;
                    onFulfilled(value)
                }, 
                // 第二个回调
                function (reason) {
                    if (done) return; // 异步任务执行完毕,就没必要往下走了。因为任务函数中的两个函数有一个已经执行完了
                    done = true;
                    onRejected(reason)
                }
            )
        } catch (error) {
            if(done) return
            done = true
            onRejected(error)
        }
    }

    // 向外暴露的then收集的任务都会被二次封装,才会放入handlers或者直接执行掉
    function handle (handler) {
        if (state === PENDING) {
            handlers.push(handler)
            return
        }

        if (state === FULFILLED && 'function' === typeof handler.onFulfilled) {
            handler.onFulfilled(value)
        }

        if (state === REJECTED && 'function' === typeof handler.onRejected) {
            handler.onRejected(value)
        }
    }

    this.done = function (onFulfilled, onRejected) {
        // 让then中传递过来的内容异步的去执行
        setTimeout(function(){
            handle({
                onFulfilled,
                onRejected
            })
        }, 0)
    }

    // then中的任务会进行二次封装,封装成promise对象,在Promise中会执行上面done
    this.then = function (onFulfilled, onRejected) {
        const self = this;
        return new Promise(function (resolve, reject) {
            return self.done(
                function (result) {
                    if ('function' === typeof onFulfilled) {
                        try {
                            return resolve(onFulfilled(result))
                        } catch (error) {
                            return reject(error)
                        }
                    } else {
                        return resolve(result)
                    }
                },
                function (error) {
                    if ('fcuntion' === typeof onRejected) {
                        try {
                            return resolve(onRejected(error))
                        } catch (error) {
                            return reject(error)
                        }
                    } else {
                        return reject(error)
                    }
                }
            )
        })
    }
}

总结

面试过程中 JS的问题相对于CSS来说会难点,掌握以上的内容,JS基础的应用是没啥问题的。

和HTML DOM 和网络有关的 HTTP、NodeJS呀,和JS构建工具有关的Webpack呀,还有较为常用的JS超集TypeScript等等。
耽搁了耽搁了【狗头】,后面有兴趣有精力了,继续更新。

本篇文章说了JS面试过程中对个人基础(非框架)方面的较常见也最实用的一些内容。

目录
相关文章
|
3天前
|
自然语言处理 JavaScript 前端开发
三个JavaScript面试题
摘要: - 闭包是JavaScript函数能记住词法作用域,即使在外部执行。示例:计数器函数`createCounter()`返回访问`count`的匿名函数,每次调用计数递增。 - 事件循环处理异步操作,通过检查任务队列执行回调。示例:`setTimeout`异步任务在3秒后添加到队列,待执行,输出顺序为同步任务1、2,然后异步任务1。 - 箭头函数是ES6简洁的函数定义方式,如`greet = name => `Hello, ${name}!`。它没有自己的`this`,不适用作构造函数。
24 6
|
8天前
|
JavaScript 前端开发 C++
【Web 前端】JavaScript window.onload 事件和 jQuery ready 函数有何不同?
【5月更文挑战第2天】【Web 前端】JavaScript window.onload 事件和 jQuery ready 函数有何不同?
|
9天前
|
JavaScript 前端开发 开发者
【Web 前端】什么是JS变量提升?
【5月更文挑战第1天】【Web 前端】什么是JS变量提升?
【Web 前端】什么是JS变量提升?
|
10天前
|
缓存 前端开发 JavaScript
【JavaScript 技术专栏】JavaScript 前端路由实现原理
【4月更文挑战第30天】本文探讨了JavaScript前端路由在SPA中的重要性,阐述了其基本原理和实现方式,包括Hash路由和History路由。前端路由通过监听URL变化、匹配规则来动态切换内容,提升用户体验和交互性。同时,文章也提到了面临的SEO和页面缓存挑战,并通过电商应用案例分析实际应用。理解并掌握前端路由能助开发者打造更流畅的单页应用。
|
11天前
|
前端开发 JavaScript 数据安全/隐私保护
前端javascript的DOM对象操作技巧,全场景解析(二)
前端javascript的DOM对象操作技巧,全场景解析(二)
|
11天前
|
移动开发 缓存 JavaScript
前端javascript的DOM对象操作技巧,全场景解析(一)
前端javascript的DOM对象操作技巧,全场景解析(一)
|
11天前
|
缓存 编解码 自然语言处理
前端javascript的BOM对象知识精讲
前端javascript的BOM对象知识精讲
|
11天前
|
JavaScript 前端开发 开发者
【Web 前端】JS模块化有哪些?
【4月更文挑战第22天】【Web 前端】JS模块化有哪些?
|
11天前
|
前端开发 JavaScript
【Web 前端】 js中call、apply、bind有什么区别?
【4月更文挑战第22天】【Web 前端】 js中call、apply、bind有什么区别?
【Web 前端】 js中call、apply、bind有什么区别?
|
9月前
|
Web App开发 前端开发 JavaScript
前端学习笔记202307学习笔记第五十七天-模拟面试笔记react-fiber解决了什么问题
前端学习笔记202307学习笔记第五十七天-模拟面试笔记react-fiber解决了什么问题
98 0