前端面试题——JS篇
1.介绍一下JS的数据类型有那些,值是如何存储的?
JavaScript一共有8种数据类型,其中有7中基本数据类型:Undefined、Null、Boolean、Number、String、Symbol(ES6新增,表示独一无二的值)和BigInt(ES10新增);
1种引用数据类型——Object(Object本质上是由一组无序的名值队组成)。里面包含function、Array、Date等。JavaScript不支持任何创建自定义类型的机制,而所有值最终都将是上述8种数据类型之一。
原始数据类型:直接存储在栈(stack)中,占据空间小、大小固定,属于频繁使用数据,所以放入栈中存储。
引用数据类型:同事存储在栈(stack)堆(heap)中,占据空间大、大小不固定。引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获取实体。
2.JavaScript的作用域和作用域链
作用域:作用域是定义变量的区域,它有一套访问变量的规则,这套规则来管理浏览器引擎如何在当前作用域以及嵌套的作用域中根据变量(标识符)进行变量查找。
作用域链:作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,我们可以访问到外层环境的变量和函数。
作用域链的本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。作用域链的前端始终都是当前执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象。
当我们查找一个变量时,如果当前执行环境中没有找到,我们可以沿着作用域链向后查找。
作用域链的创建过程跟执行上下文的建立有关....
3.谈谈你对this、call、apply和bind的理解
1.在浏览器里,在全局范围内this指向Windows对象;
2.在函数中,this永远指向最后调用他的那个对象;
3.构造函数中,this指向new出现的那个新的对象;
4.call、apply、bind中的this被强绑定在指定的那个对象上;
5.箭头函数中this比较特殊,箭头函数this为父作用域的this,不是调用时的this要知道前四种方式都是调用时确定,也就是动态的,而箭头函数的this指向是静态的,生命的时候就确定了下来;
6.apply、call、bind都是js给函数内置的一些API,调用他们可以为函数指定this的执行,同时也可以传参。
4.什么是闭包,为什么要用它?
闭包是是有权访问另一个函数作用域内变量的函数。创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。
闭包有两个常用的用途:
1.闭包的第一个用途是使我们在函数外部能访问到函数内部的变量。通过使用闭包,我们可以通过外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
2.函数的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。
function a(){
var n = 0;
function add(){
n++;
console.log(n);
}
return add;
}
var a1 = a(); //注意,函数名只是一个标识(指向函数的指针),而()才是执行函数;
a1(); //1
a1(); //2 第二次调用n变量还在内存中
其实闭包的本质就是作用域链的一个特殊的应用,只要了解了作用域链的创建过程,就能够理解闭包的实现原理。
5.Ajax是什么?如何创建一个Ajax?
它是一种异步通信的方法,通过直接由 js 脚本向服务器发起 http 通信,然后根据服务器返回的数据,更新网页的相应部分,而不用刷新整个页面的一种方法。
原生:
//1:创建Ajax对象
var xhr = window.XMLHttpRequest?new XMLHttpRequest():new ActiveXObject('Microsoft.XMLHTTP');// 兼容IE6及以下版本
//2:配置 Ajax请求地址
xhr.open('get','index.xml',true);
//3:发送请求
xhr.send(null); // 严谨写法
//4:监听请求,接受响应
xhr.onreadysatechange=function(){
if(xhr.readySate==4&&xhr.status==200 || xhr.status==304 )
console.log(xhr.responsetXML)
}
JQuery:
$.ajax({
type:'post',
url:'',
async:ture,//async 异步 sync 同步
data:data,//针对post请求
dataType:'jsonp',
success:function (msg) {
},
error:function (error) {
}
})
promise 封装实现
// promise 封装实现:
function getJSON(url) {
// 创建一个 promise 对象
let promise = new Promise(function(resolve, reject) {
let xhr = new XMLHttpRequest();
// 新建一个 http 请求
xhr.open("GET", url, true);
// 设置状态的监听函数
xhr.onreadystatechange = function() {
if (this.readyState !== 4) return;
// 当请求成功或失败时,改变 promise 的状态
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
};
// 设置错误监听函数
xhr.onerror = function() {
reject(new Error(this.statusText));
};
// 设置响应的数据类型
xhr.responseType = "json";
// 设置请求头信息
xhr.setRequestHeader("Accept", "application/json");
// 发送 http 请求
xhr.send(null);
});
return promise;
}
6.简单介绍一下 V8 引擎的垃圾回收机制
v8 的垃圾回收机制基于分代回收机制,这个机制又基于世代假说,这个假说有两个特点,一是新生的对象容易早死,另一个是不死的对象会活得更久。基于这个假说,v8 引擎将内存分为了新生代和老生代。
新创建的对象或者只经历过一次的垃圾回收的对象被称为新生代。经历过多次垃圾回收的对象被称为老生代。
新生代被分为 From 和 To 两个空间,To 一般是闲置的。当 From 空间满了的时候会执行 Scavenge 算法进行垃圾回收。当我们执行垃圾回收算法的时候应用逻辑将会停止,等垃圾回收结束后再继续执行。这个算法分为三步:
(1)首先检查 From 空间的存活对象,如果对象存活则判断对象是否满足晋升到老生代的条件,如果满足条件则晋升到老生代。如果不满足条件则移动 To 空间。
(2)如果对象不存活,则释放对象的空间。
(3)最后将 From 空间和 To 空间角色进行交换。
新生代对象晋升到老生代有两个条件:
(1)第一个是判断是对象否已经经过一次 Scavenge 回收。若经历过,则将对象从 From 空间复制到老生代中;若没有经历,则复制到 To 空间。
(2)第二个是 To 空间的内存使用占比是否超过限制。当对象从 From 空间复制到 To 空间时,若 To 空间使用超过 25%,则对象直接晋升到老生代中。设置 25% 的原因主要是因为算法结束后,两个空间结束后会交换位置,如果 To 空间的内存太小,会影响后续的内存分配。
老生代采用了标记清除法和标记压缩法。标记清除法首先会对内存中存活的对象进行标记,标记结束后清除掉那些没有标记的对象。由于标记清除后会造成很多的内存碎片,不便于后面的内存分配。所以了解决内存碎片的问题引入了标记压缩法。
由于在进行垃圾回收的时候会暂停应用的逻辑,对于新生代方法由于内存小,每次停顿的时间不会太长,但对于老生代来说每次垃圾回收的时间长,停顿会造成很大的影响。 为了解决这个问题 V8 引入了增量标记的方法,将一次停顿进行的过程分为了多步,每次执行完一小步就让运行逻辑执行一会,就这样交替运行。
7.哪些操作会造成内存泄漏?
1.意外的全局变量
2.被遗忘的计时器或回调函数
3.脱离 DOM 的引用
4.闭包
第一种情况是我们由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收。
第二种情况是我们设置了setInterval定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。
第三种情况是我们获取一个DOM元素的引用,而后面这个元素被删除,由于我们一直保留了对这个元素的引用,所以它也无法被回收。
第四种情况是不合理的使用闭包,从而导致某些变量一直被留在内存当中。
8.var、let和const的区别是什么?
var声明的变量会挂载在window上,而let和const声明的变量不会:
var a = 100;
console.log(a,window.a); // 100 100
let b = 10;
console.log(b,window.b); // 10 undefined
const c = 1;
console.log(c,window.c); // 1 undefined
var声明变量存在变量提升,let和const不存在变量提升:
console.log(a); // undefined ===> a已声明还没赋值,默认得到undefined值
var a = 100;
console.log(b); // 报错:b is not defined ===> 找不到b这个变量
let b = 10;
console.log(c); // 报错:c is not defined ===> 找不到c这个变量
const c = 10;
let和const声明形成块作用域
if(1){
var a = 100;
let b = 10;
}
console.log(a); // 100
console.log(b) // 报错:b is not defined ===> 找不到b这个变量
-------------------------------------------------------------
if(1){
var a = 100;
const c = 1;
}
console.log(a); // 100
console.log(c) // 报错:c is not defined ===> 找不到c这个变量
同一作用域下let和const不能声明同名变量,而var可以
var a = 100;
console.log(a); // 100
var a = 10;
console.log(a); // 10
-------------------------------------
let a = 100;
let a = 10;
// 控制台报错:Identifier 'a' has already been declared ===> 标识符a已经被声明了。
暂存死区
var a = 100;
if(1){
a = 10;
//在当前块作用域中存在a使用let/const声明的情况下,给a赋值10时,只会在当前作用域找变量a,
// 而这时,还未到声明时候,所以控制台Error:a is not defined
let a = 1;
}
const
/*
* 1、一旦声明必须赋值,不能使用null占位。
*
* 2、声明后不能再修改
*
* 3、如果声明的是复合类型数据,可以修改其属性
*
* */
const a = 100;
const list = [];
list[0] = 10;
console.log(list); // [10]
const obj = {a:100};
obj.name = 'apple';
obj.a = 10000;
console.log(obj); // {a:10000,name:'apple'}
9.什么是箭头函数?
箭头函数表达式的语法比函数表达式更简洁,并且没有自己的this,arguments,super或new.target。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。
var getCurrentDate = function (){
return new Date();
}
//ES6 Version
const getCurrentDate = () => new Date();
在本例中,ES5 版本中有function(){}声明和return关键字,这两个关键字分别是创建函数和返回值所需要的。在箭头函数版本中,我们只需要()括号,不需要 return 语句,因为如果我们只有一个表达式或值需要返回,箭头函数就会有一个隐式的返回。
//ES5 Version
function greet(name) {
return 'Hello ' + name + '!';
}
//ES6 Version
const greet = (name) => `Hello ${name}`;
const greet2 = name => `Hello ${name}`;
我们还可以在箭头函数中使用与函数表达式和函数声明相同的参数。如果我们在一个箭头函数中有一个参数,则可以省略括号。
const getArgs = () => arguments
const getArgs2 = (...rest) => rest
箭头函数不能访问arguments对象。所以调用第一个getArgs函数会抛出一个错误。相反,我们可以使用rest参数来获得在箭头函数中传递的所有参数。
const data = {
result: 0,
nums: [1, 2, 3, 4, 5],
computeResult() {
// 这里的“this”指的是“data”对象
const addAll = () => {
return this.nums.reduce((total, cur) => total + cur, 0)
};
this.result = addAll();
}
};
箭头函数没有自己的this值。它捕获词法作用域函数的this值,在此示例中,addAll函数将复制computeResult 方法中的this值,如果我们在全局作用域声明箭头函数,则this值为 window 对象。
10. js的深浅拷贝
JavaScript的深浅拷贝一直是个难点,如果现在面试官让我写一个深拷贝,我可能也只是能写出个基础版的。所以在写这条之前我拜读了收藏夹里各路大佬写的博文。具体可以看下面我贴的链接,这里只做简单的总结。
浅拷贝:创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
深拷贝:将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。
浅拷贝的实现方式:
Object.assign()方法: 用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。
** Array.prototype.slice():slice() 方法**:返回一个新的数组对象,这一对象是一个由 begin和end(不包括end)决定的原数组的浅拷贝。原始数组不会被改变。
拓展运算符 ...:
let a = {
name: "Jake",
flag: {
title: "better day by day",
time: "2020-05-31"
}
}
let b = {...a};
深拷贝的实现方式
乞丐版: JSON.parse(JSON.stringify(object)),缺点诸多(会忽略undefined、symbol、函数;不能解决循环引用;不能处理正则、new Date())
基础版: 浅拷贝+递归 (只考虑了普通的 object和 array两种数据类型)
function cloneDeep(target,map = new WeakMap()) {
if(typeOf taret ==='object'){
let cloneTarget = Array.isArray(target) ? [] : {};
if(map.get(target)) {
return target;
}
map.set(target, cloneTarget);
for(const key in target){
cloneTarget[key] = cloneDeep(target[key], map);
}
return cloneTarget
}else{
return target
}
}
终极版
const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';
const argsTag = '[object Arguments]';
const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';
const deepTag = [mapTag, setTag, arrayTag, objectTag, argsTag];
function forEach(array, iteratee) {
let index = -1;
const length = array.length;
while (++index < length) {
iteratee(array[index], index);
}
return array;
}
function isObject(target) {
const type = typeof target;
return target !== null && (type === 'object' || type === 'function');
}
function getType(target) {
return Object.prototype.toString.call(target);
}
function getInit(target) {
const Ctor = target.constructor;
return new Ctor();
}
function cloneSymbol(targe) {
return Object(Symbol.prototype.valueOf.call(targe));
}
function cloneReg(targe) {
const reFlags = /\w*$/;
const result = new targe.constructor(targe.source, reFlags.exec(targe));
result.lastIndex = targe.lastIndex;
return result;
}
function cloneFunction(func) {
const bodyReg = /(?<={)(.|\n)+(?=})/m;
const paramReg = /(?<=\().+(?=\)\s+{)/;
const funcString = func.toString();
if (func.prototype) {
const param = paramReg.exec(funcString);
const body = bodyReg.exec(funcString);
if (body) {
if (param) {
const paramArr = param[0].split(',');
return new Function(...paramArr, body[0]);
} else {
return new Function(body[0]);
}
} else {
return null;
}
} else {
return eval(funcString);
}
}
function cloneOtherType(targe, type) {
const Ctor = targe.constructor;
switch (type) {
case boolTag:
case numberTag:
case stringTag:
case errorTag:
case dateTag:
return new Ctor(targe);
case regexpTag:
return cloneReg(targe);
case symbolTag:
return cloneSymbol(targe);
case funcTag:
return cloneFunction(targe);
default:
return null;
}
}
function clone(target, map = new WeakMap()) {
// 克隆原始类型
if (!isObject(target)) {
return target;
}
// 初始化
const type = getType(target);
let cloneTarget;
if (deepTag.includes(type)) {
cloneTarget = getInit(target, type);
} else {
return cloneOtherType(target, type);
}
// 防止循环引用
if (map.get(target)) {
return map.get(target);
}
map.set(target, cloneTarget);
// 克隆set
if (type === setTag) {
target.forEach(value => {
cloneTarget.add(clone(value, map));
});
return cloneTarget;
}
// 克隆map
if (type === mapTag) {
target.forEach((value, key) => {
cloneTarget.set(key, clone(value, map));
});
return cloneTarget;
}
// 克隆对象和数组
const keys = type === arrayTag ? undefined : Object.keys(target);
forEach(keys || target, (value, key) => {
if (keys) {
key = value;
}
cloneTarget[key] = clone(target[key], map);
});
return cloneTarget;
}
module.exports = {
clone
};
11.什么是回调函数?回调函数有什么缺点
回调函数是一段可执行的代码段,它作为一个参数传递给其他的代码,其作用是在需要的时候方便调用这段(回调函数)代码。
在JavaScript中函数也是对象的一种,同样对象可以作为参数传递给函数,因此函数也可以作为参数传递给另外一个函数,这个作为参数的函数就是回调函数。
const btnAdd = document.getElementById('btnAdd');
btnAdd.addEventListener('click', function clickCallback(e) {
// do something useless
});
在本例中,我们等待id为btnAdd的元素中的click事件,如果它被单击,则执行clickCallback函数。回调函数向某些数据或事件添加一些功能。
回调函数有一个致命的弱点,就是容易写出回调地狱(Callback hell)。假设多个事件存在依赖性:
setTimeout(() => {
console.log(1)
setTimeout(() => {
console.log(2)
setTimeout(() => {
console.log(3)
},3000)
},2000)
},1000)
这就是典型的回调地狱,以上代码看起来不利于阅读和维护,事件一旦多起来就更是乱糟糟,所以在es6中提出了Promise和async/await来解决回调地狱的问题。当然,回调函数还存在着别的几个缺点,比如不能使用 try catch 捕获错误,不能直接 return。
12.Promise是什么,可以手写实现一下吗?
Promise,翻译过来是承诺,承诺它过一段时间会给你一个结果。从编程讲Promise 是异步编程的一种解决方案。下面是Promise在MDN的相关说明:
Promise 对象是一个代理对象(代理一个值),被代理的值在Promise对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法(handlers)。这让异步方法可以像同步方法那样返回值,但并不是立即返回最终执行结果,而是一个能代表未来出现的结果的promise对象。
一个 Promise有以下几种状态:
pending: 初始状态,既不是成功,也不是失败状态。
fulfilled: 意味着操作成功完成。
rejected: 意味着操作失败。
这个承诺一旦从等待状态变成为其他状态就永远不能更改状态了,也就是说一旦状态变为 fulfilled/rejected 后,就不能再次改变。可能光看概念大家不理解Promise,我们举个简单的栗子;
假如我有个女朋友,下周一是她生日,我答应她生日给她一个惊喜,那么从现在开始这个承诺就进入等待状态,等待下周一的到来,然后状态改变。如果下周一我如约给了女朋友惊喜,那么这个承诺的状态就会由pending切换为fulfilled,表示承诺成功兑现,一旦是这个结果了,就不会再有其他结果,即状态不会在发生改变;反之如果当天我因为工作太忙加班,把这事给忘了,说好的惊喜没有兑现,状态就会由pending切换为rejected,时间不可倒流,所以状态也不能再发生变化。
上一条我们说过Promise可以解决回调地狱的问题,没错,pending 状态的 Promise 对象会触发 fulfilled/rejected 状态,一旦状态改变,Promise 对象的 then 方法就会被调用;否则就会触发 catch。我们将上一条回调地狱的代码改写一下:
new Promise((resolve,reject) => {
setTimeout(() => {
console.log(1)
resolve()
},1000)
}).then((res) => {
setTimeout(() => {
console.log(2)
},2000)
}).then((res) => {
setTimeout(() => {
console.log(3)
},3000)
}).catch((err) => {
console.log(err)
})
其实Promise也是存在一些缺点的,比如无法取消 Promise,错误需要通过回调函数捕获。
promise手写实现,面试够用版:
function myPromise(constructor){
let self=this;
self.status="pending" //定义状态改变前的初始状态
self.value=undefined;//定义状态为resolved的时候的状态
self.reason=undefined;//定义状态为rejected的时候的状态
function resolve(value){
//两个==="pending",保证了状态的改变是不可逆的
if(self.status==="pending"){
self.value=value;
self.status="resolved";
}
}
function reject(reason){
//两个==="pending",保证了状态的改变是不可逆的
if(self.status==="pending"){
self.reason=reason;
self.status="rejected";
}
}
//捕获构造异常
try{
constructor(resolve,reject);
}catch(e){
reject(e);
}
}
// 定义链式调用的then方法
myPromise.prototype.then=function(onFullfilled,onRejected){
let self=this;
switch(self.status){
case "resolved":
onFullfilled(self.value);
break;
case "rejected":
onRejected(self.reason);
break;
default:
}
}
13.什么是 async/await 及其如何工作,有什么优缺点?
async/await是一种建立在Promise之上的编写异步或非阻塞代码的新方法,被普遍认为是 JS异步操作的最终且最优雅的解决方案。相对于 Promise 和回调,它的可读性和简洁度都更高。毕竟一直then()也很烦。
async 是异步的意思,而 await 是 async wait的简写,即异步等待。
所以从语义上就很好理解 async 用于声明一个 function 是异步的,而await 用于等待一个异步方法执行完成。
一个函数如果加上 async ,那么该函数就会返回一个 Promise
async function test() {
return "1"
}
console.log(test()) // -> Promise {: "1"}
可以看到输出的是一个Promise对象。所以,async 函数返回的是一个 Promise 对象,如果在 async 函数中直接 return 一个直接量,async 会把这个直接量通过 PromIse.resolve()封装成Promise对象返回。
相比于Promise,async/await能更好地处理 then 链
function takeLongTime(n) {
return new Promise(resolve => {
setTimeout(() => resolve(n + 200), n);
});
}
function step1(n) {
console.log(`step1 with ${n}`);
return takeLongTime(n);
}
function step2(n) {
console.log(`step2 with ${n}`);
return takeLongTime(n);
}
function step3(n) {
console.log(`step3 with ${n}`);
return takeLongTime(n);
}
现在分别用 Promise 和async/await来实现这三个步骤的处理。
使用Promise
function doIt() {
console.time("doIt");
const time1 = 300;
step1(time1)
.then(time2 => step2(time2))
.then(time3 => step3(time3))
.then(result => {
console.log(`result is ${result}`);
});
}
doIt();
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
使用async/await
async function doIt() {
console.time("doIt");
const time1 = 300;
const time2 = await step1(time1);
const time3 = await step2(time2);
const result = await step3(time3);
console.log(`result is ${result}`);
}
doIt();
结果和之前的 Promise 实现是一样的,但是这个代码看起来是不是清晰得多,优雅整洁,几乎跟同步代码一样。
await关键字只能在async function中使用。在任何非async function的函数中使用await关键字都会抛出错误。await关键字在执行下一行代码之前等待右侧表达式(可能是一个Promise)返回。
优缺点
async/await的优势在于处理 then 的调用链,能够更清晰准确的写出代码,并且也能优雅地解决回调地狱问题。当然也存在一些缺点,因为 await 将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 await 会导致性能上的降低。
14.js 的节流与防抖
函数防抖 是指在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。这可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求。
函数节流 是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。节流可以使用在 scroll 函数的事件监听上,通过事件节流来降低事件调用的频率。
// 函数防抖的实现
function debounce(fn, wait) {
var timer = null;
return function() {
var context = this,
args = arguments;
// 如果此时存在定时器的话,则取消之前的定时器重新记时
if (timer) {
clearTimeout(timer);
timer = null;
}
// 设置定时器,使事件间隔指定事件后执行
timer = setTimeout(() => {
fn.apply(context, args);
}, wait);
};
}
// 函数节流的实现;
function throttle(fn, delay) {
var preTime = Date.now();
return function() {
var context = this,
args = arguments,
nowTime = Date.now();
// 如果两次时间间隔超过了指定时间,则执行函数。
if (nowTime - preTime >= delay) {
preTime = Date.now();
return fn.apply(context, args);
}
};
}
发表评论 (审核通过后显示评论):