JavaScript面试题

145 阅读9分钟

闭包

什么是闭包?

函数使⽤了不属于⾃⼰的局部变量,通俗来讲就是函数套函数,⾥⾯函数使⽤了外⾯函数定义的变量

闭包的作用

避免全局变量的污染

闭包为什么会导致内存泄漏?

闭包中的局部变量一直被全局变量引用着,而全局变量不能被垃圾回收机制清除,所以也会导致闭包中的局部变量一直无法被释放

什么是原型?

一个函数可以看成一个类,原型是所有类都有的一个属性

什么是原型链?

任何一个函数都有隐式原型,隐式原型默认指向创建该对象的构造函数的原型对象,原型对象也是一个对象,也有隐式原型,原型对象也会指向他的构造函数原型对象,以此层层指向指到父级,一直指到null,以此来构成原型链

原型链作用?

原型链的主要作用是可以实现继承

什么是继承?

继承:是由同一构造函数创建出来的对象都能使⽤⽗类实例对象原型上的数据

数组去重的⽅案

数组去重的⽅案

第一种:

1、使用双层for循环

2、splice() 方法:用于添加或删除数组中的元素。

3、删除相同的值,后续的值往前移

var arr = [1, 2, 3, 3, 2, 'hello', 'hello'];
// 1.双层for循环
function unique(arr) {
  // 第一层for循环控制第一个数
  for (let i = 0; i < arr.length; i++) {
    // 第二层循环控制第二个数
    for (let j = i + 1; j < arr.length; j++) {
      // 判断前后是否相等
      if (arr[i] === arr[j]) {
        arr.splice(j, 1); //j:下标 1:删除个数
        // 后面的往前移一位
        j--;
      }
    }
  }
}
unique(arr);
console.log(arr); //[ 1, 2, 3, 'hello' ]

第二种:for循环+indexOf() +push()

思路:建立新数组、使用for循环遍历,再使用indexOf()判断,若为-1则使用push()加入新数组

1、indexOf() 方法:可返回数组中某个指定的元素位置。(如果有多个则返回第一次出现的位置,若找不到则返回-1)

2、push() 方法:可向数组的末尾添加一个或多个元素,并返回新的长度。

// 2. indexOf() 
var arr = [1, 2, 3, 3, 2, 'hello', 'hello'];
 
function unique(arr) {
  let newArr = []; //建立新数组
  for (let i = 0; i < arr.length; i++) { //for循环遍历
    // indexOf()返回-1说明,该值不在数组中
    if (newArr.indexOf(arr[i]) === -1) {
      newArr.push(arr[i]) //将该值加入新数组
    }
  }
  return newArr //返回新数组
}
console.log(unique(arr)); //[ 1, 2, 3, 'hello' ]

第三种:for循环+include() +push()

思路:建立新数组、使用for循环遍历,再使用include()判断该值是否存在于新数组,若为false则使用push()加入新数组

1、include()用来判断一个数组是否包含一个指定的值,如果是返回 true,否则false。

var arr = [1, 2, 3, 3, 2, 'hello', 'hello'];
 
function unique(arr) {
  let newArr = [];
  for (let i = 0; i < arr.length; i++) {
    // 判断是否存在于新数组中
    if (!newArr.includes(arr[i])) {
      newArr.push(arr[i])
    }
  }
  return newArr
}
console.log(unique(arr));

第四种:forEach()遍历+indexOf+push() [同理也可以foreach+lastIndexOf+push]

var arr = [1, 2, 3, 3, 2, 'hello', 'hello'];
 
function unique(arr) {
  let newArr = [];
  arr.forEach(function (item) {
    if (newArr.indexOf(item) === -1) {
      newArr.push(item)
    }
  })
  return newArr;
}
console.log(unique(arr));

第五种:filter()+indexOf/includes()+push()

1、filter():创建一个新的数组,新数组中的元素是通过检查指定数组中符合条件的所有元素。

2、indexOf()/includes():例2、例3有,不做过多阐述。

var arr = [1, 2, 3, 3, 2, 'hello', 'hello'];
 
