乐趣区

关于面试:面试路上的五道JS思考题

先说说我为什么会写这个吧,从上个月开始就在筹备去魔都的面试,经验了几次失败后,意识到本人基础知识的单薄,于是开始了补习,学习过程中看到了别的笔者写的思考题与一些思路,跟着思路实现题目后感到受益匪浅,在此总结,若有谬误,欢送探讨。


答复题目的过程须要体现出本人的思考,如果能将碎片知识点串联起来,就能防止面试过程中的一问一答,晋升面试官的评估,题目一共五道,包含我的答复,内容如下:

1.JS 分为哪两大类型?都有什么各自的特点?你该如何判断正确的类型?

JS 分为 原始类型 对象类型

原始类型 有 6 种:undefined string symbol boolean number null

其中 number 须要留神 精度失落的问题 比方 0.1+0.2~!==0.3。这是因为 JS 采纳IEEE 754 双精度版本,只有是采纳了 IEEE754 的语言都有这个问题,0.1 在二进制是有限循环的一些数字,而 JS 采纳的规范会裁剪这些数字。可做如下转换:
parseFloat((0.1 + 0.2).toFixed(10)) === 0.3 // true

另一个须要留神的是 null,因为 原始类型 按理说是能够通过 typeof 判断类型的,然而 typeof null 会返回 object
这是因为历史起因流传下来的 bug,JS 最后为了性能思考应用低位存储变量的类型信息,000 结尾代表的是对象,而 null 示意为全 0。

对象类型 和原始类型不同的是,原始类型存储的是值,对象类型存储的是地址。
当创立一个对象类型的变量时,计算机会在内存中开拓一个空间来存值,但咱们须要找到这个空间,这个空间会领有一个地址。
解决对象类型的变量时,须要留神因为传递的是地址,会产生扭转一方其余也都被扭转的状况,咱们能够采纳 深、浅拷贝 的形式来防止。

