JS常见面试题

504 阅读14分钟

介绍JS的基本数据类型

UndefinedNullNumberStringBoolean es6新增Symbol

JS有哪些内置对象

objectjs中所有对象的父对象

数据封装类对象:Object Array Boolean Number String

其他对象 Function Arguments Math Date Error RegExp

JS有几种类型的值

原始数据类型string undefined boolean number null

引用数据类型function array object

两者存储位置不同

原始数据类型储存在栈中 占据空间小,大小固定

引用数据储存在堆中 占据空间大,大小不固定

JS原型 原型链

参考 juejin.cn/post/684490…

每个对象都会初始化一个属性就是prototype原型)。当我们访问一个对象的属性时,如果该对象内部没有这个属性,就会去prototype里面找。而这个prototype又有自己的prototype。这样一直找下去,也就是我们所说的原型链

作用域,作用域链,闭包

作用域

决定了代码区块中变量和其他资源的可见性

全局作用域:
    1. 最外层函数外面定义的变量拥有全局作用域
    2. 所有末定义直接赋值的变量自动声明为拥有全局作用域
    3. 所有window对象的属性拥有全局作用域
函数作用域:
    函数作用域是指声明在函数内部的变量
    和全局作用域相反,局部作用域一般只在固定的代码片段内可访问到
块级作用域:
    块级作用域可通过let和const声明

作用域链

全局函数无法查看局部函数的内部细节,但局部函数可以查看上一层,直至全局细节。
当需要从局部函数查找某一属性或方法时。如果当前作用域没找到,就会追溯到上一层,
直至全局函数。这种组织形式就是作用域链

闭包

闭包就是两个函数嵌套,内部函数引用外部函数变量
function fn1(){
    let a = "1"
    let fn2 =function (){
        console.log(a)
    }
    return fn2
}
let fn3 = fn1() 

优点

  • 变量长期驻扎在内存中;
  • 避免全局变量的污染;
  • 私有成员的存在; 缺点
  • 容易造成内存泄漏

call apply bind三者用法以及区别

参考 juejin.cn/post/684490…

call和apply的区别:

  • 它们的共同点是,都能够变函数体内的 this 指向,将一个对象的方法交给另一个对象来执行,并且是立即执行的。

  • 它们的写法也很类似,调用 callapply 的对象,必须是一个函数 Function

  • 它们的区别,主要体现在参数的写法上。

call和aplly的第一个参数都是代替Function类里this对象,
call之后接收的是参数列表,而apply接收的是数组或者类数组的值。
它们的返回值都是调用有指定this值和参数的函数的结果

bind把thisArg作为this参数传递给目标函数,返回一个新的函数。
  • bind 方法 与 applycall 比较类似,也能改变函数体内的 this 指向。不同的是,bind 方法的返回值是函数,并且需要稍后调用,才会执行。而 applycall 则是立即调用。

  • 如果 bind 的第一个参数是 null 或者 undefinedthis 就指向全局对象 window

总结:

callapply 的主要作用,是改变对象的执行上下文,并且是立即执行的。它们在参数上的写法略有区别。 bind 也能改变对象的执行上下文,它与 callapply 不同的是,返回值是一个函数,并且需要稍后再调用一下,才会执行。

this问题

参考 juejin.cn/post/684490…

如果要判断一个运行中函数的 this 绑定, 就需要找到这个函数的直接调用位置。

  • 函数是否在new中调用(new绑定),如果是,那么this绑定的是新创建的对象。

  • 函数是否通过call,apply调用,或者使用了bind(即硬绑定),如果是,那么this绑定的就是指定的对象。

  • 函数是否在某个上下文对象中调用(隐式绑定),如果是的话,this绑定的是那个上下文对象。一般是obj.foo()

  • 如果以上都不是,那么使用默认绑定。如果在严格模式下,则绑定到undefined,否则绑定到全局对象。

  • 如果把null或者undefined作为this的绑定对象传入callapply或者bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。

  • 如果是箭头函数,箭头函数的this继承的是外层代码块的this

JS实现继承的几种方式

参考 juejin.cn/post/691421…

  1. 原型链
  2. 借用构造函数
  3. 组合继承
  4. 原型式继承
  5. 寄生式继承
  6. 寄生组合式继承

new做了什么:

  1. 创建了一个全新的对象。
  2. 这个对象会被执行[[Prototype]](也就是__proto__)链接。
  3. 生成的新对象会绑定到函数调用的this
  4. 通过new创建的每个对象将最终被[[Prototype]]链接到这个函数的prototype对象上。
  5. 如果函数没有返回对象类型Object(包含Functoin, Array, Date, RegExg, Error),那么new表达式中的函数调用会自动返回这个新的对象。