function unique(arr) {
  let newArr = [];
  newArr = arr.filter(function (item) {
    //三目运算符,
    return newArr.includes(item) ? "" : newArr.push(item)
  })
  return newArr;
}
console.log(unique(arr));

第六种:set()+Array.from()

1、Set 对象允许你存储任何类型的唯一值,无论是原始值或者是对象引用

2、Array.from():将一个类数组对象或者可遍历对象转换成一个真正的数组

var arr = [1, 2, 3, 3, 2, 'hello', 'hello'];
let set = new Set(arr); //set()数组去重之后变成类数组
console.log(Array.from(set)); //Array.from()将它变为数组

第七种:利用数组的sort方法去重(相邻元素对比法)

var arr =  [5,7,1,8,1,8,3,4,9,7];
function unique( arr ){
	arr = arr.sort();

	var arr1 = [arr[0]];
	for(var i=1,len=arr.length;i<len;i++){
		if(arr[i] !== arr[i-1]){
			arr1.push(arr[i]);
		}
	}
	return arr1;
}
console.log(unique(arr));   //  1, 1, 3, 4, 5, 7, 7, 8, 8, 9

第八种:利用函数递归去重

var arr = [1,1,5,6,0,9,3,0,6]
function unique(arr){
	var arr1 = arr;
	var len = arr1.length;
	arr1.sort((a,b)=>{
		return a-b
	})
	function loop(index){
		if(index >= 1){
			if(arr1[index] === arr1[index-1] ){
				arr1.splice(index,1);
			}
			loop(index - 1);  // 递归loop,然后数组去重
		}
	}
	loop(len-1);   
	return arr1
}
console.log(unique(arr));    // 0, 1, 3, 5, 6, 9

第九种:利用ES6中的Map方法去重

/*
创建一个空Map数据结构,遍历需要去重的数组,把数组的每一个元素作为key存到Map中。由于Map中不会出现相同的key值,所以最终得到的就是去重后的结果。
*/
let arr = [1, 0, 8, 3, -9, 1, 0, -9, 7]
function unique(arr) {
	let map = new Map();
	// console.log(map)
	//let arr1 = new Array();      // 数组用于返回结果
	let arr1 = [];
	for (let i = 0, len = arr.length; i < len; i++) {
		if (map.has(arr[i])) {      // 判断是否存在该key值
			map.set(arr[i], true);
		} else {
			map.set(arr[i], false);
			arr1.push(arr[i]);
		}
	}
	return arr1;
}
console.log(unique(arr)); // 1, 0, 8, 3, -9, 7

总结:第一种是使用双层for循环+splice(),第二种for循环+indexOf()/include +push(),第三种是forEach()+indexOf/include+push(),第四种是filter()+indexOf/includes()+push(),第五种是set(),第六种map(),第七种是利用数组的sort方法去重,第八种是利用函数递归去重

跨域

什么是跨域?

由于浏览器同源策略,凡是发送请求url的协议、域名、端⼝三者之间任意⼀与当前⻚⾯地址不同即为跨域。

存在跨域的情况

⽹络协议不同,如http协议访问https协议。

端⼝不同,如80端⼝访问8080端⼝。

域名不同,如qianduanblog.com访问baidu.com。

⼦域名不同,如abc.qianduanblog.com访问def.qianduanblog.com。

域名和域名对应ip,如www.a.com访问20.205.28.90.

跨域的解决方案

1.proxy代理(vue中常⽤)

2.jsonp

3.⽣产环境上⽤nginx反向代理

call,apply,bind 三者用法和区别

call,apply,bind 三者都是用来更改this指向的

区别:

call是接收若干个参数,apply是接收数组

bind需要主动调用

方法.call(对象,参数1,参数2,...);

方法.apply(对象,[参数1,参数2,...]);

bind()会返回一个函数,需要主动调用才能执行

请说出三种降低页面加载时间的方法

  1. 压缩css、js⽂件

  2. 合并js、css⽂件,减少http请求

  3. 外部js、css⽂件放在最底下

  4. 减少dom操作,尽可能⽤变量替代不必要的dom操作

promise

概念

Promise 是⼀个对象,它代表⼀个异步操作的最终完成或失败。由于它的 then ⽅法和 catch、finally ⽅法会返回⼀个新的 Promise,所以可以允许我们链式调⽤,解决了传统的回调地狱问题。

