面试系列——JavaScript基础篇

·  阅读 2819

前言

本篇是面试系列的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名义上不是一种函数式编程语言,但是这种方式,可以提高编码效率;整体的上手成本还是比较高的,而且深涩难懂。但是未来,函数式编程仍然会持续火爆。

总结

最后希望分享的内容,对你的面试能有帮助。这些知识点只是浅显的概述,仍然需要你去看一些文章,深入理解。网上也有很多深入讲解的系列,比如冴羽老师的。

分类:
前端
标签:
分类:
前端
标签: