乐趣区

《前端面试手记》之JavaScript基础知识梳理(下)

???? 内容速览 ????

实现 ES5 继承的 4 种方法
原型和原型链
作用域和作用域链
Event Loop
执行上下文
闭包的理解和分析

???? 查看全部教程 / 阅读原文????
ES5 继承
题目:ES5 中常用继承方法。
方法一:绑定构造函数
缺点:不能继承父类原型方法 / 属性
function Animal(){
this.species = ‘ 动物 ’
}

function Cat(){
// 执行父类的构造方法, 上下文为实例对象
Animal.apply(this, arguments)
}

/**
* 测试代码
*/
var cat = new Cat()
console.log(cat.species) // output: 动物

方法二:原型链继承
缺点:无法向父类构造函数中传递参数;子类原型链上定义的方法有先后顺序问题。
注意:js 中交换原型链,均需要修复 prototype.constructor 指向问题。
function Animal(species){
this.species = species
}
Animal.prototype.func = function(){
console.log(‘Animal’)
}

function Cat(){}
/**
* func 方法是无效的, 因为后面原型链被重新指向了 Animal 实例
*/
Cat.prototype.func = function() {
console.log(‘Cat’)
}

Cat.prototype = new Animal()
Cat.prototype.constructor = Cat // 修复: 将 Cat.prototype.constructor 重新指向本身

/**
* 测试代码
*/
var cat = new Cat()
cat.func() // output: Animal
console.log(cat.species) // undefined
方法 3: 组合继承
结合绑定构造函数和原型链继承 2 种方式,缺点是:调用了 2 次父类的构造函数。
function Animal(species){
this.species = species
}
Animal.prototype.func = function(){
console.log(‘Animal’)
}

function Cat(){
Animal.apply(this, arguments)
}

Cat.prototype = new Animal()
Cat.prototype.constructor = Cat

/**
* 测试代码
*/
var cat = new Cat(‘cat’)
cat.func() // output: Animal
console.log(cat.species) // output: cat
方法 4: 寄生组合继承
改进了组合继承的缺点,只需要调用 1 次父类的构造函数。它是引用类型最理想的继承范式。(引自:《JavaScript 高级程序设计》)
/**
* 寄生组合继承的核心代码
* @param {Function} sub 子类
* @param {Function} parent 父类
*/
function inheritPrototype(sub, parent) {
// 拿到父类的原型
var prototype = Object(parent.prototype)
// 改变 constructor 指向
prototype.constructor = sub
// 父类原型赋给子类
sub.prototype = prototype
}

function Animal(species){
this.species = species
}
Animal.prototype.func = function(){
console.log(‘Animal’)
}

function Cat(){
Animal.apply(this, arguments) // 只调用了 1 次构造函数
}

inheritPrototype(Cat, Animal)

/**
* 测试代码
*/

var cat = new Cat(‘cat’)
cat.func() // output: Animal
console.log(cat.species) // output: cat
原型和原型链

所有的引用类型(数组、对象、函数),都有一个__proto__属性,属性值是一个普通的对象

所有的函数,都有一个 prototype 属性,属性值也是一个普通的对象
所有的引用类型(数组、对象、函数),__proto__属性值指向它的构造函数的 prototype 属性值

注:ES6 的箭头函数没有 prototype 属性,但是有__proto__属性。
const obj = {};
// 引用类型的 __proto__ 属性值指向它的构造函数的 prototype 属性值
console.log(obj.__proto__ === Object.prototype); // output: true
原型
题目:如何 JS 中的原型?
// 构造函数
function Foo(name, age) {
this.name = name
}
Foo.prototype.alertName = function () {
alert(this.name)
}
// 创建示例
var f = new Foo(‘zhangsan’)
f.printName = function () {
console.log(this.name)
}
// 测试
f.printName()
f.alertName()
但是执行 alertName 时发生了什么?这里再记住一个重点 当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的__proto__(即它的构造函数的 prototype)中寻找,因此 f.alertName 就会找到 Foo.prototype.alertName。
原型链
题目:如何 JS 中的原型链?
以上一题为基础,如果调用 f.toString()。

f 试图从__proto__中寻找(即 Foo.prototype),还是没找到 toString() 方法。
继续向上找,从 f.__proto__.__proto__中寻找(即 Foo.prototype.__proto__中)。因为 Foo.prototype 就是一个普通对象,因此 Foo.prototype.__proto__ = Object.prototype

