前端面试题-JS进阶

news2024/12/23 0:23:15

1 内置类型

  • JS 中分为七种内置类型,七种内置类型⼜分为两⼤类型:基本类型和对象( Object )。
  • 基本类型有六种: null , undefined , boolea n, 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

2 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
// 我们也可以这样判断 undefined
a === undefined
// 但是 undefined 不是保留字,能够在低版本浏览器被赋值
let undefined = 1
// 这样判断就会出错
// 所以可以⽤下⾯的⽅式来判断,并且代码量更少
// 因为 void 后⾯随便跟上⼀个组成表达式
// 返回就是 undefined
a === void 0

3 类型转换

转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

== 操作符

这⾥来解析⼀道题⽬ [] == ![] // -> true ,下⾯是这个表达式为何为true 的步骤

// [] 转成 true,然后取反变成 false
[] == false
// 根据第 8 条得出
[] == ToNumber(false)
[] == 0
// 根据第 10 条得出
ToPrimitive([]) == 0
// [].toString() -> ''
'' == 0
// 根据第 6 条得出
0 == 0 // -> true

⽐较运算符

  • 如果是对象,就通过 toPrimitive 转换对象
  • 如果是字符串,就通过 unicode 字符索引来⽐较

4 原型

在这里插入图片描述

  • 每个函数都有 prototype 属性,除了 Function.prototype.bind() ,该属性指向原型。
  • 每个对象都有 proto 属性,指向了创建该对象的构造函数的原型。其实这个属性指向了 [[prototype]] ,但是 [[prototype]] 是内部属性,我们并不能访问到,所以使⽤ proto 来访问。
  • 对象可以通过 proto 来寻找不属于该对象的属性, proto 将对象连接起来组成了原型链

5 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
}

6 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__
}
}

7 this

function foo() {
console.log(this.a)
}
var a = 1
foo()
var obj = {
a: 2,
foo: foo
}
obj.foo()
// 以上两者情况 `this` 只依赖于调⽤函数前的对象,优先级是第⼆个情况⼤于第⼀个情况
// 以下情况是优先级最⾼的,`this` 只会绑定在 `c` 上,不会被任何⽅式修改 `this` 指向

var c = new foo()
c.a = 3
console.log(c.a)

// 还有种就是利⽤ call,apply,bind 改变 this,这个优先级仅次于 new
看看箭头函数中的 this
function a() {
return () => {
return () => {
console.log(this)
}
}
}
console.log(a()()())
箭头函数其实是没有 this 的,这个函数中的 this 只取决于他外⾯的第⼀个不是箭头函数的函数的 this 。在这个例⼦中,因为调
⽤ a 符合前⾯代码中的第⼀个情况,所以 this 是 window 。并且 this ⼀旦绑定了上下⽂,就不会被任何代码改变

8 执⾏上下⽂

当执⾏ JS 代码时,会产⽣三种执⾏上下⽂
  • 全局执⾏上下⽂

  • 函数执⾏上下⽂

  • eval 执⾏上下⽂

    每个执⾏上下⽂中都有三个重要的属性
    
  • 变量对象( VO ),包含变量、函数声明和函数的形参,该属性只能在全局上下⽂中访问

  • 作⽤域链( JS 采⽤词法作⽤域,也就是说变量的作⽤域是在定义时就决定了)

  • this

var a = 10
function 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.AO
fooContext.AO {
i: undefined,
b: undefined,
arguments: <>
}
// arguments 是函数独有的对象(箭头函数没有)
// 该对象是⼀个伪数组,有 `length` 属性且可以通过下标访问元素
// 该对象中的 `callee` 属性代表函数本身
// `caller` 属性代表函数的调⽤者
对于作⽤域链,可以把它理解成包含⾃身变量对象和上级变量对象的列表,通过 [[Scope]] 属性查找上级变量
fooContext.[[Scope]] = [
globalContext.VO
]
fooContext.Scope = fooContext.[[Scope]] + fooContext.VO
fooContext.Scope = [
fooContext.VO,
globalContext.VO
]

接下来让我们看⼀个⽼⽣常谈的例⼦, var

b() // call b
console.log(a) // undefined
var a = 'Hello world'
function b() {
console.log('call b')
}
想必以上的输出⼤家肯定都已经明⽩了,这是因为函数和变量提升的原因。通常提升的解释是说将声明的代码移动到了顶部,这其实没
有什么错误,便于⼤家理解。但是更准确的解释应该是:在⽣成执⾏上下⽂时,会有两个阶段。第⼀个阶段是创建的阶段(具体步骤是
创建 VO ), JS 解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存⼊内存中
,变量只声明并且赋值为 undefined ,所以在第⼆个阶段,也就是代码执⾏阶段,我们可以直接提前使⽤。
  • 在提升的过程中,相同的函数会覆盖上⼀个函数,并且函数优先于变量提升