JS浅拷贝深拷贝

参考 juejin.cn/post/684490…

  • 浅拷贝:创建一个对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址,所以其中一个对象改变了地址,就会影响到另外一个对象
  • 深拷贝: 拷贝了多层,即使嵌套对象也会都拷贝出来。更改原对象,拷贝的对象不会发生变化

浅拷贝实现方式

1. Object.assgin

var a = {};
var b = {id:1};
Object.assign(a,b);
console.log(a); // {id:1}
b.id = 2;
console.log(a); // {id:1}
console.log(b); // {id:2}

2. 扩展运算符

var obj1 = {
	a:1,
	b:{
		c:1
	}
}
var obj2  = {...obj1}
obj1.a = 2
console.log(obj1) // a为2
console.log(obj2) // a为1

obj1.b.c =2
console.log(obj1) // a为2 c为2
console.log(obj2) // a为1 c为2

综上,浅拷贝只在根属性上在堆内存中创建了一个新的对象,复制了基本类型的值。而深拷贝则是对于复杂数据类型在堆内存中开辟了一块内存地址用于存放复制的对象,并把原有的对象复制过来。两个对象相互独立,也就是两个不同的地址。

深拷贝实现方式

1. 一个简单的深拷贝

var obj = {
	a:{
		b:1
	},
	c:1
}
var obj2 = {}
obj2.a = {}
obj2.c = obj.c
obj2.a.b = obj.a.b
console.log(obj2) //b1 c1
obj2.a.b = 2
console.log(obj) // b1 c1
console.log(obj2) //b2 c1

2. JSON.stringify()

var obj = {
	a:1,
	b:[1,2,3]
}
var str = JSON.stringify(obj) // 把对象内容转换为字符串形式保存在磁盘上

var obj1 = JSON.parse(str) // 反序列化将JSON字符串转换为一个对象

console.log(obj1)

obj1.a = 2
console.log(obj1) // a:2
console.log(obj)  // a:1

这种方法虽然可以实现数组或对象深拷贝,但不能处理函数和正则,
因为这两者基于JSON.stringifyJSON.parse处理后,得到的正则就不再是正则(变为空对象),
得到的函数就不再是函数(变为null)了。

Promisse

由于JS是单线程的,而浏览器是多线程的。promisse就可以使浏览器具有异步操作,它其实就是异步编程的一种解决方案。从语法上将,它是一个对象,可以获取异步的消息。从本意上讲,它是一个承诺,承诺过一段时间会给你一个结果。promises有三状态:pending, fulfiled, rejected;状态一旦发生改变,就不会再变,创造promisse实例后,它会立即执行

promisse主要用来解决两个问题

  • 回调地狱,代码难以维护。
  • 支持多个并发请求,获取并发请求中的数据

promisse实现:

let p = new Promisse((resolve, reject) => {
    setTimeout(() => {
        let a = 1
        if(a === 1){
            resolve(a)
        }else{
            reject('错误')
        }
    },1000)
})
p.then((res) => {
}).catch(err => {})

Promisse.all 谁跑的慢,以谁为准执行回调

let a = new Promisse((resolve, reject) => {})
let b = new Promisse((resolve, reject) => {})
let c = new Promisse((resolve, reject) => {})
Promisse.all([a,b,c]).then((res) => {
}).catch(err => {})

Promisse.race 谁跑的快,以谁为准执行回调

 //请求某个图片资源
    function requestImg(){
        var p = new Promise((resolve, reject) => {
            var img = new Image();
            img.onload = function(){
                resolve(img);
            }
            img.src = '图片的路径';
        });
        return p;
    }
    //延时函数,用于给请求计时
    function timeout(){
        var p = new Promise((resolve, reject) => {
            setTimeout(() => {
                reject('图片请求超时');
            }, 5000);
        });
        return p;
    }
    Promise.race([requestImg(), timeout()]).then((data) =>{
        console.log(data);
    }).catch((err) => {
        console.log(err);
    });

async await 原理及其简单应用

async/await构建于promise之上。是promise的语法糖,好处是可以在try catch里面捕获任意错误

  1. 自动将常规函数转为promisse
  2. 异步函数允许使用await

一个简单的例子:当我们需要的从服务器获取数据时

用promise
function getData(){
    return new Promise((resolve,reject) => {
    axios.get(url).then((res) => {
        resolve(res)
    })
    })
}

