js高级之面试题--比较常见的面试问题--递归,排序,闭包,this指向,作用域,设计模式,异步编程,宏任务微任务

js高级之面试题精讲

一、面试题难点之递归

递归是一种解决复杂的未知次数循环的问题的解决方案。其实表现是在一个函数内部自己调用自己。

如:求n的阶乘 – 求n的阶乘可以不使用递归实现,只是用来演示递归的使用

function factorial(n){
  return n == 1 ? 1 : n * factorial(n-1)
}

我们明显看出在factorial函数内部调用了自己。这就是函数递归。

此时如果我们调用这个函数,求5的阶乘,其调用过程如果

===> factorial(5)
===> 5 * factorial(4)
===> 5 * 4 * factorial(3)
===> 5 * 4 * 3 * factorial(2)
===> 5 * 4 * 3 * 2 * factorial(1)
===> 5 * 4 * 3 * (2 * 1)
===> 5 * 4 * (3 * 2)
===> 5 * (4 * 6)
===> 5 * 24
===> 120

所以这是一个层层递进,层层回归的过程 —— 简称 递归

上面只是一个简单的示例。在面试题中常见的场景主要如下:

1.深拷贝

function deepClone(value) {  
    if (value == null) return value;  
    if (typeof value !== 'object') return value;
    if (value instanceof RegExp) return new RegExp(value);  
    if (value instanceof Date) return new Date(value);  
    // 我要判断 value 是对象还是数组 如果是对象 就产生对象 是数组就产生数组  
    let obj = new value.constructor;  
    for(let key in value){    
        obj[key] = deepClone(value[key]); // 看一看当前的值是不是一个对象  
    }  
    return obj;
}

2.二分排序

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

function quickSort(arr) {
    if(arr.length == 0) {
        return [];    
    }
    var cIndex = Math.floor(arr.length / 2);
    var c = arr.splice(cIndex, 1);
    var l = [];
    var r = [];
    for (var i = 0; i < arr.length; i++) {
        if(arr[i] < c) {
            l.push(arr[i]);
        } else {
            r.push(arr[i]);
        }
    }
    return quickSort(l).concat(c, quickSort(r));
}
console.log(quickSort(arr));

3.数组扁平化

var arr = [[1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14]]]], 10];
var newArray = [];
function getArray(array) {
    array.forEach(function(e) {
        if (typeof e === "object") {
            getArray(e);
        } else {
            newArray.push(e);
        }
    });
}
getArray(arr);

二、面试题难点之闭包

1.常见闭包面试题

function fun(n, o) {
    console.log(o);
    return {
        fun: function(m) {
            return fun(m, n);
        }
    };
}

var a = fun(0); // ?
a.fun(1); // ?
a.fun(2); // ?
a.fun(3); // ?
var b = fun(0).fun(1).fun(2).fun(3); // ?
var c = fun(0).fun(1); // ?
c.fun(2); // ?
c.fun(3); // ?

三、面试题难点之作用域和this指向

1.常见面试题

x = 1;
var obj = {
    x: 2,
    dbl: function () {
        this.x *= 2;
        x *= 2;
        console.log(x);
        console.log(this.x);
    }
};
// 说出下面的输出结果
obj.dbl();
var func = obj.dbl;
func();
var funcBind = obj.dbl.bind(obj);
funcBind();
function Foo() {
    getName = function () { alert (1); };
    return this;
}
Foo.getName = function () { alert (2);};
Foo.prototype.getName = function () { alert (3);};
var getName = function () { alert (4);};
function getName() { alert (5);}

Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();

四、面试题难点之设计模式

设计模式是所有的编程语言中都可以实现的一些高级技巧,现在大家代码量还少,可能还无法理解,所以不用尝试自己推导,而是把这些东西掌握使用就好。

学习设计模式,有助于写出可复用和可维护性高的程序

设计模式的原则是“找出 程序中变化的地方,并将变化封装起来”,它的关键是意图,而不是结构。

单例模式

单例模式是指某个类永远只有一个实例对象,这个模式的好处是只有一个实例对象,那么所有的数据就可以在任何位置共享,便于管理这些需要共享的数据。

代码示例:

var _instance = null;
function Manager(){
  if(!_instance){
    _instance = new Manager();
  }
  return _instance;
}
class Manager{
  constructor(){
    if(Manager._instance){
      return Manager._instance;
    }
		Manager._instance = this;
  }
}

观察者模式(订阅发布模式)

1. 定义

也称作观察者模式,定义了对象间的一种一对多的依赖关系,当一个对象的状态发 生改变时,所有依赖于它的对象都将得到通知

2. 核心

取代对象之间硬编码的通知机制,一个对象不用再显式地调用另外一个对象的某个接口。

在JS中通常使用注册回调函数的形式来订阅

写法有很多,但是思想都一样: 先给对应的事件或者状态注册多个回调函数,当触发这个事件时,调用这些函数。

