【精简版】前端面试宝典( JavaScript / TypeScript )

3,038 阅读49分钟

前言

【精简版】前端面试宝典( JavaScript / TypeScript ),精简前端各个模块的知识点,方便熟记

JavaScript

一、JavaScript基础

JavaScript 是一种直译式脚本语言,是一种动态类型弱类型基于原型的语言,内置支持类型。它的解释器被称为JavaScript引擎,为浏览器的一部分,广泛用于客户端的脚本语言,最早是在HTML(标准通用标记语言下的一个应用)网页上使用,用来给HTML网页增加动态功能。

1. 强类型语言 和 弱类型语言

强类型(定义)语言弱类型(定义)语言
定义要求变量的使用要严格符合定义,所有变量都必须先定义后使用与强类型定义相反
语言Java、C++JavaScript

对比:强类型语言在速度上可能略逊色于弱类型语言,但是强类型语言带来的严谨性可以有效地帮助避免许多错误。

2. 解释性语言 和 编译型语言

编译型语言解释型语言
相关语言C、C++JavaScript、Python
原理通过专门的编译器,将所有源代码一次性转换成特定平台(Windows、Linux 等)执行的机器码,并包装成该平台所能识别的可执行性程序的格式。在编译型语言写的程序执行之前,需要一个专门的编译过程,把源代码编译成机器语言的文件由专门的解释器,根据需要将部分源代码临时转换成特定平台的机器码并立即执行。是代码在执行时才被解释器一行行动态翻译和执行,而不是在执行之前就完成翻译。解释型语言不需要事先编译,其直接将源代码解释成机器码并立即执行,所以只要某一平台提供了相应的解释器即可运行该程序。
优点编译一次后,脱离了编译器也可以运行,并且运行效率高。跨平台性好,通过不同的解释器,将相同的源代码解释成不同平台下的机器码,方便移植
缺点与特定平台相关,可移植性差,不够灵活。每次运行都需要将源代码解释称机器码并执行,效率较低。

3. 数据类型

八种数据类型:Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt

  • Symbol(ES6新增):表示独一无二且不可变的数据类型,为了解决可能出现的全局变量冲突的问题
  • BigInt(ES10新增):一种数字类型的数据,它可以表示任意精度格式的整数

(1) 原始数据类型 和 引用数据类型

  • 栈(stack):原始数据类型(Undefined、Null、Boolean、Number、String、Symbol、BigInt)
  • 堆(heap):引用数据类型(对象、数组和函数)

区别在于存储位置的不同:

  • 原始数据类型直接存储在栈中的内存中,占据空间小、大小固定,属于被频繁使用数据;
  • 引用数据类型:存储在堆中的对象,占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。

:一种非连续的树形存储树形结构,每个节点有一个值,整科树是经过排序的。

:一种连续储存的数据结构,具有先进后出的性能.

(2) null 和 undefined 区别

  • undefined 代表的含义是未定义,null 代表的含义是空对象。一般变量声明了但还没有定义的时候会返回 undefined,null主要用于赋值给一些可能会返回对象的变量,作为初始化。
  • 当对这两种类型使用 typeof 进行判断时,Null 类型化会返回 “object”,这是一个历史遗留的问题。
Null == undefined  //true
Null === undefined  //false
  • ==:判断值相等
  • ===:严格判断,直接判断两者类型是否相同

(3) 数据类型检测方式

(1) typeof:其中数组、对象、null都会被判断为object,其他判断都为相应的类型。注意

typeof null // object
typeof undefined // undefined
typeof NaN; // "number"
NaN !== NaN // true

(2) instanceof:只能正确判断引用数据类型,而不能判断基本数据类型,其内部运行机制是判断在其原型链中能否找到该类型的原型,即判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。

console.log(2 instanceof Number); // false
console.log(true instanceof Boolean); // false
console.log([] instanceof Array); // true
console.log(function(){} instanceof Function); // true
console.log({} instanceof Object); // true

(3) constructor:有两个作用:

  • 判断数据的类型

      console.log((2).constructor === Number); // true
      console.log((true).constructor === Boolean); // true 
      console.log(('str').constructor === String); // true 
      console.log(([]).constructor === Array); // true 
      console.log((function() {}).constructor === Function); // true 
      console.log(({}).constructor === Object); //true
    
  • 对象实例通过 constrcutor 对象访问它的构造函数。需要注意,如果创建一个对象来改变它的原型,constructor就不能用来判断数据类型了:

    function Fn(){}; 
    Fn.prototype = new Array();
    var f = new Fn();
    console.log(f.constructor===Fn); // false
    console.log(f.constructor===Array); // true
    

(4) Object.prototype.toString.call() Object.prototype.toString.call() 使用 Object 对象的原型方法 toString 来判断数据类型:

var a = Object.prototype.toString;
console.log(a.call(2));
console.log(a.call(true));
console.log(a.call('str'));
console.log(a.call([]));
console.log(a.call(function(){}));
console.log(a.call({}));
console.log(a.call(undefined));
console.log(a.call(null));

(4) 判断数组的方式

  • Object.prototype.toString.call()
  • 原型链(__proto__)
  • ES6的Array.isArray()
  • instanceof
  • Array.prototype.isPrototypeOf

typeof

typeof null 的结果是 Object 原因

在 JavaScript 第一个版本中,所有值都存储在 32 位的单元中,每个单元包含一个小的 类型标签(1-3 bits) 以及当前要存储值的真实数据。类型标签存储在每个单元的低位中,共有五种数据类型:

000: object 当前存储的数据指向一个对象。

1: int 当前存储的数据是一个 31 位的有符号整数

010: double 当前存储的数据指向一个双精度的浮点数

100: string 当前存储的数据指向一个字符串。

110: boolean 当前存储的数据是布尔值。

有两种特殊数据类型:

  • undefined的值是 (-2)30(一个超出整数范围的数字);
  • null 的值是机器码 NULL 指针(null 指针的值全是 0)