promise有几种状态,什么时候会进入catch

三个状态:pending、fulfilled、reject

当pending变为rejectd时,会进入catch

Promise 中then里面的第二个函数和 catch 处理上有什么区别

then里面的第二个函数无法捕获本轮的错误,而catch可以

promise实例中的代码是同步的还是异步的?

同步

promise的then方法为什么可以链式调用?

由于 then方法 会返回 promise,它们可以被链式调用

你可以传递一个匿名函数给 then,并且,如果它返回一个 Promise,一个等价的 Promise 将暴露给后续的方法链。

promise源码

const PENDING = "PENDING";
        const FULFILLED = "FULFILLED";
        const REJECTED = "REJECTED";
        class MyPromise {
            constructor(fn) {
                this.status = PENDING;
                this.value = undefined;//存放resolve传递出来的值
                this.resovleArr = [];
                this.rejectArr = []
                fn(this.resolve, this.reject);
            }
            resolve = (res) => {

                setTimeout(() => {
                    if (this.status === PENDING) {
                        this.status = FULFILLED;
                        this.value = res;
                        this.resovleArr.forEach(cb => {
                            cb(res)
                        })
                    }
                })
            }
            reject = (res) => {


                if (this.status === PENDING) {
                    this.status = REJECTED
                    this.value = res;
                    this.rejectArr.forEach(cb => {
                        cb(res)
                    })
                }
            }
            then(onFulfilled, onRejected) {
              //每一个then都要返回Promise,所以return一下
                return new MyPromise((resolve, reject) => {

                    /**************************** 加入如下判断*******************************/
                    onFulfilled = typeof (onFulfilled) == "function" ? onFulfilled : () => { }
                    onRejected = typeof (onRejected) == "function" ? onRejected : () => { }
                    /*******************************************************************/
                    if (this.status === PENDING) {
                        this.resovleArr.push(onFulfilled);
                        this.rejectArr.push(onRejected)
                    }
                    if (this.status === FULFILLED) {
                        setTimeout(() => {
                            onFulfilled(this.value)
                        })

                    }
                    if (this.status === REJECTED) {
                        onRejected(this.value)
                    }
                })

            }
        }

new的核心原理

arguments 实参的集合(不是数组,但是类似数组,有length,也可以用下标找到其中的数据),当函数参数个数无法确定的时候,用arguments

function New(Fn) {
            //创建一个空对象
            let obj = {}
            //取出除了构造函数的其他参数
            //类数组是可以当作数组来使用的对象,arguments是类数组不具备slice方法
            let arg = Array.prototype.slice.call(arguments, 1)//没看懂这句
            //将obj的原型指向构造函数
            obj.__proto__ = Fn.prototype
            obj.__proto__.constructor = Fn
            //调用函数生成属性
            Fn.apply(obj, arg)
            return obj
        }

        function Student(name, age) {
            this.name = name;
            this.age = age
        }

        var a = New(Student, "张三", "18");
        console.log(a)

JS中的深拷⻉与浅拷⻉

区别

  1. 深拷⻉递归地复制新对象中的所有值或属性,⽽浅拷⻉只复制引⽤。

  2. 在深拷⻉中,新对象中的更改不会影响原始对象,⽽在浅拷⻉中,新对象中的更改,原始对 象中也会跟着改。

  3. 在深拷⻉中,原始对象不与新对象共享相同的属性,⽽在浅拷⻉中,它们具有相同的属性。

slice⽅法和concat⽅法不能彻底的复制数组,如果是⼆维数组就⽆法深复制

下⾯案例证明slice不是深拷⻉

var arr1=[1,2,3,['1','2','3']];
var arr2=arr1.slice(0);
arr1[3][0]=0;
console.log(arr1);//[1,2,3,['0','2','3']]
console.log(arr2);//[1,2,3,['0','2','3']]

如何实现深拷⻉

  1. JSON.stringfy JSON.parse