浅拷贝形式:Object.assign({}, a) 或者 {...a}

  • 浅拷贝解决了第一层的问题,然而当对象类型中的值还有对象类型的话,就须要应用深拷贝了。
  • 深拷贝形式:lodash 的深拷贝函数。(因为深拷贝实现过程简单,须要思考很多边界,倡议应用lodash

typeof能够精确判断原始类型 (除null),然而对于对象来说,除了函数都会显示object,判断一个对象的正确类型,能够应用instanceof,因为外部机制是通过原型链来判断的
然而 instanceof 也不是齐全可信,因为能够通过 Symbol.hasInstance 自定义 instanceof 行为。

所以咱们能够通过 typeof 判断原始类型,通过原型的 constructor 属性判断对象类型,实现如下:

function judge(v){if(typeof v === 'object' || typeof v === 'function'){if(v){return v.constructor.name}else{return 'null'}
    }else{return typeof v}
}
console.log(judge(null),judge(1),judge('nice'),judge(true),judge({a:1}),judge(()=>{})) 
// 输入:null number string boolean Object Function

2. 你了解的原型是什么?

对于新建进去的对象 obj 来说,能够通过 __proto__ 找到一个原型对象,在原型中定义了很多函数让咱们来应用,比方valueOftoString。并且原型对象能够通过 constructor 找到它的构造函数,可用来判断对象的类型。

其实原型链就是多个对象通过 __proto__ 的形式连贯了起来。为什么 obj 能够拜访到 valueOf 函数?就是因为 obj 通过原型链找到了 valueOf

咱们也能够应用原型链实现继承,比方组合继承:

// 组合继承
function Parent(value) {this.val = value}
Parent.prototype.getValue = function() {console.log(this.val)
}
function Child(value) {Parent.call(this, value)
}
Child.prototype = new Parent()
  
const child = new Child(1)
  
child.getValue() // 1
child instanceof Parent // true

在 ES6 中,就不须要再手写原型链了,可间接通过 class 实现继承:

//class 继承
class Parent {constructor(value) {this.val = value}
    getValue() {console.log(this.val)
    }
}
class Child extends Parent {constructor(value) {super(value)
        this.val = value
    }
}
let child = new Child(1)
child.getValue() // 1
child instanceof Parent // true

3.bind、call 和 apply 各自有什么区别?

callapplybind 是函数原型上的办法,作用都是扭转上下文 (this 指向)。

  • call 会在传入的 this 中调用办法,第二个参数到最初一个参数全都用逗号分隔 fn.call(context,1,2,3)
  • apply 会在传入的 this 中调用办法,所有参数都放在一个数组外面传进去 fn.apply(context,[1,2,3])
  • bindcall 传参统一,但它不会立刻执行,它会返回扭转 this 指向之后的办法。

说到 this,就不得不说判断this 的几种规定:

  • 形如:foo()this指向全局对象 (浏览器是windowNode 中是global), 严格模式下是undefined
  • 形如:obj.foo()this指向obj
  • 形如:foo.call(context,param1,param2,...)this指向context
  • 形如:new foo()this指向新创建的对象
new 的执行过程:新生成一个对象 -> 链接到原型 -> 绑定 this-> 返回新对象

以上就是 this 的规定了,然而可能会产生多个规定同时呈现的状况,这时候会依据优先级来决定 this 指向,优先级排序如下:

new > bind/call/apply > obj.foo() > foo()

同时,箭头函数不存在 this,如果其中呈现this 那就是外层的this


4.ES6 中有应用过什么?

1.let/const

var申明的变量存在变量晋升,这会把申明晋升到作用域顶部,并且 var 在全局作用域下申明变量会导致变量挂载在 window 上。
letconst申明的变量只能在以后的块级作用域里拜访,有“暂时性死区”的个性,也就是说申明前不可用。
const个别用来申明常量,须要给初始值,并且不能再次赋值。

2.class

其实在 JS 中并不存在类,class 只是语法糖,实质还是函数。
传统办法是通过构造函数,定义并生成新对象,ES6 提供了更靠近传统语言的写法,引入了 Class 这个概念,作为对象的模板。

构造函数的 prototype 属性,在 ES6 的“类”下面持续存在。事实上,类的所有办法都定义在类的 prototype 属性下面。在类的实例下面调用办法,其实就是调用原型上的办法。

之前都是通过原型去解决的继承的问题的,在 ES6 中,咱们能够应用 class 去实现继承。外围在于应用 extends 表明继承自哪个父类,并且在子类构造函数中必须调用 super 继承父类的属性,这段代码能够看成 Parent.call(this, value)

3.promise

Promise是 ES6 提出的异步解决方案,用链式表白取代了回调函数。
Promise 有三种状态,别离是 pending resolved rejected,一旦从 pending 状态变成为其余状态就永远不能更改状态了。

Promise 实现了链式调用,也就是说每次调用 then 之后返回的都是一个 Promise,并且是一个全新的 Promise,起因也是因为状态不可变。

还有一点,当咱们在结构 Promise 的时候,构造函数外部的代码是立刻执行的,这在判断代码执行程序时须要晓得。

最初,它也是存在一些毛病的,比方无奈进行 Promise,或者能够通过自定义reject 返回的标记位或手动抛出异样的形式达到目标,但究竟不够优雅。

Promise之后在 ES7 中呈现的 async/await 算是异步编程的终极计划。

一个函数如果加上 async,那么该函数就会返回一个 Promise
async 就是将函数返回值应用 Promise.resolve() 包裹,和 then 中解决返回值一样,并且 await 只能配套 async 应用,相比间接应用 Promise 来说,劣势在于解决 then 的调用链,毕竟写一大堆 then 也很恶心,并且也能优雅地解决回调天堂问题。

当然也存在一些毛病,因为 await 将异步代码革新成了同步代码,如果多个异步代码没有依赖性却应用了 await 会导致性能上的升高,不如间接应用 Promise.all 的形式。


5.JS 是如何运行的?

家喻户晓,JS 是单线程运行的。

当你关上一个 Tab 页时,其实就是创立了一个过程,一个过程中能够有多个线程,比方渲染线程、JS 引擎线程、HTTP 申请线程等等。
当你发动一个申请时,其实就是创立了一个线程,当申请完结后,该线程可能就会被销毁。

在 JS 运行的时候可能会阻止 UI 渲染,阐明 JS 引擎线程和渲染线程是互斥的,这其中的起因是因为 JS 能够批改 DOM,如果在 JS 执行的时候 UI 线程还在工作,就可能导致不能平安的渲染 UI。这其实也是一个单线程的益处,得益于 JS 是单线程运行的,能够达到节俭内存,节约上下文切换工夫,没有锁的问题的益处。

JS 是依据执行栈运行的,当开始执行 JS 代码时,首先会执行一个 main 函数,而后执行咱们的代码。依据先进后出的准则,后执行的函数会先弹出执行栈,如下动图所示:

上面说说异步代码的执行:

当遇到异步的代码时,会被挂起并在须要执行的时候退出到 Task(有多种 Task)队列中。
一旦执行栈为空,Event Loop 就会从 Task 队列中拿出须要执行的代码并放入执行栈中执行,所以实质上来说 JS 中的异步还是同步行为。
不同的工作源会被调配到不同的 Task 队列中,工作源能够分为 微工作(microtask)和 宏工作(macrotask),大抵如下图:

浏览器中 Event Loop执行程序如下所示:

首先执行同步代码,这属于宏工作
当执行完所有同步代码后,执行栈为空,查问是否有异步代码须要执行
执行所有微工作
当执行完所有微工作后,如有必要会渲染页面
而后开始下一轮 Event Loop,执行宏工作中的异步代码,也就是 setTimeout 中的回调函数

  • 微工作包含 process.nextTick,promise,MutationObserver
  • 宏工作包含 script,setTimeout,setInterval,setImmediate,I/O,UI rendering

这里很多人会有个误区,认为微工作快于宏工作,其实是谬误的。因为宏工作中包含了 script,浏览器会先执行一个宏工作,接下来有异步代码的话才会先执行微工作。

Node 中的 Event Loop 和浏览器中的是齐全不雷同的货色。这里就不阐明了(我不会)。


我置信这五道题看上去并不难,每人都能答复出个大略,但关键点在于论述的时候须要条理清晰,组织好语言,能力让电话那头的面试官听明确你到底有多棒????,表达能力应该也是面试中考查的一点吧,最初心愿本人能在二月的尾巴拿到心仪的 Offer。
Good Luck ????

退出移动版