var event = {
  add : function(type,fn){
  	if(!this[type]){
      this[type] = []
    }
		this[type].push(fn)
	},
  remove: function(type,fn){
    var index = this[type].indexOf(fn);
    this[type].splice(index,1)
  },
  trriger(type){
    if(this[type]){
      this[type].forEach(e=>{
        e();
      })
    }
  }
}

设计模式不仅仅只是这两种,详细参考

五、面试题难点之异步编程

1.执行栈

想要想明白异步编程的执行顺序,首先要知道js代码是如何执行的。此时有一个概念一定要先知道:执行栈

执行栈,也称“调用栈”,是一种拥有 后进先出 的数据结构,被用来存储代码运行时创建的所有执行上顺序。

当 JavaScript 引擎第一次遇到你的脚本时,它会创建一个全局的执行环境并且压入当前执行栈。每当引擎遇到一个函数调用,它会为该函数创建一个新的执行环境并压入栈的顶部。

引擎会执行处于栈顶的执行环境的函数。当该函数执行结束时,执行环境从栈中弹出,控制流程到达当前栈中的下一个执行环境。

让我们通过下面的代码示例来理解:

 console.log('全局环境开始');
let a = 10;

function first() {
  console.log('函数1');
  second();
  console.log('两次回到函数1');
}

function second() {
  console.log('函数2');
}

first();
console.log('全局环境结束');

上面的代码可以用这样的过程来理解

image-20210331151641670

1.首先是全局的执行环境入栈

2.在全局环境下调用了first函数,再把first函数的环境压入栈中

3.在first函数里面调用了second函数,再把second函数的环境压入栈中

4.second执行完毕,于是把second的执行环境从栈中移除(先进后出,后入先出)

5.回到first的执行环境,再把fist的代码执行完成,从执行栈中再移除

6.最后把全局的执行环境也出栈,整个程序执行完成

2.EventLoop

2.1 javascript是单线程的

JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事。那么,为什么JavaScript不能有多个线程呢?这样能提高效率啊。

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

2.2 EventLoop和任务队列

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。

如果排队是因为计算量大,CPU忙不过来,倒也算了,但是很多时候CPU是闲着的,因为IO设备(输入输出设备)很慢(比如Ajax操作从网络读取数据),不得不等着结果出来,再往下执行。

JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。

于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

具体来说,异步执行的运行机制如下。

1.所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
2.主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
3.一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
4.主线程不断重复上面的第三步。

于是就会形成下面这样一个执行模型

image-20210331154715816

2.3 MacroTask(宏任务)和 MicroTask(微任务)

而在代码的执行过程中,我们还把所有的分为两个大类,宏任务微任务

宏任务微任务
script环境Promise的then/catch回调
setInterval/setTimeout 定时器Object.observe(先忽略)
requestAnimationFrame 浏览器的帧循环(先忽略)Proxy(先忽略)
UI Rendering 浏览器的UI渲染(先忽略)

Event Loop中,每一次循环称为tick,每一次tick的任务如下:

执行栈在执行完同步任务后,查看执行栈是否为空,如果执行栈为空,就会去执行Task(宏任务),每次宏任务执行完毕后,检查微任务(microTask)队列是否为空,如果不为空的话,会按照先入先出的规则全部执行完微任务(microTask)后,设置微任务(microTask)队列为null,然后再执行宏任务,如此循环

以下面的代码为例:

console.log('1');
setTimeout(function() {
    console.log('2');
    new Promise(function(resolve) {
        console.log('3');
        resolve();
    }).then(function() {
        console.log('4')
    })
})
new Promise(function(resolve) {
    console.log('5');
    resolve();
}).then(function() {
    console.log('6')
})
console.log('7')

一开始的执行过程如下

image-20210331161826299

然后进入循环

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-97yvZzMV-1626498202286)(assets/image-20210331162756345.png)]

所以最终的结果为: 1,5,7,6,2,3,4

2.4 实战时间

setTimeout(function () {
  console.log('1');
})
new Promise(function (resolve) {
  console.log('2');
  for (var i = 0; i < 1000; i++) {
    i == 99 && resolve();
  }
  console.log('3');
}).then(function () {
  console.log('4');
})
console.log('5')
console.log('start')
setTimeout(() => {
  console.log('setTimeout')
}, 0)
new Promise((resolve) => {
  console.log('promise')
  resolve()
}).then(() => {
  console.log('then1')
}).then(() => {
  console.log('then2')
})
console.log('end')
console.log('start')
setTimeout(() => {
  console.log('timer1')
  Promise.resolve().then(function() {
    console.log('promise1')
  })
}, 0)
setTimeout(() => {
  console.log('timer2')
  Promise.resolve().then(function() {
    console.log('promise2')
  })
}, 0)
Promise.resolve().then(function() {
  console.log('promise3')
})
console.log('end')
  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

尤雨东

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值