本篇内容包括: JS 基础1、二,ES6,JS进阶,异步编程 等等,这些知识是我在一本书上看到的,书的名字我也忘了,并不是我本身的原创。我只是整理了一下。并不是我不想加这个书名,我真的忘了。。。
JS 对于每位前端开发都是必备技能,在小册中咱们也会有多个章节去讲述这部分的知识。首先咱们先来熟悉下 JS 的一些常考和容易混乱的基础知识点。html
涉及面试题:原始类型有哪几种?null 是对象嘛?
在 JS 中,存在着 6 种原始值,分别是:前端
首先原始类型存储的都是值,是没有函数能够调用的,好比 undefined.toString()git
此时你确定会有疑问,这不对呀,明明
'1'.toString()
是可使用的。其实在这种状况下,'1'
已经不是原始类型了,而是被强制转换成了 String 类型也就是对象类型,因此能够调用 toString
函数。github
除了会在必要的状况下强转类型之外,原始类型还有一些坑。面试
其中 JS 的 number 类型是浮点类型的,在使用中会遇到某些 Bug,好比 0.1 + 0.2 !== 0.3,可是这一块的内容会在进阶部分讲到。string 类型是不可变的,不管你在 string 类型上调用何种方法,都不会对值有改变。ajax
另外对于 null 来讲,不少人会认为他是个对象类型,其实这是错误的。虽然 typeof null 会输出 object,可是这只是 JS 存在的一个悠久 Bug。在 JS 的最第一版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头表明是对象,然而 null 表示为全零,因此将它错误的判断为 object 。虽然如今的内部类型判断代码已经改变了,可是对于这个 Bug 倒是一直流传下来。算法
涉及面试题:对象类型和原始类型的不一样之处?函数参数是对象会发生什么问题?
在 JS 中,除了原始类型那么其余的都是对象类型了。对象类型和原始类型不一样的是,原始类型存储的是值,对象类型存储的是地址(指针)。当你建立了一个对象类型的时候,计算机会在内存中帮咱们开辟一个空间来存放值,可是咱们须要找到这个空间,这个空间会拥有一个地址(指针)。编程
const a = []复制代码
对于常量 a 来讲,假设内存地址(指针)为 #001,那么在地址 #001 的位置存放了值 [],常量 a存放了地址(指针) #001,再看如下代码数组
const a = []
const b = a
b.push(1)复制代码
当咱们将变量赋值给另一个变量时,复制的是本来变量的地址(指针),也就是说当前变量 b 存放的地址(指针)也是 #001,当咱们进行数据修改的时候,就会修改存放在地址(指针) #001 上的值,也就致使了两个变量的值都发生了改变。promise
接下来咱们来看函数参数是对象的状况
function test(person) { person.age = 26 person = { name: 'yyy', age: 30 } return person}const p1 = { name: 'yck', age: 25 } const p2 = test(p1) console.log(p1) // -> ? console.log(p2) // -> ?复制代码
对于以上代码,你是否能正确的写出结果呢?接下来让我为你解析一番:
因此最后 person 拥有了一个新的地址(指针),也就和 p1 没有任何关系了,致使了最终两个变量的值是不相同的。
涉及面试题:typeof 是否能正确判断类型?instanceof 能正确判断对象的原理是什么?
typeof 对于原始类型来讲,除了 null 均可以显示正确的类型 typeof 1 // 'number' typeof '1' // 'string' typeof undefined // 'undefined' typeof true // 'boolean' typeof Symbol() // 'symbol' typeof 对于对象来讲,除了函数都会显示 object,因此说 typeof 并不能准确判断变量究竟是什么类型 typeof [] // 'object' typeof {} // 'object' typeof console.log // 'function'复制代码
若是咱们想判断一个对象的正确类型,这时候能够考虑使用 instanceof,由于内部机制是经过原型链来判断的,在后面的章节中咱们也会本身去实现一个 instanceof。
const Person = function() {} const p1 = new Person() p1 instanceof Person // true var str = 'hello world' str instanceof String // false var str1 = new String('hello world') str1 instanceof String // true复制代码
对于原始类型来讲,你想直接经过 instanceof 来判断类型是不行的,固然咱们仍是有办法让 instanceof 判断原始类型的
class PrimitiveString { static [Symbol.hasInstance](x) { return typeof x === 'string' } } console.log('hello world' instanceof PrimitiveString) // true复制代码
你可能不知道 Symbol.hasInstance 是什么东西,其实就是一个能让咱们自定义 instanceof 行为的东西,以上代码等同于 typeof 'hello world' === 'string',因此结果天然是 true 了。这其实也侧面反映了一个问题, instanceof 也不是百分之百可信的。
涉及面试题:该知识点常在笔试题中见到,熟悉了转换规则就不害怕此类题目了。
首先咱们要知道,在 JS 中类型转换只有三种状况,分别是:
咱们先来看一个类型转换表格,而后再进入正题
转Boolean
在条件判断时,除了 undefined, null, false, NaN, '', 0, -0,其余全部值都转为 true,包括全部对象。
对象转原始类型
对象在转换类型的时候,会调用内置的 [[ToPrimitive]] 函数,对于该函数来讲,算法逻辑通常来讲以下:
· 若是已是原始类型了,那就不须要转换了
· 调用 x.valueOf(),若是转换为基础类型,就返回转换的值
· 调用 x.toString(),若是转换为基础类型,就返回转换的值
· 若是都没有返回原始类型,就会报错
固然你也能够重写 Symbol.toPrimitive ,该方法在转原始类型时调用优先级最高。
let a = { valueOf() { return 0 }, toString() { return '1' }, [Symbol.toPrimitive]() { return 2 } } 1 + a // => 3复制代码
四则运算符
加法运算符不一样于其余几个运算符,它有如下几个特色:
· 运算中其中一方为字符串,那么就会把另外一方也转换为字符串
· 若是一方不是字符串或者数字,那么会将它转换为数字或者字符串
1 + '1' // '11' true + true // 2 4 + [1,2,3] // "41,2,3"复制代码
若是你对于答案有疑问的话,请看解析:
另外对于加法还须要注意这个表达式 'a' + + 'b'
'a' + + 'b' // -> "aNaN"复制代码
由于 + 'b' 等于 NaN,因此结果为 "aNaN",你可能也会在一些代码中看到过 + '1' 的形式来快速获取 number 类型。
那么对于除了加法的运算符来讲,只要其中一方是数字,那么另外一方就会被转为数字
4 * '3' // 12 4 * [] // 0 4 * [1, 2] // NaN复制代码
比较运算符
1. 若是是对象,就经过 toPrimitive 转换对象
2. 若是是字符串,就经过 unicode 字符索引来比较
let a = { valueOf() { return 0 }, toString() { return '1' } } a > -1 // true 在以上代码中,由于 a 是对象,因此会经过 valueOf 转换为原始类型再比较值。复制代码
涉及面试题:如何正确判断 this?箭头函数的 this 是什么?
this 是不少人会混淆的概念,可是其实它一点都不难,只是网上不少文章把简单的东西说复杂了。在这一小节中,你必定会完全明白 this 这个概念的。
咱们先来看几个函数调用的场景
function foo() { console.log(this.a) } var a = 1 foo() const obj = { a: 2, foo: foo } obj.foo() const c = new foo()复制代码
接下来咱们一个个分析上面几个场景
说完了以上几种状况,其实不少代码中的 this 应该就没什么问题了,下面让咱们看看箭头函数中的 this
function a() { return () => { return () => { console.log(this) } } } console.log(a()()())复制代码
首先箭头函数实际上是没有 this 的,箭头函数中的 this 只取决包裹箭头函数的第一个普通函数的 this。在这个例子中,由于包裹箭头函数的第一个普通函数是 a,因此此时的 this 是 window。另外对箭头函数使用 bind 这类函数是无效的。
最后种状况也就是 bind 这些改变上下文的 API 了,对于这些函数来讲,this 取决于第一个参数,若是第一个参数为空,那么就是 window。
那么说到 bind,不知道你们是否考虑过,若是对一个函数进行屡次 bind,那么上下文会是什么呢?
let a = {} let fn = function () { console.log(this) } fn.bind().bind(a)() // => ?复制代码
若是你认为输出结果是 a,那么你就错了,其实咱们能够把上述代码转换成另外一种形式
//fn.bind().bind(a) 等于 let fn2 = function fn1() { return function() { return fn.apply() }.apply(a) } fn2()复制代码
能够从上述代码中发现,无论咱们给函数 bind 几回,fn 中的 this 永远由第一次 bind 决定,因此结果永远是 window。
let a = { name: 'yck' } function foo() { console.log(this.name) } foo.bind(a)() // => 'yck'复制代码
以上就是 this 的规则了,可是可能会发生多个规则同时出现的状况,这时候不一样的规则之间会根据优先级最高的来决定 this 最终指向哪里。
首先,new 的方式优先级最高,接下来是 bind 这些函数,而后是 obj.foo() 这种调用方式,最后是 foo 这种调用方式,同时,箭头函数的 this 一旦被绑定,就不会再被任何方式所改变。
小结
以上就是咱们 JS 基础知识点的第一部份内容了。这一小节中涉及到的知识点在咱们平常的开发中常常能够看到,而且不少容易出现的坑 也出自于这些知识点,相信认真读完的你必定会在往后的开发中少踩不少坑。若是你们对于这个章节的内容存在疑问,欢迎在评论区与我互动。
在这一章节中咱们继续来了解 JS 的一些常考和容易混乱的基础知识点。
涉及面试题:== 和 === 有什么区别?
对于 == 来讲,若是对比双方的类型不同的话,就会进行类型转换,这也就用到了咱们上一章节讲的内容。
假如咱们须要对比 x 和 y 是否相同,就会进行以下判断流程:
1. 首先会判断二者类型是否相同。相同的话就是比大小了
2. 类型不相同的话,那么就会进行类型转换
3. 会先判断是否在对比 null 和 undefined,是的话就会返回 true
4. 判断二者类型是否为 string 和 number,是的话就会将字符串转换为 number
5. 1 == '1'
6. ↓
7. 1 == 1
8. 判断其中一方是否为 boolean,是的话就会把 boolean 转为 number 再进行判断
9. '1' == true
10. ↓
11. '1' == 1
12. ↓
13. 1 == 1
14. 判断其中一方是否为 object 且另外一方为 string、number 或者 symbol,是的话就会把 object 转为原始类型再进行判断
15. '1' == { name: 'yck' }
16. ↓
17. '1' == '[object Object]'
思考题:看完了上面的步骤,对于 [] == ![] 你是否能正确写出答案呢?
固然了,这个流程图并无将全部的状况都列举出来,我这里只将经常使用到的状况列举了,若是你想了解更多的内容能够参考 标准文档。
对于 === 来讲就简单多了,就是判断二者类型和值是否相同。
涉及面试题:什么是闭包?
闭包的定义其实很简单:函数 A 内部有一个函数 B,函数 B 能够访问到函数 A 中的变量,那么函数 B 就是闭包。
function A() { let a = 1 window.B = function() { console.log(a) } } A() B() // 1复制代码
不少人对于闭包的解释多是函数嵌套了函数,而后返回一个函数。其实这个解释是不完整的,就好比我上面这个例子就能够反驳这个观点。
在 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) }复制代码
在上述代码中,咱们首先使用了当即执行函数将 i 传入函数内部,这个时候值就被固定在了参数 j上面不会改变,当下次执行 timer 这个闭包的时候,就可使用外部函数的变量 j,从而达到目的。
第二种就是使用 setTimeout 的第三个参数,这个参数会被当成 timer 函数的参数传入。
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 a = { age: 1 } let b = a a.age = 2 console.log(b.age) // 2复制代码
浅拷贝
首先能够经过 Object.assign 来解决这个问题,不少人认为这个函数是用来深拷贝的。其实并非,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复制代码
可是该方法也是有局限性的:
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 或者 symbol 的时候,该对象也不能正常的序列化
let a = { age: undefined, sex: Symbol('male'), jobs: function() {}, name: 'yck' } let b = JSON.parse(JSON.stringify(a)) console.log(b) // {name: "yck"}复制代码
你会发如今上述状况中,该方法会忽略掉函数和 undefined 。
可是在一般状况下,复杂数据都是能够序列化的,因此这个函数能够解决大部分问题。
若是你所需拷贝的对象含有内置类型而且不包含函数,可使用 MessageChannel
function structuralClone(obj) { return new Promise(resolve => { const { port1, port2 } = new MessageChannel() port2.onmessage = ev => resolve(ev.data) port1.postMessage(obj) }) } var obj = { a: 1, b: { c: 2 } } obj.b.d = obj.b // 注意该方法是异步的 // 能够处理 undefined 和循环引用对象 const test = async () => { const clone = await structuralClone(obj) console.log(clone) } test()复制代码
固然你可能想本身来实现一个深拷贝,可是其实实现一个深拷贝是很困难的,须要咱们考虑好多种边界状况,好比原型链如何处理、DOM 如何处理等等,因此这里咱们实现的深拷贝只是简易版,而且我其实更推荐使用 lodash 的深拷贝函数。
function deepClone(obj) { function isObject(o) { return (typeof o === 'object' || typeof o === 'function') && o !== null } if (!isObject(obj)) { throw new Error('非对象') } let isArray = Array.isArray(obj) let newObj = isArray ? [...obj] : {...obj } Reflect.ownKeys(newObj).forEach(key=> { newObj[key] = isObject(obj[key]) ?deepClone(obj[key]) : obj[key] }) return newObj } let obj = { a: [1, 2, 3], b: { c: 2, d: 3 } } let newObj = deepClone(obj) newObj.b.c = 1 console.log(obj.b.c) // 2复制代码
涉及面试题:如何理解原型?如何理解原型链?
当咱们建立一个对象时 let obj = { age: 25 },咱们能够发现能使用不少种函数,可是咱们明明没有定义过它们,对于这种状况你是否有过疑惑?
当咱们在浏览器中打印 obj 时你会发现,在 obj 上竟然还有一个 __proto__ 属性,那么看来以前的疑问就和这个属性有关系了。
其实每一个 JS 对象都有 __proto__ 属性,这个属性指向了原型。这个属性在如今来讲已经不推荐直接去使用它了,这只是浏览器在早期为了让咱们访问到内部属性 [[prototype]] 来实现的一个东西。
讲到这里好像仍是没有弄明白什么是原型,接下来让咱们再看看 __proto__ 里面有什么吧。
看到这里你应该明白了,原型也是一个对象,而且这个对象中包含了不少函数,因此咱们能够得出一个结论:对于 obj 来讲,能够经过 __proto__ 找到一个原型对象,在该对象中定义了不少函数让咱们来使用。
在上面的图中咱们还能够发现一个 constructor 属性,也就是构造函数
打开 constructor 属性咱们又能够发现其中还有一个 prototype 属性,而且这个属性对应的值和先前咱们在 __proto__ 中看到的如出一辙。因此咱们又能够得出一个结论:原型的 constructor 属性指向构造函数,构造函数又经过 prototype 属性指回原型,可是并非全部函数都具备这个属性,Function.prototype.bind() 就没有这个属性。
其实原型就是那么简单,接下来咱们再来看一张图,相信这张图能让你完全明白原型和原型链
看完这张图,我再来解释下什么是原型链吧。其实原型链就是多个对象经过 __proto__ 的方式链接了起来。为何 obj 能够访问到 valueOf 函数,就是由于 obj 经过原型链找到了 valueOf 函数。
对于这一小节的知识点,总结起来就是如下几点:
若是你还想深刻学习原型这部分的内容,能够阅读我以前写的文章
小结
以上就是所有的常考和容易混乱的基础知识点了,下一章节咱们将会学习 ES6 部分的知识。若是你们对于这个章节的内容存在疑问,欢迎在评论区与我互动。
本章节咱们未来学习 ES6 部分的内容。
涉及面试题:什么是提高?什么是暂时性死区?var、let 及 const 区别?
对于这个问题,咱们应该先来了解提高(hoisting)这个概念。
console.log(a) // undefined
var a = 1复制代码
从上述代码中咱们能够发现,虽然变量尚未被声明,可是咱们却可使用这个未被声明的变量,这种状况就叫作提高,而且提高的是声明。
对于这种状况,咱们能够把代码这样来看
var a
console.log(a) // undefined
a = 1复制代码
接下来咱们再来看一个例子
var a = 10
var a
console.log(a)复制代码
对于这个例子,若是你认为打印的值为 undefined 那么就错了,答案应该是 10,对于这种状况,咱们这样来看代码
var a
var a
a = 10
console.log(a)复制代码
到这里为止,咱们已经了解了 var 声明的变量会发生提高的状况,其实不只变量会提高函数也会被提高。
console.log(a) // ƒ a() {} function a() {} var a = 1复制代码
对于上述代码,打印结果会是 ƒ a() {},即便变量声明在函数以后,这也说明了函数会被提高,而且优先于变量提高。
说完了这些,想必你们也知道 var 存在的问题了,使用 var 声明的变量会被提高到做用域的顶部,接下来咱们再来看 let 和 const 。
咱们先来看一个例子:
var a = 1 let b = 1 const c = 1 console.log(window.b) // undefined console.log(window. c) // undefined function test(){ console.log(a) let a } test()复制代码
首先在全局做用域下使用 let 和 const 声明变量,变量并不会被挂载到 window 上,这一点就和 var 声明有了区别。
再者当咱们在声明 a 以前若是使用了 a,就会出现报错的状况
你可能会认为这里也出现了提高的状况,可是由于某些缘由致使不能访问。
首先报错的缘由是由于存在暂时性死区,咱们不能在声明前就使用变量,这也是 let 和 const 优于 var 的一点。而后这里你认为的提高和 var 的提高是有区别的,虽然变量在编译的环节中被告知在这块做用域中能够访问,可是访问是受限制的。
那么到这里,想必你们也都明白 var、let 及 const 区别了,不知道你是否会有这么一个疑问,为何要存在提高这个事情呢,其实提高存在的根本缘由就是为了解决函数间互相调用的状况
function test1() { test2() } function test2() { test1() } test1()复制代码
假如不存在提高这个状况,那么就实现不了上述的代码,由于不可能存在 test1 在 test2 前面而后 test2 又在 test1 前面。
那么最后咱们总结下这小节的内容:
· 函数提高优先于变量提高,函数提高会把整个函数挪到做用域顶部,变量提高只会把声明挪到做用域顶部
涉及面试题:原型如何实现继承?Class 如何实现继承?Class 本质是什么?
首先先来说下 class,其实在 JS 中并不存在类,class 只是语法糖,本质仍是函数。
class Person {} Person instanceof Function // true复制代码
在上一章节中咱们讲解了原型的知识点,在这一小节中咱们将会分别使用原型和 class 的方式来实现继承。
组合继承
组合继承是最经常使用的继承方式,
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复制代码
以上继承的方式核心是在子类的构造函数中经过 Parent.call(this) 继承父类的属性,而后改变子类的原型为 new Parent() 来继承父类的函数。
这种继承方式优势在于构造函数能够传参,不会与父类引用属性共享,能够复用父类的函数,可是也存在一个缺点就是在继承父类函数的时候调用了父类构造函数,致使子类的原型上多了不须要的父类属性,存在内存上的浪费。
寄生组合继承
这种继承方式对组合继承进行了优化,组合继承缺点在于继承父类函数时调用了构造函数,咱们只须要优化掉这点就好了。
function Parent(value) { this.val = value } Parent.prototype.getValue= function() { console.log(this.val) } function Child(value) { Parent.call(this, value) } Child.prototype = Object.create(Parent.prototype, { constructor: { value: Child, enumerable: false, writable: true, configurable: true } }) const child = new Child(1) child.getValue() // 1 child instanceof Parent // true复制代码
以上继承实现的核心就是将父类的原型赋值给了子类,而且将构造函数设置为子类,这样既解决了无用的父类属性问题,还能正确的找到子类的构造函数。
Class 继承
以上两种继承方式都是经过原型去解决的,在 ES6 中,咱们可使用 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复制代码
class 实现继承的核心在于使用 extends 代表继承自哪一个父类,而且在子类构造函数中必须调用 super,由于这段代码能够当作 Parent.call(this, value)。
固然了,以前也说了在 JS 中并不存在类,class 的本质就是函数。
涉及面试题:为何要使用模块化?都有哪几种方式能够实现模块化,各有什么特色?
使用一个技术确定是有缘由的,那么使用模块化能够给咱们带来如下好处
当即执行函数
在早期,使用当即执行函数实现模块化是常见的手段,经过函数做用域解决了命名冲突、污染全局做用域的问题
(function(globalVariable){ globalVariable.test = function() {} // ... 声明各类变量、函数都不会污染全局做用域 })(globalVariable)复制代码
AMD 和 CMD
鉴于目前这两种实现方式已经不多见到,因此再也不对具体特性细聊,只须要了解这二者是如何使用的。
// AMD define(['./a', './b'], function(a,b) { // 加载模块完毕可使用 a.do() b.do() }) // CMD define(function(require,exports, module) { // 加载模块 // 能够把 require 写在函数体的任意地方实现延迟加载 var a = require('./a') a.doSomething() })复制代码
CommonJS
CommonJS 最先是 Node 在使用,目前也仍然普遍使用,好比在 Webpack 中你就能见到它,固然目前在 Node 中的模块管理已经和 CommonJS 有一些区别了。
// a.js module.exports = { a: 1 } // or exports.a = 1 // b.js var module = require('./a.js') module.a // -> log 1 由于 CommonJS 仍是会使用到的,因此这里会对一些疑难点进行解析 先说 require 吧 var module = require('./a.js') module.a // 这里其实就是包装了一层当即执行函数,这样就不会污染全局变量了, // 重要的是 module 这里,module 是 Node 独有的一个变量 module.exports = { a: 1 } // module 基本实现 var module = { id: 'xxxx', // 我总得知道怎么去找到他吧 exports: {} // exports 就是个空对象 } // 这个是为何 exports 和 module.exports 用法类似的缘由 var exports = module.exports var load = function (module) { // 导出的东西 var a = 1 module.exports = a return module.exports }; // 而后当我 require 的时候去找到独特的 // id,而后将要使用的东西用当即执行函数包装下,over复制代码
另外虽然 exports 和 module.exports 用法类似,可是不能对 exports 直接赋值。由于 var exports = module.exports 这句代码代表了 exports 和 module.exports 享有相同地址,经过改变对象的属性值会对二者都起效,可是若是直接对 exports 赋值就会致使二者再也不指向同一个内存地址,修改并不会对 module.exports 起效。
ES Module
ES Module 是原生实现的模块化方案,与 CommonJS 有如下几个区别
// 引入模块 API import XXX from './a.js' import { XXX } from './a.js' // 导出模块 API export function a() {} export default function() {}复制代码
涉及面试题:Proxy 能够实现什么功能?
若是你平时有关注 Vue 的进展的话,可能已经知道了在 Vue3.0 中将会经过 Proxy 来替换本来的 Object.defineProperty 来实现数据响应式。 Proxy 是 ES6 中新增的功能,它能够用来自定义对象中的操做。
let p = new Proxy(target, handler)复制代码
target 表明须要添加代理的对象,handler 用来自定义对象中的操做,好比能够用来自定义 set 或者 get 函数。
接下来咱们经过 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, property) return Reflect.set(target, property, value) } } return new Proxy(obj, handler) } let obj = { a: 1 } let p = onWatch(obj,(v, property) => { console.log(`监听到属性${property}改变为${v}`) },(target, property) => { console.log(`'${property}' = ${target[property]}`) }) p.a = 2 // 监听到属性a改变 p.a // 'a' = 2复制代码
在上述代码中,咱们经过自定义 set 和 get 函数的方式,在本来的逻辑中插入了咱们的函数逻辑,实现了在对对象任何属性进行读写时发出通知。
固然这是简单版的响应式实现,若是须要实现一个 Vue 中的响应式,须要咱们在 get 中收集依赖,在 set 派发更新,之因此 Vue3.0 要使用 Proxy 替换本来的 API 缘由在于 Proxy 无需一层层递归为每一个属性添加代理,一次便可完成以上操做,性能上更好,而且本来的实现有一些数据更新不能监听到,可是 Proxy 能够完美监听到任何方式的数据改变,惟一缺陷可能就是浏览器的兼容性很差了。
涉及面试题:map, filter, reduce 各自有什么做用?
map 做用是生成一个新数组,遍历原数组,将每一个元素拿出来作一些变换而后放入到新的数组中。
[1, 2, 3].map(v => v + 1) // -> [2, 3, 4] 另外 map 的回调函数接受三个参数,分别是当前索引元素,索引,原数组 ['1','2','3'].map(parseInt) 第一轮遍历 parseInt('1', 0) -> 1 第二轮遍历 parseInt('2', 1) -> NaN 第三轮遍历 parseInt('3', 2) -> NaN复制代码
filter 的做用也是生成一个新数组,在遍历数组的时候将返回值为 true 的元素放入新数组,咱们能够利用这个函数删除一些不须要的元素
let array = [1, 2, 4, 6] let newArray = array.filter(item => item !== 6) console.log(newArray) // [1, 2, 4]复制代码
和 map 同样,filter 的回调函数也接受三个参数,用处也相同。
最后咱们来说解 reduce 这块的内容,同时也是最难理解的一块内容。reduce 能够将数组中的元素经过回调函数最终转换为一个值。
若是咱们想实现一个功能将函数里的元素所有相加获得一个值,可能会这样写代码
const arr = [1, 2, 3] let total = 0 for (let i = 0; i < arr.length; i++) { total += arr[i] } console.log(total) //6复制代码
可是若是咱们使用 reduce 的话就能够将遍历部分的代码优化为一行代码
const arr = [1, 2, 3]
const sum = arr.reduce((acc, current) =>
acc + current, 0)
console.log(sum)复制代码
对于 reduce 来讲,它接受两个参数,分别是回调函数和初始值,接下来咱们来分解上述代码中 reduce 的过程
想必经过以上的解析你们应该明白 reduce 是如何经过回调函数将全部元素最终转换为一个值的,固然 reduce 还能够实现不少功能,接下来咱们就经过 reduce 来实现 map 函数
const arr = [1, 2, 3] const mapArray = arr.map(value => value * 2) const reduceArray = arr.reduce((acc, current)=> { acc.push(current * 2) return acc }, []) console.log(mapArray, reduceArray) // [2, 4, 6]复制代码
若是你对这个实现还有困惑的话,能够根据上一步的解析步骤来分析过程。
小结
这一章节咱们了解了部分 ES6 常考的知识点,其余的一些异步内容咱们会放在下一章节去讲。若是你们对于这个章节的内容存在疑问,欢迎在评论区与我互动。
在上一章节中咱们了解了常见 ES6 语法的一些知识点。这一章节咱们将会学习异步编程这一块的内容,鉴于异步编程是 JS 中相当重要的内容,因此咱们将会用三个章节来学习异步编程涉及到的重点和难点,同时这一块内容也是面试常考范围,但愿你们认真学习。
涉及面试题:并发与并行的区别?
异步和这小节的知识点其实并非一个概念,可是这两个名词确实是不少人都常会混淆的知识点。其实混淆的缘由可能只是两个名词在中文上的类似,在英文上来讲彻底是不一样的单词。
并发是宏观概念,我分别有任务 A 和任务 B,在一段时间内经过任务间的切换完成了这两个任务,这种状况就能够称之为并发。
并行是微观概念,假设 CPU 中存在两个核心,那么我就能够同时完成任务 A、B。同时完成多个任务的状况就能够称之为并行。
涉及面试题:什么是回调函数?回调函数有什么缺点?如何解决回调地狱问题?
回调函数应该是你们常用到的,如下代码就是一个回调函数的例子:
ajax(url, () => {
// 处理逻辑
})复制代码
可是回调函数有一个致命的弱点,就是容易写出回调地狱(Callback hell)。假设多个请求存在依赖性,你可能就会写出以下代码:
ajax(url, () => {
// 处理逻辑
ajax(url1, () => {
// 处理逻辑
ajax(url2,() => {
// 处理逻辑
})
})
})复制代码
以上代码看起来不利于阅读和维护,固然,你可能会想说解决这个问题还不简单,把函数分开来写不就得了
function firstAjax() { ajax(url1,() => { // 处理逻辑 secondAjax() }) } function secondAjax() { ajax(url2,() => { // 处理逻辑 }) } ajax(url, () => { // 处理逻辑 firstAjax() })复制代码
以上的代码虽然看上去利于阅读了,可是仍是没有解决根本问题。
回调地狱的根本问题就是:
1. 嵌套函数存在耦合性,一旦有所改动,就会牵一发而动全身
2. 嵌套函数一多,就很难处理错误
固然,回调函数还存在着别的几个缺点,好比不能使用 try catch 捕获错误,不能直接 return。在接下来的几小节中,咱们未来学习经过别的技术解决这些问题。
涉及面试题:你理解的 Generator 是什么?
Generator 算是 ES6 中难理解的概念之一了,Generator 最大的特色就是能够控制函数的执行。在这一小节中咱们不会去讲什么是 Generator,而是把重点放在 Generator 的一些容易困惑的地方。
function *foo(x) { let y = 2 * (yield (x + 1)) let z = yield (y / 3) return (x + y + z) } let it = foo(5) console.log(it.next()) // => {value: 6, done: false} console.log(it.next(12)) // => {value: 8, done: false} console.log(it.next(13)) // => {value: 42, done: true}复制代码
你也许会疑惑为何会产生与你预想不一样的值,接下来就让我为你逐行代码分析缘由
Generator 函数通常见到的很少,其实也于他有点绕有关系,而且通常会配合 co 库去使用。固然,咱们能够经过 Generator 函数解决回调地狱的问题,能够把以前的回调地狱例子改写为以下代码:
function *fetch() { yield ajax(url, () => {}) yield ajax(url1, () => {}) yield ajax(url2, () => {}) } let it = fetch() let result1 = it.next() let result2 = it.next() let result3 = it.next()复制代码
涉及面试题:Promise 的特色是什么,分别有什么优缺点?什么是 Promise 链?Promise 构造函数执行和 then 函数执行有什么区别?
Promise 翻译过来就是承诺的意思,这个承诺会在将来有一个确切的答复,而且该承诺有三种状态,分别是:
1. 等待中(pending)
2. 完成了 (resolved)
3. 拒绝了(rejected)
这个承诺一旦从等待状态变成为其余状态就永远不能更改状态了,也就是说一旦状态变为 resolved 后,就不能再次改变
new Promise((resolve,reject) => { resolve('success') // 无效 reject('reject') })复制代码
当咱们在构造 Promise 的时候,构造函数内部的代码是当即执行的
new Promise((resolve, reject) => { console.log('new Promise') resolve('success') }) console.log('finifsh') // new Promise -> finifsh复制代码
Promise 实现了链式调用,也就是说每次调用 then 以后返回的都是一个 Promise,而且是一个全新的 Promise,缘由也是由于状态不可变。若是你在 then 中 使用了 return,那么 return 的值会被 Promise.resolve() 包装
Promise.resolve(1) .then(res => { console.log(res) // => 1 return 2 // 包装成 Promise.resolve(2) }) .then(res => { console.log(res) // => 2 })复制代码
固然了,Promise 也很好地解决了回调地狱的问题,能够把以前的回调地狱例子改写为以下代码:
ajax(url) .then(res => { console.log(res) return ajax(url1) }).then(res => { console.log(res) return ajax(url2) }).then(res => console.log(res))复制代码
前面都是在讲述 Promise 的一些优势和特色,其实它也是存在一些缺点的,好比没法取消 Promise,错误须要经过回调函数捕获。
涉及面试题:async 及 await 的特色,它们的优势和缺点分别是什么?await 原理是什么?
一个函数若是加上 async ,那么该函数就会返回一个 Promise
async function test() { return "1" } console.log(test()) // -> Promise {<resolved>:"1"}复制代码
async 就是将函数返回值使用 Promise.resolve() 包裹了下,和 then 中处理返回值同样,而且 await 只能配套 async 使用
async function test() { let value = await sleep() }复制代码
async 和 await 能够说是异步终极解决方案了,相比直接使用 Promise 来讲,优点在于处理 then的调用链,可以更清晰准确的写出代码,毕竟写一大堆 then 也很恶心,而且也能优雅地解决回调地狱问题。固然也存在一些缺点,由于 await 将异步代码改形成了同步代码,若是多个异步代码没有依赖性却使用了 await 会致使性能上的下降。
async function test() { // 如下代码没有依赖性的话,彻底可使用 Promise.all 的方式 // 若是有依赖性的话,其实就是解决回调地狱的例子了 await fetch(url) await fetch(url1) await fetch(url2) }复制代码
下面来看一个使用 await 的例子:
let a = 0 let b = async () => { a = a + await 10 console.log('2', a) // -> '2' 10 } b() a++ console.log('1', a) // -> '1' 1复制代码
对于以上代码你可能会有疑惑,让我来解释下缘由
上述解释中提到了 await 内部实现了 generator,其实 await 就是 generator 加上 Promise 的语法糖,且内部实现了自动执行 generator。若是你熟悉 co 的话,其实本身就能够实现这样的语法糖。
涉及面试题:setTimeout、setInterval、requestAnimationFrame 各有什么特色?
异步编程固然少不了定时器了,常见的定时器函数有 setTimeout、setInterval、requestAnimationFrame。咱们先来说讲最经常使用的setTimeout,不少人认为 setTimeout 是延时多久,那就应该是多久后执行。
其实这个观点是错误的,由于 JS 是单线程执行的,若是前面的代码影响了性能,就会致使 setTimeout 不会定期执行。固然了,咱们能够经过代码去修正 setTimeout,从而使定时器相对准确
let period = 60 * 1000 * 60 * 2 let startTime = new Date().getTime() let count = 0 let end = new Date().getTime() + period let interval = 1000 let currentInterval = interval function loop() { count++ // 代码执行所消耗的时间 let offset = new Date().getTime() - (startTime + count *interval); let diff = end - new Date().getTime() let h = Math.floor(diff / (60 * 1000 * 60)) let hdiff = diff % (60 * 1000 * 60) let m = Math.floor(hdiff / (60 * 1000)) let mdiff = hdiff % (60 * 1000) let s = mdiff / (1000) let sCeil = Math.ceil(s) let sFloor = Math.floor(s) // 获得下一次循环所消耗的时间 currentInterval = interval - offset console.log('时:'+h, '分:'+m, '毫秒:'+s, '秒向上取整:'+sCeil, '代码执行时间:'+offset, '下次循环间隔'+currentInterval) setTimeout(loop, currentInterval) } setTimeout(loop,currentInterval)复制代码
接下来咱们来看 setInterval,其实这个函数做用和 setTimeout 基本一致,只是该函数是每隔一段时间执行一次回调函数。
一般来讲不建议使用 setInterval。第一,它和 setTimeout 同样,不能保证在预期的时间执行任务。第二,它存在执行累积的问题,请看如下伪代码
function demo() { setInterval(function(){ console.log(2) },1000) sleep(2000) } demo()复制代码
以上代码在浏览器环境中,若是定时器执行过程当中出现了耗时操做,多个回调函数会在耗时操做结束之后同时执行,这样可能就会带来性能上的问题。
若是你有循环定时器的需求,其实彻底能够经过 requestAnimationFrame 来实现
function setInterval(callback, interval) { let timer const now = Date.now let startTime = now() let endTime = startTime const loop = () => { timer = window.requestAnimationFrame(loop) endTime = now() if (endTime - startTime >=interval) { startTime = endTime = now() callback(timer) } } timer = window.requestAnimationFrame(loop) return timer } let a = 0 setInterval(timer=> { console.log(1) a++ if (a === 3) cancelAnimationFrame(timer) }, 1000)复制代码
首先 requestAnimationFrame 自带函数节流功能,基本能够保证在 16.6 毫秒内只执行一次(不掉帧的状况下),而且该函数的延时效果是精确的,没有其余定时器时间不许的问题,固然你也能够经过该函数来实现 setTimeout。
小结
异步编程是 JS 中较难掌握的内容,同时也是很重要的知识点。以上提到的每一个知识点其实均可以做为一道面试题,但愿你们能够好好掌握以上内容若是你们对于这个章节的内容存在疑问,欢迎在评论区与我互动。
在这一章节中,咱们将会学习到一些原理相关的知识,不会解释涉及到的知识点的做用及用法,若是你们对于这些内容还不怎么熟悉,推荐先去学习相关的知识点内容再来学习原理知识。
涉及面试题:call、apply 及 bind 函数内部实现是怎么样的?
首先从如下几点来考虑如何实现这几个函数
那么咱们先来实现 call
Function.prototype.myCall = function(context){ if (typeof this !== 'function') { throw new TypeError('Error') } context = context || window context.fn = this const args = [...arguments].slice(1) const result = context.fn(...args) delete context.fn return result }复制代码
如下是对实现的分析:
以上就是实现 call 的思路,apply 的实现也相似,区别在于对参数的处理,因此就不一一分析思路了
Function.prototype.myApply = function(context){ if (typeof this !== 'function') { throw new TypeError('Error') } context = context || window context.fn = this let result // 处理参数和 call 有区别 if (arguments[1]) { result = context.fn(...arguments[1]) } else { result = context.fn() } delete context.fn return result }复制代码
bind 的实现对比其余两个函数略微地复杂了一点,由于 bind 须要返回一个函数,须要判断一些边界问题,如下是 bind 的实现
Function.prototype.myBind = function(context) { if (typeof this !== 'function') { throw new TypeError('Error') } const _this = this const args = [...arguments].slice(1) // 返回一个函数 return function F() { // 由于返回了一个函数,咱们能够 new F(),因此须要判断 if (this instanceof F){ return new _this(...args,...arguments) } return _this.apply(context, args.concat(...arguments)) } }复制代码
如下是对实现的分析:
涉及面试题:new 的原理是什么?经过 new 的方式建立对象和经过字面量建立有什么区别?
在调用 new 的过程当中会发生以上四件事情:
1. 新生成了一个对象
2. 连接到原型
3. 绑定 this
4. 返回新对象
根据以上几个过程,咱们也能够试着来本身实现一个 new
function create() { let obj = {} let Con = [].shift.call(arguments) obj.__proto__ = Con.prototype let result = Con.apply(obj, arguments) return result instanceof Object ? result : obj }复制代码
如下是对实现的分析:
对于对象来讲,其实都是经过 new 产生的,不管是 function Foo() 仍是 let a = { b : 1 } 。
对于建立一个对象来讲,更推荐使用字面量的方式建立对象(不管性能上仍是可读性)。由于你使用 new Object() 的方式建立对象须要经过做用域链一层层找到 Object,可是你使用字面量的方式就没这个问题。
function Foo() {} // function 就是个语法糖 // 内部等同于 new Function() let a = { b: 1 } // 这个字面量内部也是使用了 new Object() 更多关于 new 的内容能够阅读我写的文章 聊聊 new 操做符。复制代码
涉及面试题:instanceof 的原理是什么?
instanceof 能够正确的判断对象的类型,由于内部机制是经过判断对象的原型链中是否是能找到类型的 prototype。
咱们也能够试着实现一下 instanceof
function myInstanceof(left, right) { let prototype = right.prototype left = left.__proto__ while (true) { if (left === null || left === undefined) return false if (prototype === left) return true left = left.__proto__ } }复制代码
如下是对实现的分析:
涉及面试题:为何 0.1 + 0.2 != 0.3?如何解决这个问题?
先说缘由,由于 JS 采用 IEEE 754 双精度版本(64位),而且只要采用 IEEE 754 的语言都有该问题。
咱们都知道计算机是经过二进制来存储东西的,那么 0.1 在二进制中会表示为
// (0011) 表示循环
0.1 = 2^-4 * 1.10011(0011)复制代码
咱们能够发现,0.1 在二进制中是无限循环的一些数字,其实不仅是 0.1,其实不少十进制小数用二进制表示都是无限循环的。这样其实没什么问题,可是 JS 采用的浮点数标准却会裁剪掉咱们的数字。
IEEE 754 双精度版本(64位)将 64 位分为了三段
那么这些循环的数字被裁剪了,就会出现精度丢失的问题,也就形成了 0.1 再也不是 0.1 了,而是变成了 0.100000000000000002
0.100000000000000002 === 0.1 // true复制代码
那么一样的,0.2 在二进制也是无限循环的,被裁剪后也失去了精度变成了 0.200000000000000002
0.200000000000000002 === 0.2 // true复制代码
因此这二者相加不等于 0.3 而是 0.300000000000000004
0.1 + 0.2 === 0.30000000000000004 // true复制代码
那么可能你又会有一个疑问,既然 0.1 不是 0.1,那为何 console.log(0.1) 倒是正确的呢?
由于在输入内容的时候,二进制被转换为了十进制,十进制又被转换为了字符串,在这个转换的过程当中发生了取近似值的过程,因此打印出来的实际上是一个近似值,你也能够经过如下代码来验证
console.log(0.100000000000000002) // 0.1复制代码
那么说完了为何,最后来讲说怎么解决这个问题吧。其实解决的办法有不少,这里咱们选用原生提供的方式来最简单的解决问题
parseFloat((0.1 + 0.2).toFixed(10)) === 0.3 // true复制代码
涉及面试题:V8 下的垃圾回收机制是怎么样的?
V8 实现了准确式 GC,GC 算法采用了分代式垃圾回收机制。所以,V8 将内存(堆)分为新生代和老生代两部分。
新生代算法
新生代中的对象通常存活时间较短,使用 Scavenge GC 算法。
在新生代空间中,内存空间分为两部分,分别为 From 空间和 To 空间。在这两个空间中,一定有一个空间是使用的,另外一个空间是空闲的。新分配的对象会被放入 From 空间中,当 From 空间被占满时,新生代 GC 就会启动了。算法会检查 From 空间中存活的对象并复制到 To 空间中,若是有失活的对象就会销毁。当复制完成后将 From 空间和 To 空间互换,这样 GC 就结束了。
老生代算法
老生代中的对象通常存活时间较长且数量也多,使用了两个算法,分别是标记清除算法和标记压缩算法。
在讲算法前,先来讲下什么状况下对象会出如今老生代空间中:
老生代中的空间很复杂,有以下几个空间
enum AllocationSpace { // TODO(v8:7464): Actually map this space's memory as read-only. RO_SPACE, // 不变的对象空间 NEW_SPACE, // 新生代用于 GC 复制算法的空间 OLD_SPACE, // 老生代常驻对象空间 CODE_SPACE, // 老生代代码对象空间 MAP_SPACE, // 老生代 map 对象 LO_SPACE, // 老生代大空间对象 NEW_LO_SPACE, // 新生代大空间对象 FIRST_SPACE = RO_SPACE, LAST_SPACE = NEW_LO_SPACE, FIRST_GROWABLE_PAGED_SPACE = OLD_SPACE, LAST_GROWABLE_PAGED_SPACE = MAP_SPACE };复制代码
在老生代中,如下状况会先启动标记清除算法:
在这个阶段中,会遍历堆中全部的对象,而后标记活的对象,在标记完成后,销毁全部没有被标记的对象。在标记大型对内存时,可能须要几百毫秒才能完成一次标记。这就会致使一些性能上的问题。为了解决这个问题,2011 年,V8 从 stop-the-world 标记切换到增量标志。在增量标记期间,GC 将标记工做分解为更小的模块,可让 JS 应用逻辑在模块间隙执行一会,从而不至于让应用出现停顿状况。但在 2018 年,GC 技术又有了一个重大突破,这项技术名为并发标记。该技术可让 GC 扫描和标记对象时,同时容许 JS 运行,你能够点击 该博客 详细阅读。
清除对象后会形成堆内存出现碎片的状况,当碎片超过必定限制后会启动压缩算法。在压缩过程当中,将活的对象像一端移动,直到全部对象都移动完成而后清理掉不须要的内存。
小结
以上就是 JS 进阶知识点的内容了,这部分的知识相比于以前的内容更加深刻也更加的理论,也是在面试中可以于别的候选者拉开差距的一块内容。若是你们对于这个章节的内容存在疑问,欢迎在评论区与我互动。
JS 思考题
以前咱们经过了七个章节来学习关于 JS 这部分的内容,那么接下来,会以几道思考题的方式来确保你们理解这部分的内容。
这种方式不只能加深你对知识点的理解,同时也能帮助你串联起多个碎片知识点。一旦你拥有将多个碎片知识点串联起来的能力,在面试中就不会常常出现一问一答的状况。若是面试官的每一个问题你都能引伸出一些相关联的知识点,那么面试官必定会提升对你的评价。
思考题一:JS 分为哪两大类型?都有什么各自的特色?你该如何判断正确的类型?
首先这几道题目想必不少人都可以很好的答出来,接下来就给你们一点思路讲出不同凡响的东西。
思路引导:
1. 对于原始类型来讲,你能够指出 null 和 number 存在的一些问题。对于对象类型来讲,你能够从垃圾回收的角度去切入,也能够说一下对象类型存在深浅拷贝的问题。
2. 对于判断类型来讲,你能够去对比一下 typeof 和 instanceof 之间的区别,也能够指出 instanceof 判断类型也不是彻底准确的。
以上就是这道题目的回答思路,固然不是说让你们彻底按照这个思路去答题,而是存在一个意识,当回答面试题的时候,尽可能去引伸出这个知识点的某些坑或者与这个知识点相关联的东西。
思考题二:你理解的原型是什么?
思路引导:
起码说出原型小节中的总结内容,而后还能够指出一些小点,好比并非全部函数都有 prototype 属性,而后引伸出原型链的概念,提出如何使用原型实现继承,继而能够引伸出 ES6 中的 class 实现继承。
思考题三:bind、call 和 apply 各自有什么区别?
思路引导:
首先确定是说出三者的不一样,若是本身实现过其中的函数,能够尝试说出本身的思路。而后能够聊一聊 this 的内容,有几种规则判断 this 究竟是什么,this 规则会涉及到 new,那么最后能够说下本身对于 new 的理解。
思考题四:ES6 中有使用过什么?
思路引导:
这边可说的实在太多,你能够列举 1 - 2 个点。好比说说 class,那么 class 又能够拉回到原型的问题;能够说说 promise,那么线就被拉到了异步的内容;能够说说 proxy,那么若是你使用过 Vue 这个框架,就能够谈谈响应式原理的内容;一样也能够说说 let 这些声明变量的语法,那么就能够谈及与 var 的不一样,说到提高这块的内容。
思考题五:JS 是如何运行的?
思路引导:
这实际上是很大的一块内容。你能够先说 JS 是单线程运行的,这里就能够说说你理解的线程和进程的区别。而后讲到执行栈,接下来的内容就是涉及 Eventloop 了,微任务和宏任务的区别,哪些是微任务,哪些又是宏任务,还能够谈及浏览器和 Node 中的 Eventloop 的不一样,最后还能够聊一聊 JS 中的垃圾回收。
小结
虽然思考题很少,可是其实每一道思考题背后均可以引伸出不少内容,你们接下去在学习的过程当中也应该始终有一个意识,你学习的这块内容到底和你如今脑海里的哪个知识点有关联。同时也欢迎你们总结这些思考题,而且把总结的内容连接放在评论中,我会挑选出不错的文章单独放入一章节给你们参考。
DOM 断点
给 JS 打断点想必各位都听过,可是 DOM 断点知道的人应该就少了。若是你想查看一个 DOM 元素是如何经过 JS 更改的,你就可使用这个功能。
当咱们给 ul 添加该断点之后,一旦 ul 子元素发生了改动,好比说增长了子元素的个数,那么就会自动跳转到对应的 JS 代码
其实不光能够给 DOM 打断点,咱们还能够给 Ajax 或者 Event Listener 打断点。
查看事件
咱们还能够经过 DevTools 来查看页面中添加了多少的事件。假如当你发现页面滚动起来有性能上的问题时,就能够查看一下有多少 scroll 事件被添加了
找到以前查看过的 DOM 元素
不知道你是否遇到过这样的问题,找不到以前查看过的 DOM 元素在哪里了,须要一个个去找这就有点麻烦了,这时候你就可使用这个功能。
咱们能够经过 $0 来找到上一次查看过的 DOM 元素,$1 就是上上次的元素,以后以此类推。这时候你可能会说,打印出来元素有啥用,在具体什么位置还要去找啊,不用急,立刻我就能够解决这个问题
当你点击这个选项时,页面立马会跳转至元素所在位置,而且 DevTools 也会变到 Elements 标签。
Debugging
给 JS 打断点想必你们都会,可是打断点也是有一个鲜为人知的 Tips 的。
for (let index = 0; index < 10; index++) { // 各类逻辑 console.log(index) }复制代码
对于这段代码来讲,若是我只想看到 index 为 5 时相应的断点信息,可是一旦打了断点,就会每次循环都会停下来,很浪费时间,那么经过这个小技巧咱们就能够圆满解决这个问题
首先咱们先右键断点,而后选择 Edit breakpoint... 选项
在弹框内输入 index === 5,这样断点就会变为橙色,而且只有当符合表达式的状况时断点才会被执行
小结
虽然这一章的内容并很少,可是涉及到的几个场景都是平常常常会碰到的,但愿这一章节的内容会对你们有帮助。