var arr1 = ['red','green'];
var arr2 = JSON.parse(JSON.stringify(arr1));//复制
console.log(arr2)//['red','green'];
arr1.push('black') ;//改变color1的值
console.log(arr2)//["red", "green"]
console.log(arr1)//["red", "green", "black"]
  1. 递归
function deepClone(obj){
//判断参数是不是⼀个对象
let objClone = obj instanceof Object?[]:{};
if(obj && typeof obj==="object"){
  for(key in obj){
    if(obj.hasOwnProperty(key)){
    //判断ojb⼦元素是否为对象,如果是,递归复制
      if(obj[key]&&typeof obj[key] ==="object"){
      objClone[key] = deepClone(obj[key]);
        }else{
      //如果不是,简单复制
      objClone[key] = obj[key];
      }
    }
  }
}
return objClone;
  } 
var a ={
  x:1,
  y:2
};
b=deepClone(a);
a.x=3
console.log(a);
console.log(b);

从打开浏览器输⼊⽹址,到⻚⾯显示出来经历了什么?

1- 输⼊⽹址:那肯定是输⼊你要访问的⽹站⽹址了,俗称url;

2- 缓存解析:它先去缓存当中看看有没有,从 浏览器缓存-系统缓存-路由器缓存 当中查看,如

果有从缓存当中显示⻚⾯,然后没有那就进⾏步骤三;

缓存就是把你之前访问的web资源,⽐如⼀些js,css,图⽚什么的保存在你本机的内存或

者磁盘当中

3- 域名解析:域名到IP地址的转换过程。域名的解析⼯作由DNS服务器完成。解析后可以获取域

名相应的IP地址

4- tcp连接:在域名解析之后,浏览器向服务器发起了http请求,tcp连接,三次握⼿建⽴tcp连

接。TCP协议是⾯向连接的,所以在传输数据前必须建⽴连接

5- 服务器收到请求:服务器收到浏览器发送的请求信息,返回⼀个响应头和⼀个响应体。

6-⻚⾯渲染:浏览器收到服务器发送的响应头和响应体,进⾏客户端渲染,⽣成Dom树、解析

css样式、js交互。

说⼀下事件代理/事件委托

事件委托是指将事件绑定到⽬标元素的⽗元素上,利⽤冒泡机制触发该事件

描述事件循环

程序分为同步任务和异步任务,程序执行时遇到同步任务会优先执行,等执行完同步任务再去从事件队列中执行异步代码。遇到异步任务就会存放在事件队列中,遇到异步任务分为宏任务和微任务每次宏任务执行完需要清除本轮所有的微任务,然后再从事件队列里取出下一个任务,以此反复形成事件循环

线程和进程

  1. 线程是最⼩的执⾏单元,进程是最⼩的资源管理单元

  2. ⼀个线程只能属于⼀个进程,⽽⼀个进程可以有多个线程,但⾄少有⼀个线程(⼀般情况)

  3. ⼀个进程对应多个线程最为常⻅,Linux、Windows等是这么实现的.其实理论上这种关系并不

是⼀定的,可以存在多个进程对应⼀个线程,例如⼀些分布式操作系统的研究使⽤过这种⽅式,让线

程能从⼀个地址空间转移到另⼀个地址空间,甚⾄跨机器调⽤不同的进程⼊⼝

⽤正则表达式实现千分位分隔符

function format(number){
  let reg =/(\d{1,3})(?=(\d{3})+(.\d*)?$)/g;
  return number.toString().replace(reg,'$&,');
}

image.png

数组扁平化

数组扁平化就是把多维数组转化成一维数组

1.通过es5递归实现

let result = [];
 function fn(ary) {
    for(let i = 0; i < ary.length; i++) }{
    let item = ary[i];
    if (Array.isArray(ary[i])){
    fn(item);
    } else {
        result.push(item);
        }
    }
}

当拿到一个多维数组时,先用isArray判断多维数组中的每一项是不是数组,如果是数组,就调用fn函数,利用for循环来拿到数组中的每一项形成新数组,如果不是数组,就直接将该值添加到新数组里,最终形成一维数组

2.es6 +reduce+递归实现

let arr=[[1,2,3],4,5,[6,7,[8,9]]];
function bianping(arr){
    return arr.reduce((res,item) =>{
        return res.concat(Array.isArray(item)?bianping(item):item)
    },[])
}
console.log(bianping(arr));