那也就是说null的类型标签也是000,和Object的类型标签一样,所以会被判定为Object。

为什么 0.1+0.2 ! == 0.3

console.log( 0.1 + 0.2) //0.30000000000000004

原因:计算机是通过二进制来进行计算的,即 0 和 1。 就拿 0.1 + 0.2 来说,0.1表示为0.0001100110011001...,而0.2表示为0.0011001100110011...。简单来说就是,浮点数转成二进制时丢失了精度,因此在二进制计算完再转回十进制时可能会和理论结果不同

解决方案

  • toFixed():可以控制小数点后几位,如果为空的话会用0补充,返回一个字符串。缺点:在不同浏览器中得出的值可能不相同,且部分数字得不到预计的结果,并不是执行严格的四舍五入。
  • 乘以一个10的幂次方:把需要计算的数字乘以10的n次方,让数值都变为整数,计算完后再除以10的n次方。缺点:JS中的存储都是通过8字节的double浮点类型表示的,它存在一个数值范围,因此它并不能准确记录所有数字,超出这个范围的话JS是无法表示的。虽然范围有限制,但是数值一般都够用。

new的执行过程

执行过程

  1. 首先创建了一个新的空对象
  2. 设置原型,将对象的原型设置为函数的 prototype 对象。
  3. 让函数的 this 指向这个对象,执行构造函数的代码,为这个新对象添加属性
  4. 判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。

JSON的理解

JSON 是一种基于文本的轻量级的数据交换格式,它可以被任何的编程语言读取和作为数据格式来传递。

  • JSON.stringify 函数:JSON 格式的数据结构转换为JSON 字符串
  • JSON.parse() 函数:JSON 格式的字符串转换为js数据结构

JS 脚本延迟加载的方式

延迟加载就是等页面加载完成之后再加载 JavaScript 文件。 js 延迟加载有助于提高页面加载速度

方式

  • defer 属性:这个属性会让脚本的加载与文档的解析同步解析,然后在文档解析完成后再执行这个脚本文件,这样的话就能使页面的渲染不被阻塞
  • async 属性:这个属性会使脚本异步加载,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行 js 脚本,这个时候如果文档没有解析完成的话同样会阻塞
  • 动态创建 DOM 方式
  • 使用 setTimeout 延迟方法
  • 将 js 脚本放在文档的底部,让 JS 最后加载

原码、补码、反码

原码:原码就是一个数的二进制数

反码:正数的反码与原码相同;负数的反码为除符号位,按位取反,即0变1,1变0。

(3)补码

  • 正数的补码与原码相同,如:10 补码为 0000 1010
  • 负数的补码是原码除符号位外的所有位取反即0变1,1变0,然后加1,也就是反码加1。

DOM 和 BOM

DOM :文档对象模型,指的是把文档当做一个对象来对待,这个对象主要定义了处理网页内容的方法和接口。

BOM :浏览器对象模型,它指的是把浏览器当做一个对象来对待,这个对象主要定义了与浏览器窗口进行交互的对象,其核心对象是 window。

常见的DOM操作

getElementById // 按照 id 查询 
getElementsByTagName // 按照标签名查询
getElementsByClassName // 按照类名查询 
querySelectorAll // 按照 css 选择器查询
createElement // 创建新节点
innerHTML // 节点内容
appendChild // 添加新节点
removeChild // 删除节点
insertBefore(_,_) // 交换节点内容 

use strict(严格模式)

use strict 是一种 ECMAscript5 添加的(严格模式)运行模式,这种模式使得 Javascript 在更严格的条件下运行。

目的

  • 消除 Javascript 语法的不合理、不严谨之处,减少怪异行为;
  • 消除代码运行的不安全之处,保证代码运行的安全;
  • 提高编译器效率,增加运行速度;
  • 为未来新版本的 Javascript 做好铺垫。

区别

  • 禁止使用 with 语句。
  • 禁止 this 关键字指向全局对象。
  • 对象不能有重名的属性。

ajax、axios、fetch 的区别

AJAX

AJAX(Asynchronous JavaScript and XML),是指一种创建交互式网页应用的网页开发技术,指的是通过 JavaScript 的 异步通信,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。

创建AJAX请求的步骤

  • 创建一个 XMLHttpRequest 对象。
  • 在这个对象上使用 open 方法创建一个 HTTP 请求,open 方法所需要的参数是请求的方法、请求的地址、是否异步和用户的认证信息。
  • 在发起请求前,可以为这个对象添加一些信息和监听函数。比如说可以通过 setRequestHeader 方法来为请求添加头信息。还可以为这个对象添加一个状态监听函数。一个 XMLHttpRequest 对象一共有 5 个状态,当它的状态变化时会触发onreadystatechange 事件,可以通过设置监听函数,来处理请求成功后的结果。当对象的 readyState 变为 4 的时候,代表服务器返回的数据接收完成,这个时候可以通过判断请求的状态,如果状态是 2xx 或者 304 的话则代表返回正常。这个时候就可以通过 response 中的数据来对页面进行更新了。
  • 当对象的属性和监听函数设置完成后,最后调用 sent 方法来向服务器发起请求,可以传入参数作为发送的数据体。

缺点

  • 本身是针对MVC编程,不符合前端MVVM的浪潮
  • 基于原生XHR开发,XHR本身的架构不清晰
  • 不符合关注分离(Separation of Concerns)的原则
  • 配置和调用方式非常混乱,而且基于事件的异步模型不友好。

Fetch

fetch号称是AJAX的替代品,是在ES6出现的,使用了ES6中的promise对象。Fetch是基于promise设计的。Fetch的代码结构比起ajax简单多。fetch不是ajax的进一步封装,而是原生js,没有使用XMLHttpRequest对象。

优点

  • 语法简洁,更加语义化
  • 基于标准 Promise 实现,支持 async/await
  • 更加底层,提供的API丰富(request, response)
  • 脱离了XHR,是ES规范里新的实现方式