用async/await
async function getData(){
    let res = await axios.get(url);
    return  res
}

Eventloop

JS代码执行的过程中,除了依靠函数调用栈来搞定函数的执行顺序外,还依靠任务队列(task queue)来搞定另外一些代码的执行。整个执行过程中,我们称为事件循环过程。一个线程中,事件循环是唯一的,但是任务队列又可以拥有多个。任务队列又分为macro-task(宏任务)和micro-task(微任务),在最新标准中,它们被称为taskjobs

  • macro-task 大概包括
  1. script(整体代码)
  2. setTimeout
  3. setInterval
  4. setImmediate
  5. I/O
  6. UI render
  • micro-task大概包括
  1. process-nextTick
  2. Promise
  3. async/await
  4. MutationObserver

执行过程 执行宏任务,执行该宏任务下的微任务,若微任务执行下产生了新的微任务,就继续执行微任务,微任务执行完后,再回到宏任务进行下一轮循环

数组去重

更多详见 juejin.cn/post/684490…

  • 利用ES6 Set去重
  • 利用for嵌套for,然后用splice去重
  • 利用indexOf
  • 利用sort
  • 利用includes
  • 利用hasOwnProperty
  • 利用filter
  • 利用Map数据结构 1. new Set()
function unique(arr){
    // Array.from给一个类似数组或可迭代对象,创建一个新的浅拷贝实例
    return Array.from(new Set(arr))
    // Set对象,允许储存任何类型的唯一值,成员不可重复
}

2. 利用for嵌套for,然后splice去重

NaN和{}没有去重,两个null直接消失了
function unique(arr){
    for(var i = 0; i < arr.length; i++){
        for(var j = i+1; j < arr.length; j++){
            if(arr[i] == arr[j]){
                arr.splice(j , 1)
                j--
            }
        }
    }
    return arr
}

JS数组方法

参考 juejin.cn/post/684490…

函数防抖和节流

参考 juejin.cn/post/684490…

1. 防抖

当一个动作连续触发,只执行一次

2. 节流

限制一个函数在一定时间内值执行一次

JS处理错误

参加 juejin.cn/post/687286…

  1. try/catch/finally
  2. generator 函数
  3. 定时器的错误处理
  4. onerror
  5. 使用 Promise 处理错误
  6. 使用 async/await 来处理错误

数据类型判断

参考 juejin.cn/post/684490…

typeof

返回的数据类型包含7种:number,boolean,symbol,string,object,undefined,function

toString

它是object的原型方法,调用该方法默认返回[object xxx],其中xxx就是对象的类型
Object.prototype.toString.call(1) // [object Number]

constructor

它是原型的一个属性,nullundefined无法使用它判断。

instanceof

判断a是否为b的实例,返回truefalse

arguments对象及其应用

概念:函数内置的一个类数组对象,存储了所有实参。

  1. 形参只提供便利,但不是必须的
  2. argumentslength由实参个数决定
  3. 每个实参与arguments[i]一一对应
  4. arguments与形参访问的内存空间是独立的

事件捕获、事件冒泡、事件委托(代理)

  • 事件捕获:当一个事件触发后,从window对象触发,不断经过下级节点,直到目标节点。这过程就是捕获阶段,所有经过的节点,都会触发对应事件。
  • 事件冒泡:当事件达到目标节点后,会沿捕获阶段的路线原路返回。同样,所有经过的节点都会触发对应事件

但是如果我们不希望事件冒泡呢?那么如何阻止事件冒泡? 实际上,事件的对象有一个stopPropagation()方法可以阻止事件冒泡,我们只需要把例子中button的事件处理程序修改如下:

document.getElementById("button").addEventListener("click",function(event){
    alert("button");
    event.stopPropagation();    
},false);
  • 事件委托(代理)利用事件冒泡,只指定一个事件处理程序,来管理某一类型的所有事件

事件委托作用

  1. 支持为同一个DOM元素注册多个同类型事件
  2. 可将事件分成事件捕获和事件冒泡机制

如何让事件先冒泡后捕获

在DOM标准事件中,是先捕获再冒泡。想要实现先冒泡再捕获。
可以对同一事件监听捕获和冒泡,监听到捕获事件后先暂缓执行,
直到冒泡事件被捕获后再进行捕获事件。

onloadDOMContentLoaded区别理解

当onload事件触发时,页面页面上所有的DOM,样式表,脚本,图片,flash都已经加载完成。而DOMContentLoaded事件触发时,仅DOM加载完成。

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

