1.JavaScript有哪些数据类型,它们的区别?
JavaScript共有八种数据类型:分别是Undefined、Null、Bollean、Number、String、Object、Symbol、BigInt
- Symbol代表创建后独一无二且不可变的数据类型,它主要是为了 解决可能出现的全局变量冲突的问题
- BigInt是一种数字类型的数据,它可以表示任意精度格式的整数, 使用BigInt可以安全地存储和操作大整数,即使这个数已经超出了 Number能够表示的安全整数范围
这些数据可以分为原始数据类型和引用数据类型:
- 栈:原始数据类型(Undefined、Null、Boolean、Number、String)--直接存储在栈(stack)中的简单数据段,占据空间 小、大小固定,属于被频繁使用数据,所以放入栈中存储
- 堆:引用数据类型(对象、数组和函数)--占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址
在操作系统中,内存被分为栈区和堆区:
- 栈区内存由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈
- 堆区内存一般由开发者分配释放,若开发者不释放,程序结束时可能由垃圾回收机制回收
2.数据类型检测的方式有哪些
(1)typeof
其中数组、对象、null都会被判断为object,其他判断都正确
(2)instanceof
instanceof可以正确判断对象的类型,其内部运行机制是判断在其 原型链中能否找到该类型的原型,而不能判断基 本数据类型。instanceof运算符可以用来测试一个对象在其原型链 中是否存在一个构造函数的prototype属性
(3)constructor
constructor有两个作用,一是判断数据的类型,二是对象实例通过 constrcutor对象访问它的构造函数。需要注意,如果创建一个对象 来改变它的原型,constructor就不能用来判断数据类型了:
3.null和undefined区别
- 首先Undefined和Null都是基本数据类型,这两个基本数据类型 分别都只有一个值,就是undefined和null
- undefined代表的含义是未定义,null代表的含义是空对象。一般 变量声明了但还没有定义的时候会返回undefined,null主要用于 赋值给一些可能会返回对象的变量,作为初始化
- undefined在JavaScript中不是一个保留字,这意味着可以使用 undefined来作为一个变量名,但是这样的做法是非常危险的,它会 影响对undefined值的判断。我们可以通过一些方法获得安全的 undefined值,比如说void0
- 当对这两种类型使用typeof进行判断时,Null类型化会返回 “object”,这是一个历史遗留的问题。当使用双等号对两种类型的 值进行比较时会返回true,使用三个等号时会返回false
4.什么是JavaScript中的包装类型?
在JavaScript中,基本类型是没有属性和方法的,但是为了便于操 作基本类型的值,在调用基本类型的属性或方法时JavaScript会在 后台隐式地将基本类型的值转换为对象,如:
在访问'abc'.length时,JavaScript将'abc'在后台转换成 String('abc'),然后再访问其length属性
JavaScript也可以使用Object函数显式地将基本类型转换为包装类型:
也可以使用valueOf方法将包装类型倒转成基本类型:
看看如下代码会打印出什么:
答案是什么都不会打印,因为虽然包裹的基本类型是false,但是 false被包裹成包装类型后就成了对象,所以其非值为false,所以 循环体中的内容不会运行
5.如何判断一个对象是空对象
使用JSON自带的.stringify方法来判断:
使用ES6新增的方法Object.keys()来判断:
6.const对象的属性可以修改吗
const保证的并不是变量的值不能改动,而是变量指向的那个内存地 址不能改动。对于基本类型的数据(数值、字符串、布尔值),其值 就保存在变量指向的那个内存地址,因此等同于常量
但对于引用类型的数据(主要是对象和数组)来说,变量指向数据的 内存地址,保存的只是一个指针,const只能保证这个指针是固定不 变的,至于它指向的数据结构是不是可变的,就完全不能控制了
7.箭头函数的this指向哪⾥?
箭头函数是ES6中的提出来的,它没有prototype,也没有自己的this 指向,更不可以使用arguments参数,它所谓的this是捕获其所在上下⽂的this值,作为⾃ ⼰的this值,并且由于没有属于⾃⼰的this,所以是不会被new 调⽤的,这个所谓的this也不会被改变
8.扩展运算符的作用及使用场景
(1)对象扩展运算符
对象的扩展运算符(...)用于取出参数对象中的所有可遍历属性,拷贝到当前对象之中
上述方法实际上等价于:
Object.assign方法用于对象的合并,将源对象(source)的所有可 枚举属性,复制到目标对象(target)
如果用户自定义的属性,放在扩展运算符后面,则扩展运算符 内部的同名属性会被覆盖掉
利用上述特性就可以很方便的修改对象的部分属性,需要注意:扩展运算符对对象实例的拷贝属于浅拷贝
(2)数组扩展运算符
数组的扩展运算符可以将一个数组转为用逗号分隔的参数序列,且每次只能展开一层数组
应用:
- 将数组转换为参数序列
- 复制数组
要记住:扩展运算符(…)用于取出参数对象中的所有可遍历属性,拷贝到当前对象之中,这里参数对象是个数组,数组里面的所有对象都 是基础数据类型,将所有基础数据类型重新拷贝到新的数组中
- 合并数组
- 扩展运算符与解构赋值结合起来,用于生成数组
需要注意:如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错
- 将字符串转为真正的数组
比较常见的应用是可以将某些数据结构转为数组:
用于替换es5中的Array.prototype.slice.call(arguments)写法
使用Math函数获取数组中特定的值:
9.Proxy可以实现什么功能?
在Vue3.0中通过Proxy来替换原本的Object.defineProperty来实现数据响应式
Proxy是ES6中新增的功能,它可以用来自定义对象中的操作
代表需要添加代理的对象,handler用来自定义对象中的操作,比如 可以用来自定义set或者get函数
下面来通过Proxy来实现一个数据响应式:
在上述代码中,通过自定义set和get函数的方式,在原本的逻辑中插入了我们的函数逻辑,实现了在对对象任何属性进行读写时发出通知
当然这是简单版的响应式实现,如果需要实现一个Vue中的响应式,需要在get中收集依赖,在set派发更新,之所以Vue3.0要使用 Proxy替换原本的API原因在于Proxy无需一层层递归为每个属 性添加代理,一次即可完成以上操作,性能上更好,并且原本的实现 有一些数据更新不能监听到,但是Proxy可以完美监听到任何方式 的数据改变,唯一缺陷就是浏览器的兼容性不好
10.常用的正则表达式有哪些?
11.JavaScript脚本延迟加载的方式有哪些?
延迟加载就是等页面加载完成之后再加载JavaScript文件。js延迟加载有助于提高页面加载速度,一般有以下几种方式:
- defer属性:给js脚本添加defer属性,这个属性会让脚本的加载与文档的解析同步解析,然后在文档解析完成后再执行这个脚本文 件,这样的话就能使页面的渲染不被阻塞。多个设置了defer属性脚本按规范来说最后是顺序执行的,但是在一些浏览器中可能不是这样
- async属性:给js脚本添加async属性,这个属性会使脚本异步加载,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行js 脚本,这个时候如果文档没有解析完成的话同样会阻塞。多个async 属性的脚本的执行顺序是不可预测的,一般不会按照代码的顺序依次执行
- 动态创建DOM方式:动态创建DOM标签的方式,可以对文档的加载事件进行监听,当文档加载完成后再动态的创建script标签来引入js脚本
- 使用setTimeout延迟方法:设置一个定时器来延迟加载js脚本文件
- 让JS最后加载:将js脚本放在文档的底部,来使js脚本尽可能的在最后来加载执行
12.什么是DOM和BOM?
DOM指的是文档对象模型,它指的是把文档当做一个对象,这个对象主要定义了处理网页内容的方法和接口
BOM指的是浏览器对象模型,它指的是把浏览器当做一个对象来对待,这个对象主要定义了与浏览器进行交互的方法和接口
BOM的核心是 window,而window对象具有双重角色,它既是通过js访问浏览器窗口的一个接口,又是一个Global(全局)对象。这意味着在网页中定义的任何对象,变量和函数,都作为全局对象的一个属性或者方法存在
window对象含有location对象、navigator对象、screen对象等子对象,并且DOM的最根本的对象document对象也是BOM的window对象的子对象
13.ajax、axios、fetch的区别
- ajax是Asynchronous JavaScript and XML的缩写,指的是通过JavaScript的异步通信,从服务器获取XML文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页
- fetch号称是AJAX的替代品,是在ES6出现的,使用了ES6中的 promise对象。Fetch是基于promise设计的。Fetch的代码结构比起ajax简单多。fetch不是ajax的进一步封装,而是原生js,没有使用XMLHttpRequest对象
fetch的优点:
语法简洁,更加语义化
基于标准Promise实现,支持async/await
更加底层,提供的API丰富(request,response)
脱离了XHR,是ES规范里新的实现方式
fetch的缺点:
fetch只对网络请求报错,对400,500都当做成功的请求,服务器 返回400,500错误码时并不会reject,只有网络错误这些导致请 求不能完成时,fetch才会被reject
fetch默认不会带cookie,需要添加配置项:fetch(url, {credentials:'include'}) fetch不支持abort,不支持超时控制,使用setTimeout及 Promise.reject的实现的超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费fetch没有办法原生监测请求的进度,而XHR可以
- axios是一种基于Promise封装的HTTP客户端,其特点如下: 浏览器端发起XMLHttpRequests请求
node端发起http请求
支持PromiseAPI
监听请求和返回
对请求和返回进行转化
取消请求
自动转换json数据
客户端支持抵御XSRF攻击
14.什么是尾调用,使用尾调用有什么好处?
尾调用指的是函数的最后一步调用另一个函数。代码执行是基于执行 栈的,所以当在一个函数里调用另一个函数时,会保留当前的执行上下文,然后再新建另外一个执行上下文加入栈中。使用尾调用的话,因为已经是函数的最后一步,所以这时可以不必再保留当前的执行上下文,从而节省了内存,这就是尾调用优化。但是ES6的尾调用优化只在严格模式下开启,正常模式是无效的
15.ES6模块与CommonJS模块有什么异同?
ES6Module和CommonJS模块的区别:
CommonJS是对模块的浅拷⻉,ES6 Module是对模块的引⽤,即ES6 Module只存只读,不能改变其值,也就是指针指向不能变,类似const
import的接⼝是read-only(只读状态),不能修改其变量值。即不能修改其变量的指针指向,但可以改变变量内部指针指向,可以对 commonJS重新赋值(改变指针指向),但是对ES6Module赋值会编译报错
ES6Module和CommonJS模块的共同点:
CommonJS和ES6Module都可以对引⼊的对象进⾏赋值,即对对象内部属性的值进⾏改变
16.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对象
17.对原型、原型链的理解
在JavaScript中是使用构造函数来新建一个对象的,在这个对象的内部将包含一个指针,这个 指针指向构造函数的prototype属性对应的值,在ES5中这个指针被称为对象的原型,ES5中新增了一个Object.getPrototypeOf()方法,可以通过这个方法来获取对象的原型
当访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。原型链的尽头一般来说都是Object.prototype所以这就是新建的对象为什么能够使用toString()等方法的原因
特点:JavaScript对象是通过引用来传递的,创建的每个新对象实体中并没有一份属于自己的原型副本。当修改原型时,与之相关的对象也会继承这一改变
由于Object是构造函数,原型链终点Object.prototype._proto_, 而Object.prototype._proto_===null//true,所以,原型链的终点是null。原型链上的所有原型都是对象,所有的对象最终都是由Object构造的,而Object.prototype的下一级是 Object.prototype._proto_
18.对作用域、作用域链的理解
(1)全局作用域
- 最外层函数和最外层函数外面定义的变量拥有全局作用域
- 所有未定义直接赋值的变量自动声明为全局作用域
- 所有window对象的属性拥有全局作用域
- 全局作用域有很大的弊端,过多的全局作用域变量会污染全局命名空间,容易引起命名冲突
(2)函数作用域
- 函数作用域声明在函数内部的变量,一般只有固定的代码片段可以访问到
- 作用域是分层的,内层作用域可以访问外层作用域,反之不行
(3)块级作用域
- 使用ES6中新增的let和const指令可以声明块级作用域,块级作用域可以在函数中创建也可以在一个代码块中的创建(由{}包裹的代码片段)
- let和const声明的变量不会有变量提升,也不可以重复声明
(4)作用域链
- 在当前作用域中查找所需变量,但是该作用域没有这个变量,那这个变量就是自由变量。如果在自己作用域找不到该变量就去父级作用域查找,依次向上级作用域查找,直到访问到window对象就被终止,这一层层的关系就是作用域链
- 作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,可以访问到外层环境的变量和函数
19.对this对象的理解
this是执行上下文中的一个属性,它指向最后一次调用这个方法的对象,在实际开发中,this的指向可以通过四种调用模式来判断:
- 第一种是函数调用模式,当一个函数不是一个对象的属性时,直接作为函数来调用时,this指向全局对象
- 第二种是方法调用模式,如果一个函数作为一个对象的方法来调用时,this指向这个对象
- 第三种是构造器调用模式,如果一个函数用new调用时,函数执行前会新创建一个对象,this指向这个新创建的对象
- 第四种是apply、call和bind调用模式,这三个方法都可以显示的指定调用函数的this指向
这四种方式,使用构造器调用模式的优先级最高,然后是apply、call 和bind调用模式,然后是方法调用模式,然后是函数调用模式
20.call()和apply()的区别?
它们的作用一模一样,区别仅在于传入参数的形式的不同
- apply接受两个参数,第一个参数指定了函数体内this对象的指向, 第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类 数组,apply方法把这个集合中的元素作为参数传递给被调用的函数
- call传入的参数数量不固定,跟apply相同的是,第一个参数也是 代表函数体内的this指向,从第二个参数开始往后,每个参数被依次传入函数
21.对Promise的理解
Promise是异步编程的一种解决方案,它是一个对象,可以获取异步操作的消息,它的出现大大改善了异步编程的困境,避免了地狱回调,它比传统的解决方案回调函数和事件更合理和更强大,Promise提供统一的API,各种异步操作都可以用同样的方法进行处理
(1)Promise的实例有三个状态:Pending(进行中)、Resolved(已完成)、Rejected(已拒绝)
(2)Promise的实例有两个过程:pending->fulfilled:Resolved(已完成)、pending->rejected:Rejected(已拒绝)
Promise的特点:对象的状态不受外界影响。promise对象代表一个异步操作,有三种状态,只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态,这也是promise这个名字的由来——“承诺”,一旦状态改变就不会再变,任何时候都可以得到这个结果
如果改变已经发生了,你再对promise对象添加回调函数,也会立即得到这个结果。这与事件(event)完全不同,事件的特点是:如果你错过了它,再去监听是得不到结果的
Promise的缺点:无法取消Promise,一旦新建它就会立即执行,无法中途取消;如果不设置回调函数,Promise内部抛出的错误,不会反应到外部;当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)
22.对async/await的理解
async/await其实是Generator的语法糖,从字面上来看,async是“异步”的简写,await则为等待,所以很好理解async用于申明一个function是异步的,而await用于等待一个异步方法执行完成。当然语法上强制规定await只能出现在asnyc函数中,先来看看async函数返回了什么:
async函数(包含 函数语句、函数表达式、Lambda表达式)会返回一个Promise对象, 如果在函数中return一个直接量,async会把这个直接量通过 Promise.resolve()封装成Promise对象,async函数返回的是一个Promise对象,所以在最外层不能用await获取其返回值的情况下,当然应该用原来的方式:then()链来处理这个Promise对象,就像这样:
那如果async函数没有返回值,又该如何?很容易想到,它会返回 Promise.resolve(undefined)。联想一下Promise的特点——无等待,所以在没有await的情况下执行async函数,它会立即执行,返回一个Promise对象,并且,绝不会阻塞后面的语句。这和普通返回Promise对象的函数并无二 致。注意:Promise.resolve(x)可以看作是newPromise(resolve=> resolve(x))的简写,可以用于快速封装字面量对象或其他对象,将 其封装成Promise实例。
23.async/await的优势
单一的Promise链并不能发现async/await的优势,但是,如果需要处理由多个Promise组成的then链的时候,优势就能体现出来了,假设一个业务,分多个步骤完成,每个步骤都是异步的,而且依赖于上一个步骤的结果。仍然用setTimeout来模拟异步操作:
现在用Promise方式来实现这三个步骤的处理:
输出结果result是step3()的参数700+200=900。doIt()顺序执行了三个步骤,一共用了300+500+700=1500毫秒,和console.time()/console.timeEnd()计算的结果一致。
如果用async/await来实现呢,会是这样:
结果和之前的Promise实现是一样的,但是这个代码看起来是不是清晰得多,几乎跟同步代码一样
24.async/await对比Promise的优势
代码读起来更加同步,Promise虽然摆脱了回调地狱,但是then的链式调⽤也会带来额外的阅读负担
Promise传递中间值⾮常麻烦,⽽async/await⼏乎是同步的写法,⾮常优雅
错误处理友好,async/await可以⽤成熟的try/catch,Promise的错误捕获⾮常冗余
调试友好,Promise的调试很差,由于没有代码块,你不能在⼀个返回表达式的箭头函数中设置断点,如果你在⼀个.then代码块中使⽤调试器的步进(step-over)功能,调试器并不会进⼊后续的.then代码块,因为调试器只能跟踪同步代码的每⼀步
25.哪些情况会导致内存泄漏
- 意外的全局变量:由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收
- 被遗忘的计时器或回调函数:设置了setInterval定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收
- 脱离DOM的引用:获取一个DOM元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收
- 闭包:不合理的使用闭包,从而导致某些变量一直被留在内存当中