缺点

  • fetch只对网络请求报错,对400,500都当做成功的请求,服务器返回 400,500 错误码时并不会 reject,只有网络错误这些导致请求不能完成时,fetch 才会被 reject。
  • fetch默认不会带cookie,需要添加配置项: fetch(url, {credentials: 'include'})
  • fetch不支持abort,不支持超时控制,使用setTimeout及Promise.reject的实现的超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费
  • fetch没有办法原生监测请求的进度,而XHR可以

Axios

Axios 是一个基于 promise 网络请求库,作用于node.js 和浏览器中。 它是 isomorphic 的(即同一套代码可以运行在浏览器和node.js中)。在服务端它使用原生 node.js http 模块, 而在客户端 (浏览端) 则使用 XMLHttpRequests,其特点如下:

  • 浏览器端发起XMLHttpRequests请求
  • node端发起http请求
  • 支持Promise API
  • 监听请求和返回
  • 对请求和返回进行转化
  • 取消请求
  • 自动转换json数据
  • 客户端支持抵御XSRF攻击

深拷贝 和 浅拷贝

浅拷贝:将原对象或原数组的引用直接赋给新对象,新数组,新对象知识对原对象的一个引用,而不复制对象本身,新旧对象还是共享同一块内存。修改新拷贝的值会影响原来的值。如果属性是一个基本数据类型,拷贝就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址。

浅拷贝方式

  • Object.assign(target, ...sources)
  • lodash 里面 _.clone
  • 使用拓展运算符(...)
  • Array.prototype.slice()
  • Array.prototype.concat()

深拷贝:把一个对象,从内存中完整的拷贝出来,从堆内存中开辟了新区域,用来存新对象,并且修改新对象不会影响原对象。

深拷贝方式

  • JSON.parse(JSON.stringify())
  • 循环递归
  • _.cloneDeep()
  • jQuery.extend()

for、for in、for of、forEach 和 map方法

  • for in:可以循环数组和对象,推荐对象的时候使用
  • for of:ES6 新引入的特性。它既比传统的for循环简洁,同时弥补了forEach和for-in循环的短板,for of无法循环遍历普通对象,for in 会遍历自定义属性,for of不会。推荐数组的时候使用
  • forEach():方法会针对每一个元素执行提供的函数,对数据的操作会改变原数组,该方法没有返回值;
  • map():不改变原数组,返回一个新数组,新数组中的值为原数组调用函数处理之后的值,不会对空数组检测
  • filter():不改变原数组,创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素

节流与防抖

节流:指连续触发事件但是在 n 秒中只执行一次函数。两种方式可以实现,分别是时间戳版和定时器版

防抖:指触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间

使用场景

  • 节流:滚动加载更多、搜索框搜的索联想功能、高频点击、表单重复提交
  • 防抖:搜索框搜索输入,并在输入完以后自动搜索、手机号,邮箱验证输入检测、窗口大小 resize 变化后,再重新渲染。

二、JavaScript 代码执行顺序

微任务 / 宏任务

异步队列中包括:微任务(micro-task) 和 宏任务(macro-task)

微任务process.nextTickPromiseprocess.nextTick 为 Node 独有)

宏任务scriptsetTimeoutsetIntervalsetImmediateI/OUI rendering

Tips

  • 微任务优先级高于宏任务的前提是:同步代码已经执行完成。因为 script 属于宏任务,程序开始后会首先执行同步脚本,也就是script 。
  • Promise 里边的代码属于同步代码,.then() 中执行的代码才属于异步代码。

Event Loop(事件轮询)

Event Loop 是一个程序结构,用于等待和发送消息和事件。

Event Loop 执行顺序如下所示:

  • 首先执行同步代码(宏任务)
  • 当执行完所有同步代码后,执行栈为空,查询是否有异步代码需要执行
  • 执行所有微任务
  • 当执行完所有微任务后,如有必要会渲染页面
  • 然后开始下一轮 Event Loop,执行宏任务中的异步代码,也就是 setTimeout 中的回调函数

Tips:简化讲:先执行一个宏任务(script同步代码),然后执行并清空微任务,再执行一个宏任务,然后执行并清空微任务,再执行一个宏任务,再然后执行并清空微任务......如此循环往复(一个宏任务 -> 清空微任务 -> 一个宏任务 -> 清空微任务)

三、ES6

1. var、let 和 const

  • 块级作用域:块作用域由 { }包括,let和const具有块级作用域,var不存在块级作用域。块级作用域解决了ES5中的两个问题:
    • 内层变量可能覆盖外层变量
    • 用来计数的循环变量泄露为全局变量
  • 变量提升 :var存在变量提升,let和const不存在变量提升,即在变量只能在声明之后使用,否在会报错。
  • 给全局添加属性:浏览器的全局对象是window,Node的全局对象是global。var声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是let和const不会。
  • 重复声明:var声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。const和let不允许重复声明变量。
  • 暂时性死区:在使用let、const命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。使用var声明的变量不存在暂时性死区。
  • 初始值设置:在变量声明时,var 和 let 可以不用设置初始值。而const声明变量必须设置初始值。
  • 指针指向:let和const都是ES6新增的用于创建变量的语法。 let创建的变量是可以更改指针指向(可以重新赋值)。但const声明的变量是不允许改变指针的指向。

注意:const是常量,一旦声明,常量的值就不能改变。但const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的,至于它指向的数据结构是不是可变的,就完全不能控制了。

变量提升

变量提升的表现是,无论在函数中何处位置声明的变量,好像都被提升到了函数的首部,可以在变量声明前访问到而不会报错。

造成变量声明提升的本质原因:是 js 引擎在代码执行前有一个解析的过程,创建了执行上下文,初始化了一些代码执行时需要用到的对象。当访问一个变量时,会到当前执行上下文中的作用域链中去查找,而作用域链的首端指向的是当前执行上下文的变量对象,这个变量对象是执行上下文的一个属性,它包含了函数的形参、所有的函数和变量声明,这个对象的是在代码解析的时候创建的。JS在拿到变量或者函数的时候,会有进行解析和执行

  • 在解析阶段,JS会检查语法,并对函数进行预编译。
  • 在执行阶段,就是按照代码的顺序依次执行。

