阅读 8987

面试必备!JS高频面试题汇总

阿里内推二维码,需要的自取

image.png

一、js中的数据类型

基本类型:number string boolean null undefined symbol bigint

引用数据类型:object (包含,Date,RegExp,Function,Array,Math..)

二、symbol的作用

首先说明symbol是基本类型之一,symbols 是一种无法被重建的基本类型。这时 symbols 有点类似与对象创建的实例互相不相等的情况,但同时 symbols又是一种无法被改变的基本类型数据。

const s1 = Symbol();
const s2 = Symbol();
console.log(s1 === s2);//false
复制代码

可以看到symbol创建出来的值互不相同,即使传入相同的参数,也不相同,同时要注意,symbol不是被实例化出来的,不用new创建。 所以symbol可以用作

  1. 作为对象的属性名,可以保证属性名不会重复。但要注意,symbol不能过通过for... in...遍历出来
const s1 = Symbol('a');
const s2 = Symbol('a');


var obj = {};
obj['a'] = "aaa"
obj[s1] = "asjdkl"
obj[s2] = "u can not see me"
for (const key in obj) {
    console.log(key);

}
//a
复制代码

如果想获取,可以通过Object.getOwnPropertySymbols()来获取

for (const key of Object.getOwnPropertySymbols(obj)) {
    console.log(key);

}
//Symbol(a)
//Symbol(a)
复制代码

三、类型转换

基本就是看代码得出结果,但是要完全理解背后的原理,推荐看冴羽的博客 github.com/mqyqingfeng…

四、如何判断变量的类型

typeof对于原始类型(除了Null)是可以检测到的,但是引用类型就统一返回object

instance of 用于检测构造函数的原型是否出现在某个实例函数的原型链上

最好的方法是使用 Object.prototype.toString方法,它可以检测到任何类型,返回的结果是[object Type]的形式,基本可以实现所有类型的检测,我们用下面的代码来演示一下。

//实现一个检测类型的公共接口
function detectType(type) {
    return function(obj) {
        return {}.toString.call(obj) === `[object ${type}]`
    }
}
//根据自己的需求进行扩展,记住类型的首字母要大写
const isArray = detectType("Array")
const isFunc = detectType("Function")
const isRegExp = detectType("RegExp")

console.log(isArray([1, 2, 3])); //true
console.log(isArray("[1,2,3]")); //false
console.log(isFunc(detectType)); //true
console.log(isRegExp(/test/)); //true
复制代码

五、this的指向

js中this 的指向大致可以分为以下四个场景

  1. 在对象的方法中使用,this指向当前的对象
var obj = {
    a: "hhh",
    test() {
        return this.a;
    }
}
console.log(obj.test());//'hhh'
复制代码
  1. 在独立的函数中使用
  • 在严格模式下,this指向undefined
  • 非严格模式下,this指向全局对象,比如windo
var a = "jjj"
var obj = {
    a: "hhh",
    test() {
        return this.a;
    }
}
const test = obj.test;
console.log(test());//"jjj"

复制代码
  1. 通过call\apply\bind来指定

三者都可传入一个要改变的this的值,来改变this指向,区别就是call\apply改变的同时执行函数,bind不执行,而是返回这个函数

call\apply 第一个参数就是要改变的this的值,区别就是call传入的是参数列表,apply传入的是参数数组

  1. 构造函数

如果一个函数是构造函数,那么this就指向它实例化出来的对象

  1. 箭头函数

箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this,另外箭头函数里也不能使用call\apply\bind修改this的指向

六、说一下什么是闭包?

闭包是一个可以访问其他作用域的变量函数

产生的原因

首先要了解作用域链的概念,函数的作用域就是它所创建的地方,也就是说,函数在它被创建的时候就已经确定好它的作用域了。

函数在执行的时候遇到一个变量,他会先看看自己的作用域里有没有该变量,没有的话就会向上从父级作用域里去查找,直到找到位置,否则报错undefined

