js相关面试题总结
异步加载JS有哪些方式?
浏览器在解析HTML中,如果遇到script脚本,会停止页面的解析和渲染,去下载script脚本,多个script脚本下载是并行的,但按照html中的先后顺序依次执行(即使后面的脚本先下载完,也要等到前面的脚本下载完并执行完后,才能执行)。script脚本执行时,页面也是停止解析和渲染的。页面渲染完,才会触发DOMContentLoaded(所以script的下载和执行如果慢,会延迟DOMContentLoaded事件的触发时间)。下面的图展示了浏览器下载和执行script过程
在开发中脚本往往比HTML页面更“重”,处理时间更长,所以会造成页面的解析阻塞,在脚本下载和执行之前,用户在界面上什么都看不到。为了解决这个问题,我们可以采用异步加载JS脚本的方式。
异步加载js脚本常用方式:
1. defer 异步加载
* 在script标签中添加defer属性
* defer脚本的 下载和执行 都不会阻塞页面的解析渲染。因为要等到页面解析渲染完毕后,defer脚本才执行
* 多个defer脚本的下载是并行的,但执行却是按照顺序依次执行
* 当页面的解析和渲染完毕后,触发HTMLContentLoaded事件前,依次执行defer脚本。所以defer脚本的下载和执行如果慢,会延迟DOMContentLoaded触发时间
* 浏览器加载和执行 defer脚本的顺序如下图
2.async异步加载
*在script标签中添加async属性
* async 脚本的 下载 不会阻塞页面的解析和渲染。但async脚本的 执行 会阻塞页面的解析和渲染
* 多个async脚本的下载是并行的,哪个先下载完哪个脚本就立即执行,所以async不会按照页面的脚本顺序执行
* async 脚本的执行 只有在DOMContentLoaded事件之前时,才会影响DOMContentLoaded触发时间。又因为脚本的执行时间一般都比较短,所以可以认为async脚本基本不影响DOMContentLoaded事件的触发时间
* 浏览器加载和执行 async脚本的顺序如下图
3. 动态创建script标签 (基本不用了)
* 在还没有定义defer和async 前,异步加载的方式是通过动态创建script,通过window.onload方法确保页面加载完毕,再将script标签插入到DOM中
var let const 区别
- 变量提升:var 有声明提升,let与const 不存在变量声明提升的问题。
console.log(num) //undefined console.log(num1) //Cannot access 'num1' before initialization //console.log(num2) //Cannot access 'num2' before initialization var num = 1; let num1 = 2; //const num2 = 3
上面例子中,var如果在声明之前调用会显示undefined,但let和const 则直接报错
- 重复声明:var定义的变量可以覆盖,记可以重复定义(后面定义的覆盖前面定义的);let和const定义的变量不可以重复定义
var num = 1; var num = 2; console.log(num) //2 // let num1 = 1; // let num1 = 2 //Identifier 'num1' has already been declared //const num1 = 1; //const num1 = 2 //Identifier 'num2' has already been declared
- 块级作用域:var定义的变量没有块级作用域;let和const定义的变量存在块级作用域
for(var k = 0; k < 3 ;k++){ console.log(k) //0,1,2 } console.log(k) //k is not defined for(let i = 0; i < 3 ;i++){ console.log(i) //0,1,2 } console.log(i) //i is not defined
- 初始值设置:const声明的变量必须赋值,否则会报错;var 和 let 则允许先声明后赋值
const a; //控制台会报错
- 是否可修改:const 定义的变量不允许修改,否则会报错;var 和let 则允许修改
- 暂时性死区:let 和 const 在变量声明之前,该变量都是不可用的。这在语法上称之为暂时性死区;var不存在暂时性死区
- 给全局添加属性:浏览器的全局对象是window。var声明的变量为全局变量时会将该变量添加为window的属性。let和const不会
JS数据类型有哪些?
基本数据类型:
- 字符串 string
字符串与任何类型的数据结合,都转化成字符串拼接形式 。如下例子:
var a = 'hello' var c = true var b var d = null alert(a + true) //hellotrue alert(a + b) //helloundefined alert(a + d) //hellonull alert(a + 1) //hello1
- 数值 number
- 取值范围 (-2^53, 2^53)范围内,开区间。超出范围,会表示为Infinity 或者-Infinity;超出范围的大整数可以用 bigint 来表示
- 数字类型采用64位浮点数表示,从最左边开始:
- 第1位:符号位,决定了一个数的正负。0表示正数,1表示负数
- 第2位~第12位:存储指数部分(共11位),指数部分决定了数值的大小
- 第13位~第64位:存储小数部分,决定了数值的精度。(有效数字52位)
- NaN是number类型,但不是一个具体的数字。NaN与任何值都不相等,包括NaN本身即 NaN != NaN
- isNaN() 判断数据是否是NaN
- isFinite() 判断数据是否在范围内
-
var a = 1 alert(a + true) //2 alert(a + undefined) //NaN alert(a + null) //1
- 布尔值 boolean
- 布尔值只能有两个值:true 或者 false
- undefined
- 表示未定义或不存在,本来应该有值但没定义
- null
- 表示空值,即此处的值为空
- typeof null 是 object
- symbol
- 表示独一无二的值
- 格式: let xxx = Symbol('标识字符串')
- Symbol()通常作为属性名,在开发中需要对第三方插件或者框架添加自定应属性或者方法时会用到,防止跟框架的原有属性或者方法重名
- bigint
- 是一种数据类型的数字,可以表示任意精度格式的整数
- 可以安全的存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围
引用类型(对象类型):
- 对象Object
- 数组Array
- 函数Function
- 正则RegExp
- 日期Date
number、string、boolean、undefined、object、function、symbol、bigint
null 和 undefined 区别
大多数计算机语言,有且仅有一个表示“无”的值(比如:C语言NULL,java的null),但是JavaScript居然有两个表示“无”的值:null和 undefined.这又是为什么呢?
这与JsvaScript的历史有关。JsvaScript诞生时,借鉴了Java,只设置了一个null作为表示“无”的值,被当成对象。有根据C语言的传统,将null设计成可以自动转为0。但是设计者后来又觉得null像Javay一样被当成一个对象不是很好,再加上作者又觉得null自动转为0很不容易发现错误,因此,作者就又设计了一个undefined,为了填补之前的坑。
二者的具体区别:
null 是表示一个“无”的对象,转为数值时为0,typeof 为 object
undefined 是表示一个“无”的原始值,转为数值时为NaN, typeof 为 undefined
null 和 undefined 进行 == 比较时两者相等,全等===比较时不相等
== 和 === 有什么不同?
==:表示相等(比较的是值)
1.如果两边数据类型不同时,会将两个数据先转换为同一类型(隐式转换),再进行比较。具体的规则如下:
1)数字 和 字符串 进行比较时,会将字符串转换成为数字值
2)如果操作数之一是Boolean,会将Boolean转换成1或者0
3)如果操作数之一是对象,另一个是数字或者字符串,则会调用对象的 valueOf() 和 toString() 方法将对象转换为数字或者字符串
4)只有 null == undefined 情况为true,其他null和undefined以任何类型组合都是false
2.如果两边数据类型相同,则直接比较值、
1 == '1' // true '1' -> 1 1 == 'true' // false 0 == false // true false转化成数值0 [] == false // true []先调用valueOf()->转化成空字符串''->空字符串再转化成数值0; false转化成数值0 [] == 0 // true []先调用valueOf()->转化成空字符串''->空字符串再转化成数值0 ![] == [] //true ![]-> false -> false转化成数值0 ;[]先调用valueOf()->转化成空字符串''->空字符串再转化成数值0 null == undefined // true null == 0 // false ( 虽然null转换为数值是0,但依然不相等 ) null == false // false undefined == 0 // false
==:表示严格相等(除了比较值,没还比较类型)
1.如果两边数据类型不同时,会直接返回false,不会进行比较
2.只有两边数据类型相同,会判断值是否相同
3. 如果两个都是对象类型,那么会潘丹她们引用地址是否一致
0 === '0' //false 0 === false // false null === undefined // false let a = {} let b = {} let c = a a === b // false a === c // true
typof和instanceof都是判断数据类型的方法,区别如下:
- typeof 返回的是数据类型;而insfanceof返回的是布尔值;
- typeof 虽然可以判断数据类型,但判断一个object的数据的时候不能细致到具体是哪一种object;而instanceof可以用来判断对象是否是某一具体的类型
- instanceof 可以判断一个对象是否属于某个构造函数的实例
instanceof 原理:是实例对象的__proto__ 和 构造函数的prototype 是否相等,如果相等则返回true;如果不想等则判断实例对象的__proto__ 的__proto__ 和 构造函数的prototype是否相等,相等返回true,不想等则继续原型练一层一层查找,直到原型链的尽头(Object.prototype.__proto__ == null )还未找到就放回false。
具体代码实现如下:
function MyInstanceof(L, R){ if(typeof L !== 'object' || L !== null){ return false } if(typeof R !== 'function'){ return false } while(L !== null){ if(L.__proto__ === R.prototype) { return true; //找到返回true }else{ L = L.__proto__ ; // 继续原型链上查找 } } return false //为查找到返回 false }
都是截取字符串的方法,区别如下:
substring(startIndex,endIndex): 一个起始索引和一个结束索引来指定范围,如果省略第二个参数,则截取字符串末尾
substr(startIndex,length): 从startIndex开始,长度为length的字符串,如果省略第二个参数,则截取到字符串末尾
const str = "hello, world!"; console.log(str.substring(7,12)); //world console.log(str.substr(7,5)); //world
for...of: es6新增的遍历方式
- 适用于遍历可迭代对象:数组、类数组对象、字符串、Map、Set 、以及Generator 对象
- 不能遍历普通对象,要想遍历对象,可以给对象添加一个Symbol.iterator属性,并指向一个迭代器即可
- 遍历获取的是对象的键值(value)
- 循环能保证按照对象元素的顺序进行迭代
- 只遍历自身的可枚举属性,不会遍历原型链上的可枚举属性值
for...in:
- 适用于遍历 可枚举属性:像 对象、数组、字符串
- 遍历获取的是对象的键名(key)
- 遍历对象的属性不能保证按顺序进行迭代
- 遍历自身的可枚举属性 和 原型上的可枚举属性
总结: for...in 循环主要是为了遍历对象,不适用于遍历数组;for...of循环可以用来遍历数组、类数组、字符串、Set、Map以及 Generator对象
for...of 作为ES6新增的的遍历方式,能遍历的数据都有一个遍历器iterator接口 而数组、字符串、Map、Set其内部已经实现,普通对象内部没实现。所以遍历对象时会报错。
要想遍历对象,可以给对象添加一个特殊的属性 Symbol.iterator ,并将其指向一个迭代函数。
示例代码如下:
const obj = {a:1,b:2,c:3} //为对象添加[Symbol.iterator] 属性,柄指向一个迭代器 obj[Symbol.iterator] = function* (){ for(let key in obj){ yield obj[key] } }
AJAX 是异步的Javascript 和 XML 技术。是与服务器进行交互功能的实现
AJAX 运行当中经历了5种状态,分别是:
- 0 - (未初始化) 还没有调用send() 方法
- 1 - (载入) 已调用send()方法,正在发送请求
- 2 - (载入完成) send()方法执行完成
- 3 - (交互) 正在解析相应内容
- 4 - (完成) 响应内容解析完成,成功返回
- ajax
- 基于XHLHttpRequest 对象
- 多个请求之间如果有先后关系的话,会出现回调地狱
- 需要手动处理各种请求和响应,代码冗长
- fetch
- fetch是ajax的替代品
- fetch 不是ajax的进一步封装,而是用原生js,没有使用XMLHttpRequest对象
- 使用了promise对象,支持async/await
- 使用.then方式处理回调结果,解决了回调地狱问题
- 响应数据需要手动处理JSON数据
- 默认不会带cookie,需要添加配置项
- axios--首选
- 基于XMLHttpRequest对象
- 通过Promise封装的网络请求库,使用时需要引入这个库
- 支持请求拦截和响应拦截
- 自动转换JSON数据
- 客户端支持跨站请求伪造(CSRF/XSRF)预防
尾调用:就是在函数的最后一步调用函数(即最后一步是返回的函数调用)。例子:
function f(x){ return g(x) }
下面几种请款都不属于尾调用:
情况一:
function f(x){ let y = g(x) return y } 情况二: function f(x){ return g(x) + 1 } 情况三: function f(x){ g(x) }
使用尾调用的好处:在函数里调用另外一个函数时,会保留当前的执行上下文,如果在函数的尾部调用,因为已经是最后一步,所以这时可以不用保留当前的执行上下文,从而节省内存。
浅拷贝:只复制了第一层
1.实现的方法:
1)Object.assign()方法 拷贝对象
2)扩展运算符(...)
2.手写一个浅拷贝
function shallCopy(obj){ if(!obj || typeof obj !== 'object'){ return obj } const newObj = Array.isArray(obj) ? [] : {} for(let key in obj){ if(obj.hasOwnProperty(key)){ newObj[key] = obj[key] } } return newObj }
深拷贝:将原对象的各个属性值复制过来,是值而不是引用,新旧两个对象互不影响
1.实现方法:
1)JSON.parse(JSON.stringify(obj))
-
-
-
- 缺陷:对象中的函数、undefined、symbol 会丢失
-
-
2.手写一个深拷贝
- 箭头函数是匿名函数,所以不能作为构造函数,不能使用new 关键字
- 箭头函数没有arguments
- 箭头函数没有自己的this,会获取所在的上下文作为自己的this
- call()、apply()、bind()不能改变箭头函数中的this
- 箭头函数没有prototype
- 箭头函数不能用作Generator函数,不能使用yeild 关键字
- Set是值的集合,Map是键值对,键和值可以是任何值
- Map可以通过get方法获取值,而Set不能
- Set的值是唯一的可以做数组去重,Map由于没有格式限制,可以做数据存储
Map 和 Object 都是键值对来存储数据,区别如下:
- 键的类型: Map 的值可以是任意数据类型(包括对象、函数、NaN等);而Object 只能是字符串或者Symbol类型
- 键值对的顺序:Map 中的键值对是按照插入的顺序存储的;而Object中的键值对没有顺序
- 键值对的遍历:Map 的键值对可以使用for...of进行遍历;而Object的键值对需要手动遍历
- 继承关系:Map 没有继承关系;而Object是所有对象的基类
1.、Promise 是异步编程的一种解决方案,将异步操作以同步操作流程表达出来,避免地狱回调
2、Promise 的实例有三个状态:
- Pending(初始状态)
- Fulfilled(成功状态)
- Rejected (失败状态)
3、Promise 实例有两个过程
1. pending -> fulfilled : Resolved(已完成)
2. pending -> rejected: Rejected(已拒绝)
注: 一旦从进行状态变为其他状态就永久不能改变状态了,其过程是不可逆的
4、Promise 构造函数接收一个带有 resolve 和 reject 参数的回调函数
- resolve的作用是将 Promise 状态从 pending 变为 fulfilled, 在异步操作成功时调用,将异步结果返回,作为参数传递出去
- reject 的作用是将 Promise 状态从pending 变为 rejected,在异步操作失败后,将异步操作错误的结果作为参数传递出去
5. then 和 catch 是如何影响Promise状态变化的(重要)
then 和 catch 如果正常则返回状态为 fulfilled 的Promise,如果里面有包错则返回 状态为 rejected 的Promise
注意: catch 里面没有报错的话回返回 fulfilled 的 Promise,回触发会面的 then 回调而不是catch 回调
eg: then和catch 链式调用题目
const p1 = Promise.resolve().then(()=>{ console.log('1') //无错误 返回 fulfilled 状态的Promise,触发后面的 then 回调 }).then(()=>{ console.log('3') throw new Error('err') //有错误 返回rejected状态的Promise,触发后面的 catch 回调 }).then(()=>{ console.log('4') }).catch(()=>{ console.log('5') //无错误 返回 fulfilled 状态的Promise,触发后面的 then 回调 }).catch(()=>{ console.log('6') }).then(()=>{ console.log('7') // 无错误 返回 fulfilled 状态 的Promise,触发后续的 then 回调
})
上面代码打印结果是:1 3 5 7
5. Promise 的缺点:
- 无法取消Promise,一旦新建它就会立即执行,无法中途取消
- 如果不设置回调函数,Promise 内部会抛出错误,不会返回到外部
- 当处于pending状态时,无法得知目前进展到哪一个阶段(刚开始还是即将完成)
6. Promise 方法
- promise.then() 对应resolve 的成功处理
- promise.catch() 对应 reject 失败的处理
- promise.race() 可以完成并行任务,将多个Promise实例数组包装成一个新的Promise实例。有一个完成就算完成
- promise.all() 当所有Promise实例都resolve后,才会resolve返回一个由所有Promise返回值组成的数组。如果有一个Promise实例reject,就会立即被拒绝,返回拒绝原因。all是所有都成功才算,有一个失败就算失败
- promise.allSettled() 等所有Promise执行完毕后,不管成功还是失败,都会把每个Promise状态信息放到一个数组了里返回