最终对应到了 Object.prototype.toString

这是对深度遍历的过程,寻找的依据就是一个链式结构,所以叫做“原型链”。
作用域和作用域链
题目:如何理解 JS 的作用域和作用域链。
①作用域
ES5 有”全局作用域“和”函数作用域“。ES6 的 let 和 const 使得 JS 用了”块级作用域“。
为了解决 ES5 的全局冲突,一般都是闭包编写:(function(){…})()。将变量封装到函数作用域。
②作用域链
当前作用域没有找到定义,继续向父级作用域寻找,直至全局作用域。这种层级关系,就是作用域链。
Event Loop
单线程
题目:讲解下面代码的执行过程和结果。
var a = true;
setTimeout(function(){
a = false;
}, 100)
while(a){
console.log(‘while 执行了 ’)
}

这段代码会一直执行并且输出 ”while…”。JS 是单线程的,先跑执行栈里的同步任务,然后再跑任务队列的异步任务。
执行栈和任务队列
题目:说一下 JS 的 Event Loop。
简单总结如下:

JS 是单线程的,其上面的所有任务都是在两个地方执行:执行栈和任务队列。前者是存放同步任务;后者是异步任务有结果后,就在其中放入一个事件。
当执行栈的任务都执行完了(栈空),js 会读取任务队列,并将可以执行的任务从任务队列丢到执行栈中执行。
这个过程是循环进行,所以称作 Loop。

执行上下文
题目:解释下“全局执行上下文“和“函数执行上下文”。
①全局执行上下文
解析 JS 时候,创建一个 全局执行上下文 环境。把代码中即将执行的(内部函数的不算,因为你不知道函数何时执行)变量、函数声明都拿出来。未赋值的变量就是 undefined。
下面这段代码输出:undefined;而不是抛出 Error。因为在解析 JS 的时候,变量 a 已经存入了全局执行上下文中了。
console.log(a);
var a = 1;

②函数执行上下文
和全局执行上下文差不多,但是多了 this 和 arguments 和参数。
在 JS 中,this 是关键字,它作为内置变量,其值是在执行的时候确定(不是定义的时候确定)。
闭包的理解和分析
题目:解释下 js 的闭包
直接上 MDN 的解释:闭包是函数和声明该函数的词法环境的组合。
而在 JavaScript 中,函数是被作为一级对象使用的,它既可以本当作值返回,还可以当作参数传递。理解了:“Js 中的函数运行在它们被定义的作用域,而不是它们被执行的作用域”(摘自《JavaScript 语言精粹》)这句话即可。
题目:闭包优缺点
闭包封住了变量作用域,有效地防止了全局污染;但同时,它也存在内存泄漏的风险:

在浏览器端可以通过强制刷新解决,对用户体验影响不大
在服务端,由于 node 的内存限制和累积效应,可能会造成进程退出甚至服务器沓机

解决方法是显式对外暴露一个接口,专门用以清理变量:
function mockData() {
const mem = {}

return {
clear: () => mem = null, // 显式暴露清理接口

get: (page) => {
if(page in mem) {
return mem[page]
}
mem[page] = Math.random()
}
}
}

更多系列教程
⭐在 GitHub 上收藏 / 订阅⭐
《前端知识体系》

JavaScript 基础知识梳理 (上)
JavaScript 基础知识梳理 (下)
谈谈 promise/async/await 的执行顺序与 V8 引擎的 BUG
前端面试中常考的源码实现
Flex 上手与实战
……

《设计模式手册》

单例模式
策略模式
代理模式
迭代器模式
订阅 - 发布模式
桥接模式
备忘录模式
模板模式
抽象工厂模式
……

《Webpack4 渐进式教程》

webpack4 系列教程 (二): 编译 ES6
webpack4 系列教程 (三): 多页面解决方案 – 提取公共代码
webpack4 系列教程 (四): 单页面解决方案 – 代码分割和懒加载
webpack4 系列教程 (五): 处理 CSS
webpack4 系列教程 (八): JS Tree Shaking
webpack4 系列教程 (十二):处理第三方 JavaScript 库
webpack4 系列教程 (十五):开发模式与 webpack-dev-server
……

⭐在 GitHub 上收藏 / 订阅⭐

退出移动版