function f1() {
     var a = 1;

    function f2() {
        console.log(a);
    }
    f2();
}
f1()//1
复制代码

所以闭包的本质就是 存在对父级作用域的引用,这里注意,上面的代码并不是闭包,我们并没有通过调用f1()来访问到不属于它作用域的变量,因为a本来就属于f1,我们只是通过f1调用了f2而已。 我们把上面的例子改造成闭包的形式,我们要在外面调用f2

function f1() {
    var a = 1;

    var f2 = function() {
        console.log(a);

    }
    return f2

}
const clousure = f1();
clousure();//1
复制代码

我们把f2作为返回值,在外部进行调用,可以看到我们可以访问到f1的变量,这就是闭包,

也说明f2的作用域就是它所创建时的地方,而不是调用时的地方。

七、解释一下原型链

每一个函数有一个prototype的属性,当他作为构造函数的时候,它实例化出来的函数会有一个_proto_的属性,它执行构造函数的prototype

函数通过prototype来访问其父元素的属性和方法,依此迭代访问,构成原型链,直到Object的原型为止,它位于原型链的顶端。

访问某个属性或方法的时候,会先从当前对象中查找,没有的话就顺着原型链开始寻找,直到找到最顶端,也就是Object的原型(null)为止。

另外每一个原型上都有一个constructor属性,指向相关联的构造函数

八、 DOM事件流和事件委托

DOM事件流分为三个阶段:

  1. 捕获阶段
  2. 目标阶段
  3. 冒泡阶段

  • 捕获阶段:在事件冒泡的模型中,捕获阶段不会响应任何事件;
  • 目标阶段:目标阶段就是指事件响应到触发事件的最底层元素上;
  • 冒泡阶段:冒泡阶段就是事件的触发响应会从最底层目标一层层地向外到最外层(根节点),事件代理即是利用事件冒泡的机制把里层所需要响应的事件绑定到外层

事件流描述的是从页面中接受事件的顺序,IE和网景推出了两个正好相反的概念,IE推出的是冒泡流,从下到上,网景则是事件捕获流,从上到下。

首先通过addEventListener方法给元素添加点击事件,前两个参数分别是点击事件的名称和执行的回调,第三个参数就是是否开启捕获,确定事件发生的阶段,默认是false,也就是冒泡流。

事件委托,一般来说,会把一个或一组元素的时间委托到它的父元素上或者更外层元素上,当事件响应到需要绑定的元素上时,会通过事件冒泡机制从而触发它的外层元素的绑定事件上,然后在外层元素上去执行函数。在一些场景下,可以让性能得到优化。 比如给所有的列表添加点击事件,如果采用冒泡流,那么我们需要给每个元素添加点击事件,而采用事件委托的话,只需要在ul上绑定一个事件即可。

九、js如何实现继承

  1. 通过原型链继承
function Parent() {
    this.lastName = "wang"
}
Parent.prototype.asset = ['house', 'car']

function Child() {

}
Child.prototype = new Parent();
var child = new Child();
var child1 = new Child();
child.asset.push("plane")
console.log(child.lastName);//"wang"
console.log(child1.asset);//[ 'house', 'car', 'plane' ]
复制代码

优点:可以访问父类的属性和方法和原型上的属性和方法 缺点:继承方法如果是引用类型,其中一个子类进行修改,那么全部都会受到影响

  1. 通过call来继承
function Parent() {
    this.lastName = "wang";
    this.hobby = ['a', 'b']
}
Parent.prototype.asset = ['house', 'car']

function Child() {
    Parent.call(this)
}


var child = new Child();
var child1 = new Child();
child.hobby.push("c")
console.log(child.lastName);//“wang"
console.log(child1.hobby);//['a', 'b']
console.log(child1.asset);//undefined

复制代码

优点:可以保证每个子类维护自己的属性 缺点:无法访问原型链上的属性和方法

  1. 组合继承