b() // call b second
function 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

9 闭包

闭包的定义很简单:函数 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 定义 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
}
...
}

10 深浅拷⻉

letet a a = {
age : 1
}
let b = a
a.age = 2
console.log(b.age) // 2
  • 从上述例⼦中我们可以发现,如果给⼀个变量赋值⼀个对象,那么两者的值会是同⼀个引⽤,其中⼀⽅改变,另⼀⽅也会相应改变。
  • 通常在开发中我们不希望出现这样的问题,我们可以使⽤浅拷⻉来解决这个问题

浅拷⻉

⾸先可以通过 Object.assign 来解决这个问题
let a = {
age: 1
}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // 1
当然我们也可以通过展开运算符 (…) 来解决
let a = {
age: 1
}
let b = {...a}
a.age = 2
console.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.c
let newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj)
 如果你有这么⼀个循环引⽤对象,你会发现你不能通过该⽅法深拷⻉
  • 在遇到函数或者 undefined 的时候,该对象也不能正常的序列化
let a = {
age: undefined,
jobs: function() {},
name: 'poetries'
}
let b = JSON.parse(JSON.stringify(a))
console.log(b) // {name: "poetries"}
  • 你会发现在上述情况中,该⽅法会忽略掉函数和`undefined。
  • 但是在通常情况下,复杂数据都是可以序列化的,所以这个函数可以解决⼤部分问题,并且该函数是内置函数中处理深拷⻉性能最快的。当然如果你的数据中含有以上三种情况下,可以使⽤ lodash 的深拷⻉函数。

11 模块化

在有 Babel 的情况下,我们可以直接使⽤ ES6 的模块化
// file a.js
export function a() {}
export function b() {}
// file b.js
export default function() {}
import {a, b} from './a.js'
import XXX from './b.js'

CommonJS

 CommonJs 是 Node 独有的规范,浏览器中使⽤就需要⽤到 Browserify解析了。

// a.js
module.exports = {
a: 1
}
// or
exports.a = 1
// b.js
var 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.exports
var 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 提出的
// AMD
define(['./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()
})

12 防抖

你是否在⽇常开发中遇到⼀个问题,在滚动事件中需要做个复杂计算或者实现⼀个按钮的防⼆次点击操作。
  • 这些需求都可以通过函数防抖动来实现。尤其是第⼀个需求,如果在频繁的事件回调中做复杂计算,很有可能导致⻚⾯卡顿,不如将多次计算合并为⼀次计算,只在⼀个精确点做操作
  • PS:防抖和节流的作⽤都是防⽌函数多次调⽤。区别在于,假设⼀个⽤户⼀直触发这个函数,且每次触发函数的间隔⼩于 wait ,防抖的情况下只会调⽤⼀次,⽽节流的 情况会每隔⼀定时间(参数 wait )调⽤函数
// 这个是⽤来获取当前时间戳的
function now() {
return +new Date()
}
/**
* 防抖函数,返回函数连续调⽤时,空闲时间必须⼤于或等于 wait,func 才会执⾏
*
* @param {function} func 回调函数
* @param {number} wait 表示时间窗⼝的间隔
* @param {boolean} immediate 设置为ture时,是否⽴即调⽤函数
* @return {function} 返回客户调⽤函数
*/
function debounce (func, wait = 50, immediate = true) {
let timer, context, args
// 延迟执⾏函数
const later = () => setTimeout(() => {
// 延迟函数执⾏完毕,清空缓存的定时器序号
timer = null
// 延迟执⾏的情况下,函数会在延迟函数中执⾏
// 使⽤到之前缓存的参数和上下⽂
if (!immediate) {
func.apply(context, args)
context = args = null
}
}, wait)
// 这⾥返回的函数是每次实际调⽤的函数
return function(...params) {
// 如果没有创建延迟执⾏函数(later),就创建⼀个
if (!timer) {
timer = later()
// 如果是⽴即执⾏,调⽤函数
// 否则缓存参数和调⽤上下⽂
if (immediate) {
func.apply(this, params)
} else {
context = this
args = params
}
// 如果已有延迟执⾏函数(later),调⽤的时候清除原来的并重新设定⼀个
// 这样做延迟函数会重新计时
} else {
clearTimeout(timer)
timer = later()
}
}
}


  • 对于按钮防点击来说的实现:如果函数是⽴即执⾏的,就⽴即调⽤,如果函数是延迟执⾏的,就缓存上下⽂和参数,放到延迟函数中去执⾏。⼀旦我开始⼀个定时器,只要我定时器还在,你每次点击我都重新计时。⼀旦你点累了,定时器时间到,定时器重置为null ,就可以再次点击了。
  • 对于延时执⾏函数来说的实现:清除定时器 ID ,如果是延迟调⽤就调⽤函数

13 节流

防抖动和节流本质是不⼀样的。防抖动是将多次执⾏变为最后⼀次执⾏,节流是将多次执⾏变成每隔⼀段时间执⾏
/**
* 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;
};
};

14 继承

在 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 底层的这个限制

15 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'])

16 Promise 实现

  • 可以把 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.3
function 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);
}
}

17 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();
}
}
});
}

18 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 value
let 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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/790949.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【数据动态填充到element表格;将带有标签的数据展示为文本格式】

一&#xff1a;数据动态填充到element表格&#xff1b; 二&#xff1a;将带有标签的数据展示为文本格式&#xff1b; 1、 <el-row><el-col :span"24"><el-tabs type"border-card"><el-tab-pane label"返回值"><el-…

计算机流水线在正常程序中的体现(效果可视)

众所周知,流水线技术对于软件开发人员不是可见的(visiable),毕竟已经在在机器语言之下,是组成机器语言的基本逻辑 但今天我就带领大家看看我新发现的结果,那就是流水线的可视效果,包括流水线预测技术的侧面体现,当然也是可见的 首先我先声明一下需要的基础,需要懂16位以及32位操…

leetcode 面试题01.04 回文排列

⭐️ 题目描述 &#x1f31f; leetcode链接&#xff1a;回文排列 思路&#xff1a;回文串两种可能。只有一个字符出现1次其他字符都是偶数次。要么都是偶数次。统计字母的个数即可。 代码&#xff1a; bool canPermutePalindrome(char* s){// 回文串两种可能// 1. 只有一个字…

几百本常用计算机开发语言电子书链接

GitHub - XiangLinPro/IT_book: 本项目收藏这些年来看过或者听过的一些不错的常用的上千本书籍&#xff0c;没准你想找的书就在这里呢&#xff0c;包含了互联网行业大多数书籍和面试经验题目等等。有人工智能系列&#xff08;常用深度学习框架TensorFlow、pytorch、keras。NLP、…

JavaWeb银行项目

主要功能 实现了贷款、存款、理财、提现、充值、开户、绑卡、转账等功能。 介绍 1、这个是一个类似有支付宝一样的web项目。 2、登录和注册&#xff0c;都是通过手机号来进行的。 3、注册的新用户需要先进行开户操作&#xff0c;然后进行绑卡操作。 4、在开户的时候回给你…

计算机专业还会继续火热吗

目录 引言 1.计算机专业火热的原因 2.学好计算机需要的技能 3.计算机未来会持续火热吗 4.博主的建议 引言 今年&#xff0c;张雪峰老师在高考志愿填报领域再次火了一把。他主张专业的实用性&#xff0c;建议家长填报那些未来不愁就业的专业&#xff0c;不好就业的专业不要去…

顺序表操作详解

文章目录 一、线性表二、顺序表1、概念2、接口实现1>初始化顺序表2>操作结束后释放空间3>打印顺序表4>尾插5>头插6>头删7>尾删8>顺序表查找9>顺序表在pos位置插入x10>顺序表删除pos位置的值 一、线性表 线性表&#xff08;linear list&#xff0…

爬取微博热搜榜并进行数据分析

设计方案 爬虫爬取的内容 &#xff1a;爬取微博热搜榜数据。 网络爬虫设计方案概述 用requests库访问页面用get方法获取页面资源&#xff0c;登录页面对页面HTML进行分析&#xff0c;用beautifulsoup库获取并提取自己所需要的信息。再讲数据保存到CSV文件中&#xff0c;进行…

《深度探索c++对象模型》第二章笔记

非原创&#xff0c;在学习 目录 2 构造函数语意学(The Semantics of Constructors) 2.1 Default Constructor的构建操作 “带有Default Constructor”的Member Class Object “带有Default Constructor”的Base Class “带有一个Virtual Function”的Class “带有一个Virtu…

机器学习深度学习——向量求导问题

&#x1f468;‍&#x1f393;作者简介&#xff1a;一位即将上大四&#xff0c;正专攻机器学习的保研er &#x1f30c;上期文章&#xff1a;机器学习&&深度学习——图像分类数据集 &#x1f4da;订阅专栏&#xff1a;机器学习&&深度学习 希望文章对你们有所帮助…

【低代码专题方案】iPaaS运维方案,助力企业集成平台智能化高效运维

01 场景背景 随着IT行业的发展和各家企业IT建设的需要&#xff0c;信息系统移动化、社交化、大数据、系统互联、数据打通等需求不断增多&#xff0c;企业集成平台占据各个企业领域&#xff0c;成为各业务系统数据传输的中枢。 集成平台承接的业务系统越多&#xff0c;集成平台…

详解zookeeper安装使用

目录 1.概述 1.1.功能 1.2.特点 1.3.数据结构 2.安装 2.1.Windows 2.2.Linux 3.基础操作 3.1.增 3.2.删 3.3.改 3.4.查 3.5.监听 4.JAVA操作Zookeeper 4.1.依赖 4.2.客户端 4.3.增 4.4.删 4.5.查 4.6.改 1.概述 1.1.功能 zookeeper&#xff0c;Apache旗下…

pytorch:使用tensorboardX可视化网络模型时add_graph位置报错

1.报错信息 TypeError: graph() got an unexpected keyword argument ‘use_strict_trace’ 提示 graph()这个函数多了一个参数’use_strict_trace’&#xff1b; 也觉得应该是tensorboardX版本问题&#xff0c;但uninstall 再insatall之后也不行&#xff0c;用另一台机子也…

STM32H5开发(3)----电源控制RCC

STM32H5开发----3.电源控制&RCC STM32H503 供电STM32H562/563/573 LDO 供电STM32H562/563/573 SMPS供电LDO/SMPS 供电PWR 特性电源电压监测温度监测低功耗模式低功耗模式-SLEEP 模式低功耗模式-STOP 模式低功耗模式-STANDBY模式低功耗模式监控管脚VBAT模式复位触发源时钟源…

主机漏洞利用演示MS17-010(永恒之蓝)

ms17-010危害&#xff1a;对被攻击方的电脑造成蓝屏&#xff01; 申明&#xff1a;本篇文章的用意仅做学习使用 网络搭建环境&#xff1a; 软件&#xff1a;Vmware Workstation 17 攻击机&#xff1a;Kali 靶机环境&#xff1a;Windows 7 Nmap软件的基本功能&#xff1a; …

渐进式网络恢复调研

渐进式网络恢复调研 问题定义&#xff08;PNR) 如果发生重大网络中断&#xff08;例如由地震、洪水等大规模灾害&#xff09;&#xff0c;运营商必须通过一系列修复步骤来恢复其网络基础设施。优化这个序列以在恢复过程中最大化提供的服务数量的问题通常称为渐进式网络恢复&a…

Phong光照模型原理及着色器实现

现实世界中的照明极其复杂&#xff0c;取决于太多因素&#xff0c;我们无法以有限的处理能力来计算这些因素。 因此&#xff0c;OpenGL 中的光照基于使用简化模型的现实近似值&#xff0c;这些模型更容易处理并且看起来相对相似。 这些照明模型基于我们所理解的光物理学。 其中…

桥接模式-处理多维度变化

程序员小名去摆摊卖奶茶了&#xff0c;口味有香、甜。 型号有大、中、小。假如小名先在家里把这些奶茶装好&#xff0c;那么最少要装2x3 6杯奶茶&#xff0c;如果此时新增一个口味&#xff1a;酸&#xff0c;那么就需要多装3杯奶茶了。而且这样做&#xff0c;等客户买走一种&a…

c++网络编程:Boost.asio源码剖析

1、前言 Boost库是一个可移植、提供源代码的C库&#xff0c;作为标准库的后备&#xff0c;是C标准化进程的开发引擎之一。Boost库由C标准委员会库工作组成员发起&#xff0c;其中有些内容有望成为下一代C标准库内容。在C社区中影响甚大&#xff0c;是不折不扣的“准”标准库。…

HCIA练习4

题目如下&#xff1a; 目录 第一步&#xff1a;IP的规划 第二步&#xff1a;缺省路由 第三步&#xff1a;开启telnet 第四步&#xff1a;编写ACL表 第五步&#xff1a;测试 思路分析&#xff1a; 华为默认允许所有&#xff0c;所以我们可以先写拒绝要求&#xff0c;再写允…