reduce() 方法对数组中的每个元素执行一个由您提供的reduce函数(升序执行),将其结果汇总为单个返回值。

问题:reduce方法的作用是什么?

把对象存到本地存储里有什么办法?

本地存储只能存字符串,要想存对象需要把对象转成字符串

防抖和节流

节流: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效

防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <button id="btn">防抖</button>
    <script>
        var btn = document.getElementById("btn");
        //要做的事1
        function one(){
            console.log(1);
        }

         //要做的事2
         function two(){
            console.log(2);
        }

        //封装的防抖函数
        function dobounce(fn){
            var timer=null;
            return function(){
                clearTimeout(timer);
                //定时器是延缓了点击事件的运行
                timer=setTimeout(function(){
                    fn();
                },500)
            }
        }
        //dobounce() 的调用结果就是一个函数
        btn.onclick=dobounce(one);
    </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <button id="btn">节流</button>
    <script>
        //节流:用一个一次性定时器来延缓运行

        var btn = document.getElementById("btn");
        //要做的事1
        function one(){
            console.log(1);
        }

         //要做的事2
         function two(){
            console.log(2);
        }

        function throttle(fn){
            var flag=true//flag = true门是开的
            return  function(){
            if(!flag){
                return;
            }
            flag = false;//门关上了,但是这个进来的人还能往下运行

            setTimeout(function(){
                flag = true;//这个人运行完再关上
            },500)
        }
    }
    btn.onclick=throttle(one);
    </script>
</body>
</html>

数组的常用方法

push()方法:在数组后面添加元素,并且返回数组的最新长度

pop()方法:删除数组最后一个元素,并且将删除的元素返回

unshift()方法:在数组开头添加元素,并且返回数组的最新长度

shift()方法:删除数组中第一个元素,并且将该元素返回

forEach()方法:遍历数组

slice()方法:取到该数组的指定位置的元素

splice()方法:用于删除数组中的指定元素,该方法会影响到原数组,会将指定元素从原数组中删除,并将被删除的元素作为返回值返回

concat()方法:可以连接两个或多个数组,并将新的数组返回,该方法不会对原数组产生影响

join()方法:可以将数组转换为一个字符串

reverse()方法:用来反转数组,该方法会直接修改原数组

sort()方法:可以用来对数组中的元素进行排序,也会影响原数组,默认会按照Unicode编码进行排序

手写instanceof源码

function _instanceof(child,parent){
            var left = child.__proto__;
            var right=parent.protptype;
            while(true){
                if(left===null){
                    return false;
                }
                if(left===right){
                    return true;
                }
                left=left.__proto__;
            }
              
        }

typeof运算符的结果一共有哪些可能,并说出与instanceof的区别

boolean string number object function undefined(是安全的) symbol ()

与instanceof的区别:typeof无法检测出数组和对象

事件对象和this关键字有什么共同点和不同点

不同点:

  1. 事件对象是在事件发生的时候默认的第一个参数,this就代表当前对象

  2. 事件对象永远指向事件触发的那个对象(事件源),this会随着冒泡而更改

相同点:

在没有冒泡的情况下,在事件发生的时候,this和事件对象都代表当前触发事件的对象

for in 和 for of 的异同点

相同点:都用于循环

不同点:

for in遍历的是key,for of遍历的是元素值

数组和set之间如何相互转化 数组和map之间如何相互转化

数组和set之间相互转化:1. 把数组对象转换为set对象

var arr = [1,2,3,4,5,6,7,6,6,7];

var s = new Set(arr);

console.log(s);

  1. 把set对象转为数组

var s = new Set([1,2,3,4]);

var arr2 = [...s];

console.log(arr2);

数组和map之间相互转化:

  1. 把数组对象转换为map对象

const arr = ["foo","bar","baz"];

const arrChangeMap = (arr) => new Map(arr.map( (value,key) => [key,value]));

console.log(arrChangeMap(arr));

  1. 把map对象转为数组

const map = new Map();

map.set(1,"23").set(2,"32").set(3,"45");

const arr = [...map];

console.log(arr);