将前面二者结合

function Parent() {
    this.lastName = "wang";
    this.hobby = ['a', 'b']
}
Parent.prototype.asset = ['house', 'car']

function Child() {
    Parent.call(this)
}
Child.prototype = new Parent();

var child = new Child();
var child1 = new Child();
child.hobby.push("c")

console.log(child.lastName);
console.log(child1.hobby);
//wang
//[ 'a', 'b' ]
//[ 'house', 'car' ]
复制代码

优点:既可以访问原型上的属性和方法,又可以每个子类维护自己属性 缺点:每次创建一个子类实例,父类都会被执行一次(涉及到new的内部实现,详情请看下一话题)

  1. 优化组合继承

将原型赋值语句修改如下

Child.prototype = Parent.prototype;
复制代码

打印child会发现另一个问题

child的构造函数怎么能是Parent呢,所以我们还需要手动的修改一下

Child.prototype = Parent.prototype;
Child.prototype.constructor = Child;

复制代码

另外也可以使用Object.create()这个方法来创建一个指定原型的对象

Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
复制代码

十、new操作符都做了些什么

  1. 创建一个空对象
  2. 将空对象的_proto_属性指向构造函数的原型
  3. 将this指向这个对象
  4. 返回这个对象

代码实现:

function newFactory(constructor) {
    var obj = {};
    obj.__proto__ = constructor.prototype;
    constructor.apply(obj, [].slice.call(arguments, 1));
    return obj
}

function Test(name) {
    this.name = name;
}
var t = newFactory(Test, "tom")
console.log(t.name);//"tom"
复制代码

十一、 let const

let 和 const都是es6的语法,之前的var是没有块级作用域的,所以很容易造成全局变量污染,而let 和 const都是有块级作用域的。 let可以理解为带有块级作用域的var const则是指定常量的,一旦定义就不能再改变。

let 和 cons声明的变量必须之前没有被使用过,否则会报错,即let和const声明的变量,只能在声明之后引用。

另外要注意const的一个点就是,当他定义为一个引用类型时,可不可以往里面添加内容。

经过验证,我们是可以接着向里面添加内容的。 但是不能替换它,因为原始值的话,变量直接引用的是这个值,只要值不相等就不能赋值,而引用类型,变量只是拥有它在内存地址的引用,只要这个引用的地址值没变,我们还是可以对其进行操作的。

十二、 异步编程 promise,async/await

  • promise 的介绍(状态。。)
  • async await的使用

异步编程的方法有

  1. 回调函数
  2. promise
  3. async/await等

回调函数在复杂场景下会造成回调地狱,影响代码的可读性和执行率。

promise是es6对异步编程的一个方案,它有三种状态,pending(挂起),fullfilled(成功),rejected(拒绝),状态一旦改变就不可逆。对应的变化有两种:

pending------> fullfilled (resolved 解决)

pending------> rejected (rejected 拒绝)

通过then这个方法来实现异步调用之后的逻辑,另外还支持链式调用。

async/await是es7的语法,也是用来实现异步编程的,语法是在function关键字前加上async,代表是异步函数,await只能在async函数里使用。

async将任何函数转换为promise,这时异步函数的特征之一。 await 可以使用在任何返回Promise函数的函数之前,并且会暂停在这里,直到promise返回结果才往下进行。

async/awaite基本做到了用同步代码的风格实现异步逻辑,让代码更简洁。

十三、Event Loop 机制

  • 原理
  • 看代码说出执行结果

js是一个单线程语言,所以所有的任务只能排队一个一个去做,这样效率明显很低。 所以event loop就是为解决这个问题而提出的。

在主程序中,分为两个线程,一个运行程序本身,称作主线程,另一个负责主线程和其它线程进行通信(主要是I/O操作),被称作event loop线程