进行变量提升原因:提高性能、容错性更好

2. 箭头函数

简写形式()=>{}

概念箭头函数表达式的语法比函数表达式更简洁,并且没有自己的thisargumentssupernew.target,也没有prototype属性。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数,和 new一起用会抛出错误。

和普通函数区别

(1) 箭头函数比普通函数更加简洁

(2) 箭头函数没有自己的this,它只会在自己作用域的上一层继承this,且继承来的this指向永远不会改变,call()、apply()、bind()等方法。

(3) 缺少caller - 无法确定上下文

(4) 不能作为构造函数使用

(5) 没有自己的arguments,但是可以访问外围函数的arguments对象

(6) 没有原型prototype

(7) 不能用作Generator函数,不能使用yeild关键字

3. 模板字符串

ES6新增了模板字符串,用反引号(``)表示,可以用于定义多行字符串,或者在字符串中嵌入变量。如果要在模板字符串中嵌入变量,需要将变量名写在${}之中。

4. for...of 循环

for…of 是作为ES6新增的遍历方式,允许遍历一个含有iterator接口的数据结构并且返回各项的值。

for...in和for...of的区别

for…of 是ES6新增的遍历方式,允许遍历一个含有iterator接口的数据结构(数组、对象等)并且返回各项的值,和ES3中的for…in的区别如下

  • for…of 遍历获取的是对象的键值,for…in 获取的是对象的键名;
  • for… in 会遍历对象的整个原型链,性能非常差不推荐使用,而 for … of 只遍历当前对象不会遍历原型链;
  • 对于数组的遍历,for…in 会返回数组中所有可枚举的属性(包括原型链上可枚举的属性),for…of 只返回数组的下标对应的属性值;

总结: for...in 循环主要是为了遍历对象而生,不适用于遍历数组;for...of 循环可以用来遍历数组、类数组对象,字符串、Set、Map 以及 Generator 对象。

ES6中的类 class

ES6 引入了class(类),让JavaScript的面向对象编程变得更加简单和易于理解。比如说,构造函数、继承、super、this之类的都跟Java、C++很像。

  • class类的出现弥补了之前用函数模拟类的不足。
  • class类能够使用extnds实现继承。
  • class内部只有静态方法没有静态属性 用static函数对象实现静态方法。

Promise

  • 详情见对Promise的理解

对象属性/方法简写

  • es6允许当对象的属性和值相同时,省略属性名
  • es6允许当一个对象的属性的值是一个方法时,可以使用简写的形式。省略了:function

Module模块化

  • import导入,export导出。ES6 Module是静态的,也就是说它是在编译阶段运行,和var以及function一样具有提升效果。

  • import()动态加载 把import作为一个函数可以实现动态加载模块,它返回一个Promise,Promise被resolve时的值为输出的模块。Vue中路由的懒加载的ES6写法就是使用了这个技术,使得在路由切换的时候能够动态的加载组件渲染视图。

解构赋值

从数组中提取值,按照对应位置,对变量赋值。对象解构原理的理解是,通过键找键,找到了相同的属性名就赋值了。

扩展运算符 / 剩余运算符rest

扩展运算符(spread)

扩展运算符也叫圆点运算符(…),以数组为例,使用扩展运算符可以"展开"一个数组,可以把这些元素集合放到另外一个数组里面。

作用

  • (1) 对象扩展运算符

对象的扩展运算符(...)用于取出参数对象中的所有可遍历属性,拷贝到当前对象之中。

let baz = { ...bar }; // { a: 1, b: 2 } 
// 等价于
let baz = Object.assign({}, bar); // { a: 1, b: 2 }

注意:扩展运算符对对象实例的拷贝属于浅拷贝。

  • (2) 数组扩展运算符

数组的扩展运算符可以将一个数组转为用逗号分隔的参数序列,且每次只能展开一层数组。

数组扩展运算符的应用

  • 将数组转换为参数序列

  • 复制数组

    注意:扩展运算符(…)用于取出参数对象中的所有可遍历属性,拷贝到当前对象之中,这里参数对象是个数组,数组里面的所有对象都是基础数据类型,将所有基础数据类型重新拷贝到新的数组中。

  • 合并数组

  • 扩展运算符与解构赋值结合起来,用于生成数组

    注意:如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。

  • 将字符串转为真正的数组

  • 任何 Iterator 接口的对象,都可以用扩展运算符转为真正的数组

  • 使用Math函数获取数组中特定的值

剩余运算符(rest)

消耗3个点后面的数组的所有迭代器,读取后面所有迭代器的value属性,放到右边的数组中。

运算符的扩展

