- 说说你所知道的在JS中的数据类型:
- 引用值: Array, Object, Set, Map等
- 原始值: Number, String, Boolean, undefined, null, Symbol
- 说说在JS中引用值和原始值有什么区别?
- 原始值存储在栈中, 当把一个原始值传递给变量的时候, 传递的是值的副本
- 引用值存储在堆中, 当把一个引用值传递给另一个变量的时候, 传递的是值的指针, 相当于是门牌号, 当你修改一个引用值中的数据时, 另一个变量也会受到影响
- 以上的话总结一下就是: 引用传址, 原始传值
- 说说在JS中栈(stack)和堆(heap)的区别:
- 栈因为遵循的是有规律的先进后出的特点, 所以栈的存取速度比较快, 而且容易实现内存回收, 不过就是有点不太灵活
- 堆的特点就是使用灵活, 因为他就等于是一块区域, 每个变量的存取就是在堆里给他划分一块空间, 空间还可以动态的删除和增加, 也正是因为这样太过于灵活, 导致查询速度会比较慢
- 聊聊IIFE(Immediately Invoked Function Expression)
- 立即执行函数: 当一个函数做为立即执行函数的时候, 读到他这一行代码, 立马就会执行这个函数, 且执行完毕后立即断开该函数的AO连接
- 立即执行函数的作用如下:
- 创建一个独立的作用域, 这个作用域里的变量外部是访问不到的, 这样就可以避免变量污染
- 实现闭包和私有数据, 实现“模块化”
- 顺带扩展一下, JS中我们一般常用的书写立即执行函数的写法有
(function(){})() 和(function() {}()) 两种, 但其实想要实现一个IIFE, 远不止这两种方式, 因为在JS中, 我们首先要明确一点, 只要是表达式就可以被执行, 而IIFE也不是ECMAScript的标准, 而是大家发现的一种编程技巧, 由于JS规定了只要是表达式就可以被执行, 所以我们只需要想方设法把一个函数声明变成表达式就可以了
- 聊聊作用域
- 定义: 变量和函数能够被访问到的区域
- 在JS中, ES6之前, 我们的作用域分为函数作用域和全局作用域两种, ES6以后, 我们多出来了一个块级作用域
- 往深一点讲的话, 就是每个JS函数都是一个对象, 对象上有一个
[[scope]]属性, 这个属性里面装的就是作用域, 其实也是这个函数的执行期上下文(也是我们常说的AO对象)
- 当函数执行前一刻, 他会创建一个执行期上下文, 并直接开始走预编译3部曲:
- 找变量和形参名, 将变量和形参名作为AO对象的属性,属性值为undefined
- 形参实参相统一, 把实参值赋予AO对象中已经存储的形参属性
- 找函数声明, 将函数声明的名放入到AO对象中, 值赋予函数体
- 我们还漏了一些东西, AO对象创建的时候(且在预编译之前)里面就已经有一些东西了, 比如arguments, 同时还存在了一个东西就是父级的执行期上下文 - 当函数调用完毕, 他的执行期上下文就会被销毁 - 而我们上面说的子集会攥着父级的作用域(执行期上下文), 这好像一个链条关系, 所以我们也称之为作用域
- 聊聊闭包
- 当内部函数被保存到了外部的时候, 就造成了闭包, 闭包会导致原有的作用域链不被释放, 造成内存泄露
- 闭包的作用大致如下:
- 实现公有变量(比如用闭包实现一个累加器)
- 做缓存结构(原理和实现公有变量差不多)
- 隔离作用域实现“模块化”, 减少全局污染
- 聊聊原型和原型链
- 原型是function上的一个属性, 他定义了由构造函数构造出来的对象的公有祖先, 通过该构造函数构造出来的对象, 可以集成原型的属性和方法, 原型也是对象
- 所有的对象都具有一个隐式原型
__proto__, 默认情况下, 隐式原型指向创建该对象的构造函数的prototype - 当我们访问一个对象上的成员的时候:
- 看看对象自身是否拥有该成员, 如果有的话直接使用
- 看看对象的
__proto__上是否拥有该成员, 如果有直接使用 - 由于该对象的
__proto__也是一个对象, 所以他如果没有的话, 他又会去找他的__proto上有没有, 依次类推, 最终找到Object.prototype, 这就是原型链 - 但是并不是所有对象的原型链的最顶端都是
Object.prototype, 有一个例外, 就是通过Object.create(null)创建的对象, 隐式原型会直接指向null
- 由于原型(prototype)是函数上的属性, 又由于prototype自身也是对象, 那对象又是Object构造函数创建出来的, 所以这里存在一个先有鸡还是先有蛋的问题
Function.__proto__ === Function.prototypObject.__proto__ === null
- 说说this指向的问题:
- 函数预编译过程中的this指向了window
- 全局作用域中this指向window
- 谁调用的函数, this就指向谁, 如果是自执行, 则指向window
- call, apply, bind可以改变函数的this指向
- es6箭头函数的this来源于该函数出生时向上寻找有this的父级作为自己的this
- 说说浏览器的Event Loop
- 事件循环本身是为了去协调事件, 用户交互, 脚本, 渲染引擎, 网络等工作的有序进行的
- 为什么需要事件循环?因为在JS是单线程的, 那就意味着同一时间段只能做一件事, 我打个比方, 假设JS的主线程去监听用户的点击事件的话(甚至一切IO操作), 那么在监听事件到触发事件的这个过程中, 我们将什么JS代码都不能执行, 这是非常恐怖的
- 我们可以认为JS的主线程是只会走同步代码的, 那么IO操作就需要交给其他的调度线程去做, 然后这些调度线程根据事件循环来确定优先级, 换句话说事件循环本身就是一种任务约定, 他规定了如下:
- 当JS的主线程有需要执行的任务时, 会将所有主线程的任务全部执行完毕
- 当主线程的任务执行完毕以后, JS会去查看其他线程是否有待执行的任务, 比如setTimeout被计时器线程读秒结束以后会将回调函数放入宏任务队列, 这个时候JS看到了就会将宏任务队列中的任务拉进主线程继续执行, 执行完毕以后又会去查看其它线程的情况, 如此往复, 这就是事件循环
- 浏览器端的事件循环规定了宏任务和微任务一说:
- setTimeout, setInterval进入宏任务
- MutationObserver, Promise进入微任务
微任务队列的任务始终会比宏任务队列的任务先被选入JS主线程进行执行