在运行程序的时候,当遇到I/O操作的时候,主线程就让Event loop 线程通知相应的I/O模块去执行,然后主线程接着执行之后的代码,等到I/O结束后,event loop线程再把运行结果交给主线程,然后主线程再执行相应的回调,整个任务结束。

宏任务 微任务

为了让这些任务在主线程上有条不紊的进行,V8采用队列的方式对这些任务进行存储,然后一一取出执行,其中包含了两种任务队列,除了上述提到的任务队列, 还有一个延迟队列,它专门处理诸如setTimeout/setInterval这样的定时器回调任务。 这两种任务队列里的任务都是宏任务

微任务 通常为 应当发生在当前脚本执行完后的事情 做安排,比如对一系列操作做出反应,或者让某些事情异步但是不承担宏任务的代价

微任务的执行有两种方案,一种是等所有宏任务实行完毕然后依次执行微任务,另一种是在执行完一个宏任务之后,检查微任务队列,如果不为空则依次执行完微任务,然后再执行宏任务。

显然后者更满足需求,否则回调迟迟得不到执行,会造成应用卡顿。

常见的宏任务有:setTimeout setTimeInterval 常见的微任务有:"MutationObserver、Promise.then(或.reject) 以及以 Promise 为基础开发的其他技术(比如fetch API), 还包括 V8 的垃圾回收过程"

nextTick

process.nextTick 是一个独立于 eventLoop 的任务队列。

在每一个 eventLoop 阶段完成后会去检查这个队列,如果里面有任务,会让这部分任务优先于微任务执行。

宏任务 微任务有哪些

图源链接

练习1:

console.log('start');
setTimeout(() => {
  console.log('timeout');
});
Promise.resolve().then(() => {
  console.log('resolve');
});
console.log('end');
//start
//end
//resolve
//timeout
复制代码
  1. 首先整个脚本作为宏任务开始执行,遇到同步代码直接执行
  2. 打印start
  3. 将settimeout放入宏任务队列
  4. 将Promise.resolve放入微任务队列
  5. 打印end
  6. 执行所有微任务,打印resolve
  7. 执行宏任务,打印timeout

练习2

setTimeout( () => console.log(4))

new Promise(resolve => {
  resolve()
  console.log(1)
}).then(_ => {
  console.log(3)
})

console.log(2)

//1 2 3 4
复制代码

也就是说 new Promise在实例化的过程中所执行的代码都是同步进行的,而then中注册的回调才是异步执行的。

在同步代码执行完成后才会去检查是否有异步任务完成,并执行对应的回调,而微任务又会在宏任务之前执行

十四 防抖和节流

防抖:事件被调用后,在执行之前无论被调用多少次都会从头开始计时

节流:不管事件被调用多少次,总是按规定时间间隔执行

代码实现:

//防抖
function debounce(fn, time) {
    var timer;
    return function() {
        if (timer) {
            clearTimeout(timer)
            timer = null;

        }
        timer = setTimeout(() => {
            clearTimeout(timer)

            timer = null;
            fn();
        }, time);
    }
}


//节流
function throttle(fn, time) {
    var timer;
    return function() {
        if (timer) return;
        timer = setTimeout(() => {
            clearTimeout(timer)
            timer = null;
            fn();
        }, time);
    }
}
复制代码

十五、 requestAnimationFrame的优势

requestAnimationFrame不需要指定间隔时间,它采用的是系统间隔,一般是1秒60帧,每个16ms刷新一次。 好处:

  1. 将更新在一次回流中全部提交,提升性能
  2. 当页面处于未激活状态时,requestAnimationFrame也会停止渲染,当再次激活时,就会接着上一部分继续执行

虚拟DOM的优缺点

优点

  1. 保证性能下限。 操作真实的dom结构是一件非常昂贵的事情,虚拟Dom利用js对象来模拟真实的dom,从而降低了逻辑层面对dom结构操作的成本
  2. 无需操作真实的dom。 通过双向数据绑定,当数据发生改变的时候,dom结构中的节点自动更新,无需我们手动处理
  3. 可移植性高,跨平台性好。 无论是vue、react还是weex等,我们都能看到虚拟dom的身影,通过各自的渲染进制进行将Dom结构渲染出来