指数运算符(**

2 ** 3 // 8
2 ** 3 ** 2 // 相当于 2 ** (3 ** 2) 512
let b = 4; b **= 3; // 等同于 b = b * b * b;

链判断运算符

  • 三元运算符?:也常用于判断对象是否存在
  • 链判断运算符?.:直接在链式调用的时候判断,左侧的对象是否为nullundefined。如果是,不往下运算而返回undfined

Null 判断运算符

  • 通过 || 运算符指定默认值,只要属性的值为nullundefined,默认值就会生效,但是属性的值如果为空字符串或 false0,默认值也会生效。
  • 为了避免这种情况,ES2020 引入了一个新的 Null 判断运算符 ??。它的行为类似 ||,但是只有运算符左侧的值为 nullundefined时,才会返回右侧的值。

逻辑赋值运算符

ES2021 引入了三个新的逻辑赋值运算符,将逻辑运算符与赋值运算符进行结合。

// 或赋值运算符
x ||= y // 等同于 x || (x = y)

// 与赋值运算符
x &&= y // 等同于 x && (x = y)

// Null 赋值运算符
x ??= y // 等同于 x ?? (x = y)

Set 和 Map 数据结构

Set

  • set能够创建一个没有重复元素的集合。
  • set的用途:Array数组的去重问题,但需要Array.from的配合 与其他去重方法对比有代码简化的优势。

实例方法

  • add(value):添加某个值,返回 Set 结构本身
  • delete(value):删除某个值,返回一个布尔值,表示删除是否成功
  • has(value):返回一个布尔值,表示该值是否为 Set 的成员
  • clear():清除所有成员,没有返回值

Map

本质上是键值对的集合(Hash结构),它的键和值可以是任何数据类型。

实例方法

  • size:返回Map结构的成员总数
  • set(key,value):设置键名key对应的键值为value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键
  • get(key):读取key对应的键值,如果找不到key,返回undefined

遍历方法

Map结构原生提供三个遍历器生成函数和一个遍历方法。

  • .keys():返回键名的遍历器
  • .values():返回键值的遍历器
  • .entries():返回所有成员的遍历器
  • .forEach():遍历 Map 的所有成员

WeakMap

WeakMap结构与Map结构类似,也是用于生成键值对的集合。

四、原型与原型链

1. 原型

在 JS 中,每当定义一个对象(函数也是对象)时,对象中都会包含一些预定义的属性。其中每个函数对象都有一个prototype 属性,这个属性指向函数的原型对象

2. constructor 构造函数

对象原型(__proto__)和构造函数原型对象(prototype)里面都有一个属性 constructor 属性 ,constructor 称为构造函数,因为它指回构造函数本身。 constructor 主要用于记录该对象引用于哪个构造函数,它可以让原型对象重新指向原来的构造函数。 一般情况下,对象的方法都在构造函数的原型对象中设置。如果有多个对象的方法,我们可以给原型对象采取对象形式赋值,但是这样就会覆盖构造函数原型对象原来的内容,这样修改后的原型对象 constructor 就不再指向当前构造函数了。此时,我们可以在修改后的原型对象中,添加一个 constructor 指向原来的构造函数。

3. 原型链

原型链:函数的原型链对象 constructor 默认指向函数本身,原型对象除了有原型属性外,为了实现继承,还有一个原型链指针__proto__,该指针是指向上一层的原型对象,而上一层的原型对象的结构依然类似。因此可以利用__proto__一直指向Object的原型对象上,而Object原型对象用Object.prototype.__ proto__ = null表示原型链顶端。如此形成了js的原型链继承。

image.png

如何形成口述回答

  • 在 JavaScript 中,每个对象中都有一个__proto__属性,这个属性指向了当前对象的构造函数的原型。
  • 对象可以通过自身的__proto__属性与它的构造函数的原型对象连接起来,
  • 而因为它的原型对象也有__proto__,因此这样就串联形成一个链式结构,也就是我们称为的原型链。

五、执行上下文 / 作用域链 / 闭包

1. 执行上下文

类型

  • 全局执行上下文: 代码开始执行时首先进入的环境。
  • 函数执行上下文:函数调用时,会开始执行函数中的代码。
  • eval函数执行上下文:不常使用,可忽略。

阶段创建阶段执行阶段

创建阶段

(1)this绑定

  • 在全局执行上下文中,this指向全局对象(window对象)
  • 在函数执行上下文中,this指向取决于函数如何调用。如果它被一个引用对象调用,那么 this 会被设置成那个对象,否则 this 的值被设置为全局对象或者 undefined

(2)创建词法环境组件

  • 词法环境是一种有标识符——变量映射的数据结构,标识符是指变量/函数名,变量是对实际对象或原始数据的引用。
  • 词法环境的内部有两个组件:加粗样式:环境记录器:用来储存变量个函数声明的实际位置外部环境的引用:可以访问父级作用域

(3)创建变量环境组件

  • 变量环境也是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系。

执行阶段

此阶段会完成对变量的分配,最后执行完代码。

简单来说执行上下文就是指:

在执行一点JS代码之前,需要先解析代码。解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为undefined,函数先声明好可使用。这一步执行完了,才开始正式的执行程序。

在一个函数执行之前,也会创建一个函数执行上下文环境,跟全局执行上下文类似,不过函数执行上下文会多出this、arguments和函数的参数。

  • 全局上下文:变量定义,函数声明
  • 函数上下文:变量定义,函数声明,thisarguments

2. 作用域和作用域链

作用域:是定义变量的区域,它有一套访问变量的规则,这套规则来管理浏览器引擎如何在当前作用域以及嵌套的作用域中根据变量(标识符)进行变量查找。分为 全局作用域 和 函数作用域

1)全局作用域和函数作用域

(1) 全局作用域

  • 最外层函数和最外层函数外面定义的变量拥有全局作用域
  • 所有未定义直接赋值的变量自动声明为全局作用域
  • 所有window对象的属性拥有全局作用域
  • 全局作用域有很大的弊端,过多的全局作用域变量会污染全局命名空间,容易引起命名冲突。

(2) 函数作用域

  • 函数作用域声明在函数内部的变零,一般只有固定的代码片段可以访问到
  • 作用域是分层的,内层作用域可以访问外层作用域,反之不行

2)块级作用域

  • 使用ES6中新增的let和const指令可以声明块级作用域,块级作用域可以在函数中创建也可以在一个代码块中的创建(由{ }包裹的代码片段)
  • let和const声明的变量不会有变量提升,也不可以重复声明
  • 在循环中比较适合绑定块级作用域,这样就可以把声明的计数器变量限制在循环内部。

作用域链: 在当前作用域中查找所需变量,但是该作用域没有这个变量,那这个变量就是自由变量。如果在自己作用域找不到该变量就去父级作用域查找,依次向上级作用域查找,直到访问到window对象就被终止,这一层层的关系就是作用域链。

作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,可以访问到外层环境的变量和函数。

作用域链的本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。作用域链的前端始终都是当前执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象。

当查找一个变量时,如果当前执行环境中没有找到,可以沿着作用域链向后查找。

3. 闭包

定义:指有权访问另一个函数作用域中变量的函数

