前言
本篇是面试系列的JavaScript的基础篇。文章主要围绕着基础类型、作用域、闭包、原型、继承、this的指向、深浅拷贝、字符串和数组的方法、节流、防抖、函数式编程等知识展开的。
正文
基础类型
JavaScript的数据类型主要分为两类:原始类型、引用类型
原始类型:6种
- null 【JS的数据底层都是用二进制进行存储的,前三位为0会被判断成对象,null是全都为0】
- undefined
- Number【双精度64位二进制格式的值——数字、±Infinity、NaN】
- String
- Boolean
- Symbol【ES6新增定义,实例是唯一且不可变的】
引用类型:
- Object
- Array
- Function
在内存中,存储的形式:
- 原始类型,会被保存到栈内存中
- 引用类型,会被保存到堆内存中
类型之间的赋值方式:
- 原始类型的赋值:对值类型的拷贝,会复制一份值
- 引用类型的赋值:对引用地址的拷贝
基础类型的隐式转换也是面试考察的重点,Symbol类型有兴趣的小伙伴也可以手写实现一下【面试系列——手写代码实现(一)】
执行上下文 | 作用域 | 闭包
JS中存在的执行上下文类型:
- 全局上下文:windows对象
- 函数上下文:每次调用函数时,创建一个新的上下文
- eval上下文
每个执行上下文,都存在三个属性:作用域、变量、this
作用域:JS只有全局作用域和函数作用域
- var创建的变量只有函数作用域
- let和const创建的变量既有函数作用域,也有块级作用域
作用域链:JavaScript引擎在寻找一个变量名的时候,会在当前作用域进行查找,如果没有,就会继续往外层作用域进行查找,直到全局作用域为止,这就形成了一个作用域链。
闭包:引用外部函数变量的内部函数【红宝书的定义】
主要使用场景:
- setTimeout、setInterval、setImmediate之类的定时器、事件回调、ajax请求的回调
- 被外部函数作为函数返回,或者返回对象中引用内部函数的情况
可以使用立即执行函数(IIFE)来实现闭包,代码如下:
(function(i){
console.log(i);
})(i);
复制代码
闭包只是存储外部变量的引用,不会拷贝外部变量的值;闭包引用的变量会被存放到堆内存中。
this | apply | call | bind
this的指向,是在执行上下文被创建的时候,被确定的。
this的绑定规则总共有下面五种:
- 默认绑定【严格 | 非严格】
- 默认模式下,this指向全局对象
- 严格模式下,this指向undefined
- 隐式绑定:函数调用时,会有一个上下文对象,函数的this会绑定在这个上下文对象上面
- 显式绑定:通过apply/call/bind的方式,来实现this的显式绑定
- new绑定:JS构造函数通过new操作符进行调用,此处的this指向新创建的对象实例
- 箭头函数绑定:引用外层上下文的this
- 箭头函数的this,相当于普通变量
- 寻找箭头函数的this,就相当于寻找外层作用域
- 如果改变了外层作用域的this,就可以改变箭头函数的this
this的指向优先级,依次按照 箭头函数 > new操作符 > 显示绑定 > 隐式绑定 > 默认绑定
apply | call | bind的作用:改变函数中this的指向
apply和call的区别:
- 相同点:
- 第一个参数都是指向函数中的this
- 都可以接受一个参数
- 不同点:
- apply只能接受两个参数,其中后一个参数可以是数组、类数组、对象
- call可以接受多个参数
apply/call和bind的区别:
- apply和call是函数执行时调用的
- bind是函数定义时调用的
this的指向是面试考察的重点,一般都会以题目的形式进行考察;apply、call、bind是需要手写实现的【面试系列——手写代码实现(一)】
原型和继承
原型(prototype):给其他对象提供共享属性的对象
隐式引用(proto):所有对象,都存在一个隐式引用,指向它的原型
构造函数(Constructor):构造函数,它的原型指向实例的原型
- 构造函数和普通函数的区别:
- 使用new操作符生成实例的函数就是构造函数
- 直接调用的就是普通函数
- Symbol是基础类型,不是构造函数,它通过Symbol()创建实例
原型链:如图所示
图中,__proto__形成的链条组合,就是原型链
继承方式:
- 原型链继承:本质就是重写原型对象,代之以新对象的实例
- 缺点:
- 多个实例对引用类型进行篡改
- 子类型原型上的构造函数被重写了
- 给子类型添加属性和方法需要在替换原型之后
- 创建子类型之后,无法向父类构造函数传参
- 缺点:
- 构造函数继承:使用父类构造函数来增强子类实例,等同于复制父类的实例给子类(不使用原型)
- 核心:superType.call(this)
- 缺点:
- 只能继承父类实例的属性和方法,不能复制父类原型上的属性和方法
- 无法实现复用,每个实例都会有父类实例的副本,影响性能
- 组合继承:上述两种方式的组合,用原型链的方式继承原型上的属性和方法,用构造函数的方式,来实现实例属性和方法的继承
- 缺点:
- 在实例和原型上面有一份相同的属性和方法
- 缺点:
- 寄生组合式继承:ES6 Class的继承原理
- 子类实例继承了父类实例的属性和方法
- 原型链的继承
- 父类构造函数上的属性继承
原型,基本只要记住原型链的图,面试都没有太大的问题;继承的概念比较深涩难懂,需要多加实践。new、instanceof、ES6的继承都是需要手写实现的。详见【面试系列——手写代码实现(一)】
浅拷贝和深拷贝
浅拷贝:创建一个新对象,将原对象的属性值赋值给新对象
- 使用场景:Object.assign、展开符(...)、Array.prototype.slice
Object.assign:主要将一个和多个对象的可枚举属性进行合并
深拷贝:拷贝一个对象的全部属性,拷贝完成之后,两个对象相互不影响
- 使用场景:JSON.parse(JSON.stringify(obj))
其中JSON拷贝的缺点:
- 会忽略undefined
- 会忽略Symbol
- 不能序列化函数
- 不能拷贝循环引用对象
- 不能正确调用new Date()
- 不能处理正则
深拷贝和浅拷贝是面试考察的重点,主要是手写实现深拷贝,在面试题中出现次数很多【面试系列——手写代码实现(一)】
异步处理
异步解决方案:
- Raw Callback Style:朴素函数作为回调函数,接受error,data等参数
- Promise Callback Style:通过{ then }对象,去处理onFulfilled和onRejected两个回调函数
- Generator Callback Style:通过 * 号和yield关键词,将多层嵌套的callback扁平化的语法糖
- Async/await Callback Style:通过async和await关键词,将Promise+Generator标准化和语法化的产物
Promise:是一个类,通过new来进行声明。
Promise有三种状态:
- pending:等待状态,初始化状态,可转变成fulfilled或rejected
- fulfilled:成功状态,不可转变状态,且有一个不可变值(value)
- rejected:失败状态,不可转变状态,且有一个不可变的失败原因(reason)
then方法:参数包含两个函数
- onFulfilled - 成功时执行的函数【参数是上一次返回的成功值】
- onRejected - 失败时执行的函数【参数是上一次返回的失败原因】
all方法:返回一个Promise,Promise返回一个结果是所有promise的结果数组
race方法:返回一个Promise,Promise返回一个结果值【最快响应的那个Promise的值】
Generator函数就是一个状态机,内部包含多个状态
- yield作为一种暂停标志
- next方法可以访问下一个状态
Async:Generator的语法糖,是一个通过异步执行并隐式返回Promise对象的函数
async对generator的改进:
- 内置执行器
- 返回Promise
- await后面可以是Promise,也可以是原始值
- 更好的语义
异步处理,是一个常考的命题。Promise,经常被用在开发环境中,了解其原理是必不可少的。面试中,也会被要求去实现一个简单的Promise或者Promise方法。async和await也会被问及,它的实现原理也是需要掌握的。具体实现部分可以查看【面试系列——手写代码实现(一)】
模块化
常见的模块化方案:
CommonJS
- 一个文件就是一个模块,通过执行该文件来加载模块
- 每个模块内部,module变量代表当前模块,这个变量就是一个对象,exports是它对外的接口
- require命令第一次加载该脚本时,就会执行整个脚本,在内存中生成一个对象(多次加载,只有第一次会被运行,结果被缓存)
- 特点:
- 所有代码都运行在模块作用域,不会污染全局作用域
- 独立性是模块最重要的特点,模块内部最好不会和程序其他部分直接交互
- 模块多次加载,只有第一次会被执行,并将结果会被缓存下来;如果想要重新执行,就得清除缓存
- 模块加载的顺序,按照其在代码中的位置
- CommonJS,它是同步加载,不适合浏览器
AMD
- 它采用异步的方式加载模块,模块的加载不会影响它后面语句的运行。所有依赖这个模块的代码,都会被放到回调函数里面去
- AMD通过define的方式来定义模块
define(id?, dependencies?, factory);
- 使用时,依然通过require关键词
require([module], callback);
CMD
- 它是seajs所推广的一种模块化方法
- 与AMD的区别是:
- 对于依赖的模块,AMD是提前执行的,CMD是延迟执行的;【requirejs2.0,也改成了延迟执行】
- AMD推崇依赖前置,CMD推崇就近依赖
ES6 Module
- 设计思想:尽量的静态化,使得在编译的时候,就能清楚模块之间的依赖关系,以及输入和输出的变量。【AMD和CMD,都是在运行时确定的】
- 通过import和export关键词组成,export作为对外接口,import是输入其他模块的功能
- 还有一个export default命令用于指定模块的默认输出【一个模块只有一个默认输出】
ES6 Module 和 CommonJS 的区别:
- 前者是值的引用,后者是值的拷贝
- 前者是编译时输出接口,后者是运行时加载
require的性能问题:由于它是值的拷贝,比较占用内存
CommonJS和ES6 Module是考察的重点,尤其是它们之间的区别。本篇概述的比较浅显,可以看一些详解的文章,详细了解一下。
防抖和节流
防抖:事件触发n秒后,执行回调,如果在这n秒内再次被触发,就重新计时
- 实现:
- 返回一个函数,函数内部会去清除计时器
- 然后重新执行setTimeout进行计时
节流:事件在一段时间内只会被触发一次,如果多次触发,只有一次生效。
- 实现:
- 返回一个函数,函数内部定义一个last【上回执行的时间】
- 获取当前时间now,来通过last+delay的比较,看是否执行回调
防抖和节流是日常开发工作也经常会使用的优化方法,面试必考题。面试者需要对其实现原理,聊熟于心,能够熟练编写代码。详见【面试系列——手写代码实现(一)】
函数式编程
特性:
- 函数是一等公民——函数可以跟其他变量一样,作为其他函数的输入输出
- 不可变量
- 纯函数——没有副作用的函数,不修改函数外部的变量
- 引用透明——同样的输入,必定是同样的输出
- 惰性计算
React就是函数式编程的典型代表,ReactView = render(data);
优势:
- 更好的状态管理——它的宗旨就是无状态的
- 更简单的复用
- 更加优雅的组合
高阶函数:只要满足以下两点中的一点即可
- 接受一个函数或者多个函数作为输入
- 输出一个函数
函数柯里化:就是部分求值,将使用多个参数的函数转出一系列一个参数的函数的方式,并接受剩余参数的函数
函数式编程,近些年来挺火的。虽然JavaScript名义上不是一种函数式编程语言,但是这种方式,可以提高编码效率;整体的上手成本还是比较高的,而且深涩难懂。但是未来,函数式编程仍然会持续火爆。
总结
最后希望分享的内容,对你的面试能有帮助。这些知识点只是浅显的概述,仍然需要你去看一些文章,深入理解。网上也有很多深入讲解的系列,比如冴羽老师的。