for..in实际上遍历的是对象的属性名称。for..in在遍历数组时有可能得到字符串索引,而不是数字索引,如下

var arr = ['a', 'b', 'c']
arr.name = 'tom'
for(var x in arr){
    console.log(x) // '0','1','2','tom'
}
for..in循环把name也包含在内。而for..of解决了这一问题

原生Ajax写法

get请求

function Ajax(url) {
    var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : ActiveXObjext('microsoft.XMLHttp')
    xhr.open('get', url, true)
    xhr.send()
    xhr.onreadystatechange = () => {
        if(xhr.reayState == 4){
            if(xhr.readyState == 200){
                var data = xhr.responseText
                return data
            }
        }
    }
}

post请求

function Ajax(url, data){
    var xhr = window.XMLHttpRequset ? new XMLHttpRequest() : ActiveXObject('microsoft.XMLHttp')
    xhr.open('post', url true)
    xhr.setRequestHeader('content-type','xxx')
    xhr.send(data)
    xhr.onreadystatechange = () => {
        if(readyState == 4){
            if(readyState == 200){
                var list = xhr.reponseText
                return list
            }
        }
    }
}

函数柯里化

定义:把接受多个参数的函数变为接受一个单一的参数,并返回余下的参数,而且返回结果的新函数技术

  • 简单实现
function add(x,y){
	return x + y 
}
add(1,2)

// 柯里化后
function add2(x){
	return function(y){
		return x + y
	}
}
add2(1)(2)
  • 经典应用(实现一个add方法,并满足add(1)(2)(3)=6,add(1)(2)(3)(4)=10)
function add(){
	var args = Array.prototype.slice.call(arguments) // 定义一个数组用来存储所有参数

	var Foo = function(){    //声明一个函数,利用闭包特性保存args并收集所有参数值
		args.push(...arguments)
		return Foo
	}

	// 当左后执行时隐式扎UN哈UN,并计算最终值返回
	Foo.toString = function(){
		return args.reduce(function(a,b){
			return a + b
		})
	}
	return Foo
}
add(1)(2)(3)
add(1)(2)(3)(4)
add(1)(2)(3)(4)(5)

立即执行函数

定义

声明一个函数,并马上调用这个匿名函数就叫做立即执行函数

写法

(function(){})() (function(){}())

作用

  1. 不必为函数命名,避免污染全局变量
  2. 立即执行函数内部形成一个单独的作用域,可以封装外部无法读取的私有变量
  3. 封装变量

常用BOM属性、对象、方法

BOM即javascript可以进行操作浏览器各个功能部件的接口

window对象

  1. window方法
window.confirm() 确认
window.open() 打开新窗口
window.close 关闭窗口
  1. window属性
closed opener

navigator对象

  1. navigator对象属性
navigator.appName 浏览器名称
navigator.online 系统是否处于脱机状态
navigator.userAgent 返回客户端完整信息

screen对象

  1. screen对象属性
deviceXDPI 返回显示屏幕的每英寸水平点数

history对象

  1. history对象属性
length 在同一个标签页,跳转了多少次,length就是多少
  1. history对象方法
back()
forward()
go()

location对象

  1. location对象属性
host
hostname
protocol
search
hash

什么是栈什么是堆,它们之间的区别和联系

堆和栈的概念存在于数据结构和操作系统中

  • 在数据结构中,栈中数据存取方式是先进后出。而堆是一个优先队列,是按照优先级来排序的。
  • 在操作系统中,内存被分为栈区和堆区

区别:

  • 栈去内存一般由编译器自动分配释放
  • 堆区内存一般由程序员分配释放

javascript垃圾回收机制

参考 juejin.cn/post/684490…

两种方法:标记清除、引用计数

线程和进程的关系

进程(process)和线程(thread)是操作系统的基本概念

  1. 计算机的核心是CPU,它承担了所有的计算任务

  2. 单个CPU一次只能运行一个任务

  3. 进程它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态

  4. 一个进程可以包括多个线程。

  5. 一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。

  6. 一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。

  7. 一个防止其他线程使用的简单方法"互斥锁"(Mutual exclusion,缩写 Mutex),防止多个线程同时读写某一块内存区域。

  8. 某些内存区域,只能供给固定数目的线程使用。

操作系统的设计,因此可以归结为三点:

(1)以多进程形式,允许多个任务同时运行;

(2)以多线程形式,允许单个任务分成不同的部分运行;

(3)提供协调机制,一方面防止进程之间和线程之间产生冲突,另一方面允许进程之间和线程之间共享资源。

js如何创建对象

参考 juejin.cn/post/684490…