口述回答:在JS中,变量的作用域属于函数作用域,在函数执行完毕之后,它的作用域会被销毁、内传也会被回收,但由于闭包在函数内部创建一个子函数,且子函数可访问父函数中的作用域,即使父函数执行完,作用域也不会被销毁,这就是闭包。

使用原因:Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量 但是在函数外部自然无法读取函数内的局部变量。出于种种原因,我们有时候需要得到函数内的局部变量。那就是在函数的内部,再定义一个函数。

作用

  1. 在函数外部能够访问到函数内部的变量,可以使用闭包来创建私有变量
  2. 使已经运行结束的函数上下文中的变量对象继续留在内存中,阻止变量被回收

用途

  1. 模仿块级作用域
  2. 保护外部函数的变量 能够访问函数定义时所在的词法作用域(阻止其被回收)
  3. 封装私有化变量
  4. 创建模块

优点:简单好用,延长局部变量的生命周期

缺点:比普通函数更加占用内存,过多可能会导致内存泄漏

async/await

它是一种建立在Promise之上的编写异步或非阻塞代码的新方法,被普遍认为是JS异步操作的最终且最优雅的解决方案。相对于 Promise 和回调,它的可读性和简洁度都更高。相比于 Promiseasync/await能更好地处理 then 链

async 是异步的意思,而 awaitasync wait的简写,即异步等待。

所以从语义上就很好理解 async 用于声明一个 function 是异步的,而await 用于等待一个异步方法执行完成。

一个函数如果加上 async ,那么该函数就会返回一个Promise如果在 async 函数中直接 return 一个直接量,async 会把这个直接量通过 PromIse.resolve()封装成Promise对象返回。

优缺点:

async/await的优势在于处理 then 的调用链,能够更清晰准确的写出代码,并且也能优雅地解决回调地狱问题。当然也存在一些缺点,因为 await 将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 await 会导致性能上的降低。

五、继承

继承概念:就是子类可以使用父类的所有功能,并且对这些功能进行扩展

种类

  1. 原型链继承
  2. 构造继承
  3. 组合继承
  4. 寄生组合继承
  5. 原型式继承
  6. 寄生继承
  7. 混入式继承
  8. class中的extends继承

原型链继承

将子类的原型对象指向父亲的实例

五、this/call/apply/bind

JS 中 this 的五种情况

  1. 默认绑定:全局环境中,this默认绑定到window
  2. 当函数作为对象的方法被调用时,this就会指向该对象
  3. 构造器调用,this指向返回的这个对象
  4. 箭头函数 箭头函数的this绑定看的是this所在函数定义在哪个对象下,就绑定哪个对象。如果有嵌套的情况,则this绑定到最近的一层对象上。
  5. 基于Function.prototype上的 apply 、 call 和 bind 调用模式,这三个方法都可以显示的指定调用函数的 this 指向。apply接收参数的是数组,call接受参数列表, bind方法通过传入一个对象,返回一个this 绑定了传入对象的新函数。这个函数的 this指向除了使用new 时会被改变,其他情况下都不会改变。若为空默认是指向全局对象window。

call、apply和bind区别:

  • apply:两个参数,第一个是this指向,第二个是函数接收参数,以数组的形式。如果第一个参数为null或者undefine,this默认指向window。
  • call:两个参数,第一个是this指向,第二个是参数列表。如果第一个参数为null或者undefine,this默认指向window。
  • bind:两个参数,第一个是this指向,第二个是参数列表。

相同点:作用相同,都是动态修改this指向;都不会修改原先函数的this指向。返回修改过的新函数

异同点

(1) 执行方式不同

  • call和apply是改变后页面加载之后就立即执行,临时改变this指向一次,是同步代码。
  • bind是异步代码,改变后不会立即执行,而是返回一个永久改变this指向的函数。

(2) 传参方式不同

  • call和bind传参是一个一个逐一传入,不能使用剩余参数的方式传参。
  • apply可以使用数组的方式传入的,只要是数组方式就可以使用剩余参数的方式传入。

(3) 修改this的性质不同

  • call、apply只是临时的修改一次,也就是call和apply方法的那一次;当再次调用原函数的时候,它的指向还是原来的指向。
  • bind是永久修改函数this指向,但是它修改的不是原来的函数;而是返回一个修改过后新的函数,此函数的this永远被改变了,绑定了就修改不了。

六、异步编程

1. 异步编程的实现方式

JS中的异步机制可分为:

  • 回调函数:有一个缺点是,多个回调函数嵌套的时候会造成回调函数地狱,上下两层的回调函数间的代码耦合度太高,不利于代码的可维护。
  • Promise:可以将嵌套的回调函数作为链式调用。但是这种方法,有时会造成多个 then 的链式调用,可能会造成代码的语义不够明确。
  • generator:它可以在函数的执行过程中,将函数的执行权转移出去,在函数外部还可以将执行权转移回来。当遇到异步函数执行的时候,将函数执行权转移出去,当异步函数执行完毕时再将执行权给转移回来
  • async 函数的方式:async 函数是 generator 和 promise 实现的一个自动执行的语法糖,它内部自带执行器,当函数内部执行到一个 await 语句的时候,如果语句返回一个 promise 对象,那么函数将会等待 promise 对象的状态变为 resolve 后再继续向下执行

setTimeout、Promise、Async/Await 的区别

(1)setTimeout

(2)Promise

Promise本身是同步的立即执行函数, 当在executor中执行resolve或者reject的时候, 此时是异步操作, 会先执行then/catch等,当主栈完成后,才会去调用resolve/reject中存放的方法执行,打印p的时候,是打印的返回结果,一个Promise实例。

当JS主线程执行到Promise对象时:

  • promise1.then() 的回调就是一个 task
  • promise1 是 resolved或rejected: 那这个 task 就会放入当前事件循环回合的 microtask queue
  • promise1 是 pending: 这个 task 就会放入 事件循环的未来的某个(可能下一个)回合的 microtask queue 中
  • setTimeout 的回调也是个 task ,它会被放入 macrotask queue 即使是 0ms 的情况