缺点:

  • 无法进行极致优化: 虽然虚拟 DOM+合理的优化,足以应对绝大部分应用的性能需求,但在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化。

十六、 箭头函数

箭头函数有几个使用注意点。

(1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。

(2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。

(3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。

(4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。

其中第一条非常重要,通过几个例子结合上文的this指向 来看一下

function Timer() {
  this.s1 = 0;
  this.s2 = 0;
  // 箭头函数
  setInterval(() => this.s1++, 1000);
  // 普通函数
  setInterval(function () {
    this.s2++;
  }, 1000);
}

var timer = new Timer();

setTimeout(() => console.log('s1: ', timer.s1), 3100);
setTimeout(() => console.log('s2: ', timer.s2), 3100);
// s1: 3
// s2: 0
复制代码

在上面这个例子中,定义了一个构造函数Timer,里面分别有两个定时器,第一个用箭头函数,第二个用普通函数,然后我们在外面定义了两个延时函数来获取s1,s2的值,

首先:箭头函数的this是在它定义时所在对象的值,也就是timer,经过了3.1s结果为3,

然后普通函数的this指向了全局对象,所以timer的s2,没有变还是0。

this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。

十七、手写curry

科里化函数在函数式编程中有很重要的应用,大致类似于链式调用,通过把函数科里化我们可以很好的把传参分开从而达到函数的功能复用,可以通过下面经典的🌰了解一下

function add(a, b) {
	return a + b
}

let addTen = curry(add,10)
addTen(1)//11
addTen(5)//15

let res = curry(add,1,2)//3
复制代码

上面的add函数接受两个参数,我们把它科里化之后,可以将参数拆开进行传递,我们想创建一个工具函数addTen,目的就是把传入的数字+10。当然这个功能完全没有必要通过科里化来实现,但是可以大致了解科里化的作用

下面来编写科里化函数

科里化的原理也很简单,如果我们传入的参数不少于函数的参数,那么会直接执行并返回结果,否则返回一个函数,以供我们后续调用,所以我们可以很快的实现一个简易的curry函数了

function curry(fn,...arg1){
	return arg1.length===fn.length?
    		fn(...arg1):
            function(...arg2){ return fn(...arg1,...arg2)}

}

复制代码

十八、手写异步串行、并行

from 前端面试常见的手写题


// 字节面试题,实现一个异步加法
function asyncAdd(a, b, callback) {
  setTimeout(function () {
    callback(null, a + b);
  }, 500);
}

// 解决方案
// 1. promisify
const promiseAdd = (a, b) => new Promise((resolve, reject) => {
  asyncAdd(a, b, (err, res) => {
    if (err) {
      reject(err)
    } else {
      resolve(res)
    }
  })
})

// 2. 串行处理
async function serialSum(...args) {
  return args.reduce((task, now) => task.then(res => promiseAdd(res, now)), Promise.resolve(0))
}

// 3. 并行处理
async function parallelSum(...args) {
  if (args.length === 1) return args[0]
  const tasks = []
  for (let i = 0; i < args.length; i += 2) {
    tasks.push(promiseAdd(args[i], args[i + 1] || 0))
  }
  const results = await Promise.all(tasks)
  return parallelSum(...results)
}

// 测试
(async () => {
  console.log('Running...');
  const res1 = await serialSum(1, 2, 3, 4, 5, 8, 9, 10, 11, 12)
  console.log(res1)
  const res2 = await parallelSum(1, 2, 3, 4, 5, 8, 9, 10, 11, 12)
  console.log(res2)
  console.log('Done');
})()

复制代码

十九、深拷贝

注意点:

  • 对象循环引用的处理
  • 日期、正则等对象的处理
  • 数组、对象的处理
function deepCopy(obj, cache = new WeakMap()) {
	if (typeof obj !== 'object' || obj === null) return obj
	// 正则 日期等包装类对象 参考 https://blog.csdn.net/liwusen/article/details/78759373
	if ({}.toString.call(obj) === '[object Date]') return new Date(obj.valueOf())
	if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags)
	// 防止循环引用
	if (cache.get(obj)) return cache.get(obj)

	const res = Array.isArray(obj) ? [] : {}
	cache.set(obj, res)
	for (const key in obj) {
		if (typeof obj[key] === 'object') {
			res[key] = deepCopy(obj[key], cache)
		} else {
			res[key] = obj[key]
		}
	}
	return res
}

// 测试
const source = {
	name: 'Jack',
	meta: {
		age: 12,
		birth: new Date('1997-10-10'),
		ary: [1, 2, { a: 1 }],
		say() {
			console.log('Hello')
		}
	}
}
source.source = source
const newObj = deepCopy(source)
console.log(newObj.meta.birth == source.meta.birth)

复制代码

二十、手写 随机生成16进制颜色与rgb互转

function generateRamdomColor() {
	const dic = []
	for (let i = 0; i < 16; i++) {
		dic.push(i.toString(16))
	}

	const type = [3, 6]
	let count = random(type)

	return (
		'#' +
		(function tt(res) {
			return (res += random(dic)) && res.length === count ? res : tt(res)
		})('')
	)
}

function random(arr) {
	return arr[Math.floor(Math.random() * arr.length)]
}
function hex2rgb(hex) {
	const str = hex.slice(1),
		res = []
	if (str.length === 3) {
		for (const w of str) {
			res.push(parseInt(w + w, 16))
		}
	} else {
		for (let i = 0; i < 6; i += 2) {
			res.push(parseInt(str[i] + str[i + 1], 16))
		}
	}
	return res
}
let color = generateRamdomColor()
let rgb = hex2rgb(color)
console.log(color) //random hex color
console.log(rgb)

复制代码

二十一、 BOM 之 location.hash & History

用过vue的同学都知道,路由常用的有hashhistory两种模式(abstract不管,相信没啥人用吧),其实他们就对应着BOM的api,location.hash和History,下面我们简单讨论一下两者的区别:

locatoin.hash

就是url的散列值,#后跟零或多个字符,没有就是空字符串

  • 这个哈希值代表客户端的一种状态,向服务器发送请求的时候,哈希部分不会被发送
  • 哈希值的改变不会触发页面变化
  • 哈希值改变会在浏览器中增加一条记录
  • 可以通过hasChange事件来监听哈希值的变化

History

history对象表示当前窗口首次使用以来用户的导航历史记录

history通常用来创建“前进”“后退”按钮,如果url变化(包括哈希值的改变),都会增加一条历史纪录,这个行为通常被SPA用来模拟前进和后退,这样做是因为不会触发页面刷新.

history通过状态管理api来改变url但不触发页面刷新,如pushState,这个方法执行后,就会在历史记录中增加一条记录,url也随之改变,除了这些以外,浏览器页不会向服务器发送请求,但是当你手动刷新的时候 会当作请求发出去。

需要注意的是,要确保通过pushState创建的每个“假”url背后都对应着服务器上的一个真实物理url,否则手动刷新会导致为404

为什么history模式下需要服务端进行配置呢?

在正常访问下 a.b.com, 是没问题的, 服务端直接加载对应目录下的index.html文件,但是有了子路由有不同了,服务端找不到对应的目录 就会出现404,所以需要我们在Nginx里配置 子路由的访问路径,在SPA中,自然全部指向index.html即可

而哈希模式后边的哈希值只是状态,不会跟随请求发送到服务器上,所以哈希模式可以放心使用,但是如果你在意颜值就需要使用History模式的时候进行相应配置

文章分类
阅读
文章标签