(3)async/await

async 函数返回一个 Promise 对象,但本身是同步的立即执行函数。当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。

await的含义为等待,也就是 async 函数需要等待await后的函数执行完成并且有了返回结果(Promise对象)之后,才能继续执行下面的代码。await通过返回一个Promise对象来实现同步的效果。

对 Promise 的理解

Promise 是异步编程的一种解决方案,它比传统的解决方案回调函数和事件更合理和更强大。

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。Promise的出现改变了JS的异步编程,现在基本上异步请求都是使用Promise实现。

特点

  • 状态不受外界的影响,只有异步操作的结果,决定当前是哪一种状态
  • 一旦状态改变就不会再变(pending -> fulfilled,pending -> rejected)
  • Promise的出现主要是为了解决回调地狱的问题

回调地狱就是多层嵌套的问题。 每种任务的处理结果存在两种可能性(成功或失败),那么需要在每种任务执行结束后分别处理这两种可能性,需要多次异步请求的话,就会显得代码跳跃且乱。

三个状态:pending(进行中)、fulfilled(已完成)、rejected(已拒绝)

用法:Promise是一个构造函数,用来生成Promise实例。Promise构造函数接收一个函数作为参数,这个函数有两个参数。

  • resolve函数:将Promise对象的状态从未完成->成功,异步操作成功时调用
  • reject函数:将Promise对象的状态从未完成->未成功,异步操作失败时调用

Promise的原型上定义了一个 then 方法, 分别是成功和失败的回调。我们可以使用这个 then 方法可以为两个状态的改变注册回调函数。这个回调函数属于微任务,会在本轮事件循环的末尾执行。

缺点

  • 无法取消Promise,一旦新建它就会立即执行,无法中途取消。
  • 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
  • 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

Promise方法

Promise有五个常用的方法:then()、catch()、all()、race()、finally()

then()then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。其中第二个参数可以省略。 then方法返回的是一个新的Promise实例(不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。

catch():Promise对象除了有then方法,还有一个catch方法,该方法相当于then方法的第二个参数,指向reject的回调函数。不过catch方法还有一个作用,就是在执行resolve回调函数时,如果出现错误,抛出异常,不会停止运行,而是进入catch方法中。

all()all方法可以完成并行任务, 它接收一个数组,数组的每一项都是一个promise对象。当数组中所有的promise的状态都达到resolved的时候,all方法的状态就会变成resolved,如果有一个状态变成了rejected,那么all方法的状态就会变成rejected。调用all方法时的结果成功的时候是回调函数的参数也是一个数组,这个数组按顺序保存着每一个promise对象resolve执行时的值。

race()race方法和all一样,接受的参数是一个每项都是promise的数组,但是与all不同的是,当最先执行完的事件执行完之后,就直接返回该promise对象的值。如果第一个promise对象状态变成resolved,那自身的状态变成了resolved;反之第一个promise变成rejected,那自身状态就会变成rejected

finally() finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。

对async/await 的理解

async/await其实是Generator 的语法糖,它能实现的效果都能用then链来实现,它是为优化then链而开发出来的。从字面上来看,async是“异步”的简写,await则为等待,所以很好理解async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。

对比Promise的优势

  • 代码读起来更加同步,Promise虽然摆脱了回调地狱,但是then的链式调⽤也会带来额外的阅读负担
  • Promise传递中间值⾮常麻烦,⽽async/await⼏乎是同步的写法,⾮常优雅
  • 错误处理友好,async/await可以⽤成熟的try/catch,Promise的错误捕获⾮常冗余
  • 调试友好,Promise的调试很差,由于没有代码块,你不能在⼀个返回表达式的箭头函数中设置断点,如果你在⼀个.then代码块中使⽤调试器的步进(step-over)功能,调试器并不会进⼊后续的.then代码块,因为调试器只能跟踪同步代码的每⼀步。

并发与并行的区别?

  • 并发是宏观概念,分别有任务 A 和任务 B,在一段时间内通过任务间的切换完成了这两个任务,这种情况就可以称之为并发。
  • 并行是微观概念,假设 CPU 中存在两个核心,那么我就可以同时完成任务 A、B。同时完成多个任务的情况就可以称之为并行。

七、事件

事件类型分两种:事件捕获、事件冒泡。

  • 事件捕获就是:由外往内,从事件发生的顶点开始,逐级往下查找,一直到目标元素。
  • 事件冒泡:由内往外,从具体的目标节点元素触发,逐级向上传递,直到根节点。

(1) 事件流

事件流:从页面中接收事件的顺序,DOM2级事件流包括下面几个阶段。

  • 事件捕获阶段
  • 处于目标阶段
  • 事件冒泡阶段

IE只支持事件冒泡。

(2) 什么是事件监听

addEventListener()方法,用于向指定元素添加事件句柄,它可以更简单的控制事件,语法为

element.addEventListener(event, function, useCapture);

  • 第一个参数是事件的类型(如 "click" 或 "mousedown").
  • 第二个参数是事件触发后调用的函数。
  • 第三个参数是个布尔值用于描述事件是冒泡还是捕获。该参数是可选的。

(3) mouseover 和 mouseenter 的区别

  • mouseover:当鼠标移入元素或其子元素都会触发事件,所以有一个重复触发,冒泡的过程。对应的移除事件是mouseout
  • mouseenter:当鼠标移除元素本身(不包含元素的子元素)会触发事件,也就是不会冒泡,对应的移除事件是mouseleave

(4) 事件委托

事件委托:又名事件代理,利用事件冒泡,父元素可以监听到子元素上事件的触发,通过判断事件发生元素DOM的类型,来做出不同的响应。

好处

  • 比较合适动态元素的绑定,新添加的子元素也会有监听函数,也可以有事件触发机制;
  • 提高性能,减少了事件绑定,从而减少内存占用

(5) 事件代理在捕获阶段的实际应用

可以在父元素层面阻止事件向子元素传播,也可代替子元素执行某些操作。

垃圾回收与内存泄漏

回收机制的概念:JS代码运行时,需要分配内存空间来储存变量和值,当变量、对象不在参与运行时,就需要系统收回被占用的内存空间。

回收机制的方法:标记清除,引用计数。

  • 标记清除:在JS中最常用,从根集合触发,标记出需要回收的对象,清除被标记的对象

  • 引用计数:记录每个对象被引用的次数,每次新建对象、赋值引用和删除引用的同时更新计数器,如果计数器值为0则直接回收内存。引用计数最大的优势是暂停时间短

减少垃圾回收方式:对数组、object、函数进行优化

内存泄露概念:程序中已经动态分配的堆内存,由于某些原因没有得到释放,造成系统内存的浪费导致程序运行速度减慢甚至系统崩溃等严重后果。

引发内存泄漏情况

  • 意外的全局变量
  • 被遗忘的计时器或回调函数
  • 脱离 DOM 的引用
  • 不合理使用闭包,导致某些变量一直被留在内存当中

如何避免

  • 减少不必要的全局变量,或者生命周期较长的对象,及时对无用的数据进行垃圾回收;
  • 注意程序逻辑,避免“死循环”之类的 ;
  • 避免创建过多的对象,不用了的东西要及时归还。

TypeScript

TS 优缺点

优点

  • 代码的可读性和可维护性:看后端某个接口返回值,一般需要去network看or去看接口文档,才知道返回数据结构,而正确用了ts后,编辑器会提醒接口返回值的类型,这点相当实用。
  • 编译阶段就发现大部分错误,避免了很多线上bug
  • 增强了编辑器和 IDE 的功能,包括代码补全接口提示跳转到定义重构

缺点

  • 有一定的学习成本,需要理解接口(Interfaces)、泛型(Generics)、类(Classes)、枚举类型(Enums)等前端工程师可能不是很熟悉的概念
  • 会增加一些开发成本,当然这是前期的,后期维护更简单了
  • 一些JavaScript库需要兼容,提供声明文件,像vue2,底层对ts的兼容就不是很好。
  • ts编译是需要时间的,这就意味着项目大了以后,开发环境启动和生产环境打包的速度就成了考验
  • 可以看看Deno 内部代码将停用 TypeScript,并公布五项具体理由

TS 相对于 JS 的优势

  • TS 是添加了类型系统的 JS,适用于任何规模的项目,增加了代码的可读性和可维护性
  • TS 是一门静态类型、弱类型的语言,它是完全兼容 JS 的,它不会修改 JS 运行时的特性

类型系统按照类型检查的时机来分类,分为:

  • 动态类型:在运行时才会进行类型检查,往往会导致运行时错误
  • 静态类型:指编译阶段就能确定每个变量的类型,往往会导致语法错误

JS 就是一门解释型语言,没有编译阶段,所以它是动态类型

  • TS 增强了编辑器(IDE)的功能,提供了代码补全、接口提示、跳转到定义、代码重构等能力
  • TS 与标准同步发展,符合最新的 ECMAScript 标准(stage 3)
  • TS 可以和 JS 共存,这意味着 JS 项目能够渐进式的迁移到 TS

TS类型

基础类型

  • 常用:boolean、number、string、array、enum、any、void
  • 不常用:tuple、null、undefined、never

对象类型

简单理解interface 和 type 的区别:type 更强大,interface 可以进行声明合并,type 不行;

数组类型

需要声明列表数据类型:

元组 tuple

元组和数组类似,但是类型注解时会不一样

赋值的类型、位置、个数需要和定义(生明)的类型、位置、个数一致。

联合| or 交叉&类型

  • 联合类型:某个变量可能是多个 interface 中的其中一个,用 | 分割
  • 交叉类型:由多个类型组成,用 & 连接

enum枚举

提高代码可维护性,统一维护某些枚举值,避免 JiShi === 1这种魔法数字。JiShi === JiShiEnum.BLUEJ这样写,老板一眼就知道我想找谁。

泛型 T(Type)

简单说就是:泛指的类型,不确定的类型,可以理解为一个占位符(使用T只是习惯,使用任何字母都行)

  • K(Key):表示对象中的键类型;
  • V(Value):表示对象中的值类型;
  • E(Element):表示元素类型。

断言

断言用来手动指定一个值的类型。值 as 类型 or <类型>值

注意在 tsx 语法中必须使用前者,即 值 as 类型。

in

在做类型保护时间,类似于数组和字符串的 includes 方法

也有遍历的作用,拿到ts类型定义的Key,获取Key还有个方法:keyof是取类型的key的联合类型 , in是遍历类型的key

类型注解

显式的告诉代码,我们的 count 变量就是一个数字类型,这就叫做类型注解

类型推断

  • 如果 TS 能够自动分析变量类型, 什么都不用做
  • 如果 TS 无法分析变量类型的话, 就需要使用类型注解

void和never

返回值类型,也算是基础类型。没有返回值的函数: void

如果一个函数是永远也执行不完的,就可以定义返回值为 never

一个函数有入参,也有出参

ts类里的关键字

了解ts关键字的作用,在写base类的时候可能会用到,个人用的不多。

  • public
  • private 类的外部不可用,继承也不行
  • protected 类的外部不可用,继承可以
  • public readOnly xxx 只读属性
  • static funcXXX 静态方法,不需要 new 就可以调用
  • abstract funcXXX 抽象类,所有子类都必须要实现 funcXXX

tsconfig

需要去了解 tsconfig.json 中一些参数的说明,具体参考官方文档tsconfig.json

作用

  • 用于标识 TypeScript 项目的根路径;
  • 用于配置 TypeScript 编译器;
  • 用于指定编译的文件。

注意事项

  • tsc -init 生成 tsconfig.json,项目目录下直接 tsc,编译的时候就会走配置文件
  • compilerOptions 内部字段含义 阿宝哥 这篇文章有详细说明
  • 项目别名配置:遇到过的一个坑,仅在项目config中配置别名不生效,需要在tsconfig.json中再配置一遍