一些面经的小知识(一)

482 阅读6分钟

毕业出来工作了一年多,感觉自己很少做技术总结和沉淀

借着最近比较空闲的时间,来写一篇关于我接触到的前端面试的一些小知识点吧,可能有什么错误或者不足的地方还望大神指正。

首先是JS方面

1、ES6变量声明

let、const和var的区别:

function print() {
    console.log(a);
    var a = 'abc';
}
print();

大家应该知道上面这段代码执行结果吧


是的,没错打印的是undefined。因为var的变量声明会提升,也就是执行此函数时,js会先检测你变量的声明,再按顺序执行console和变量赋值的操作,就相当于

function print() {
    var a;
    console.log(a);
    a = 'abc';
}
print();

那如果是let 和 const呢

function print() {
    console.log(a);
    let a = 'abc';
}
print();


诶,报错了~这是为什么呢

因为let声明是不提升的,所以会提示你在你未初始化a变量前不能访问a变量,这一块变成了“死区”。

来讲讲const,const大家都知道是用来声明常量居多,因为const声明的数据大多是不能修改的。

const a = 1;
a = 2;

上面👆这样是会报错的。


那如果const声明的是个对象呢

const obj = {
    a: 1
};
obj.a = 2;
console.log(a)

诶,如果const声明是个对象的话,那对象的属性是可以修改的。


那如果我们想让这个对象变成不可以修改的呢?大家很快想到的应该是用Object.defineProperty()


其实还有一种方法是用Object.freeze()实现对象总结(大家可以查阅一下用法)

2、原型链

原型链是什么?

function A() {
    this.say = function() {
        console.log('hello')    
    }
}
let a = new A();

原型链大概是构造函数、构造函数的实例、构造函数的原型以及最顶层的Function、Object之间的关系(具体可以查阅资料)

上面的构造函数A的prototype是指向A的原型,A的原型又有一个叫constructor的属性指向构造函数A。构造函数A的实例a的__proto__属性指向了A的原型,a的constructor属性指向了构造函数A。构造函数A的constructor属性指向了Function,Function的prototype指向了Function的原型,构造函数A的__proto__指向了Function的原型。

3、继承

几种继承的方式要知道:原型链继承、借用构造继承、组合继承、原型式继承、寄生式继承、寄生组合式继承。下面说一下大概实现:

原型继承--子类的原型指向父类的实例

借用构造继承--在子类构造中调用父类的构造(通过call)

组合继承--综合上面两种

原型式继承--函数return一个空实例,空实例的构造函数的原型指向父类的实例,将空实例赋给子类变量

寄生式继承--用函数包装原型式继承

寄生组合式继承--空构造函数的原型指向父类的原型,子类构造函数中用call方式调用父类构造函数,子类的原型指向空构造函数的实例

4、JS作用域为什么能生效

当函数被调用时,会有一个scope作用域链,scope可以看作是一个个VO串联而成,VO可以大致看成变量对象,变量对象其实就是一个对象包含着这个作用域内声明的变量或方法;VO指向外层作用域的VO,这样函数调用时就可以通过标识符解析,沿着scope一层层搜索变量或方法。

5、讲讲JS的事件循环(event loop)

JS的事件循环--浏览器和NodeJS还有点不一样

浏览器大致是:JS全局代码执行,调用栈空了后,会从微队列(microtask)的队首抽一个微任务丢到调用栈执行,执行完再继续抽一个微任务执行,直到微队列空了(不过需要注意的是,这期间产生的微任务,是会被插入到微队列队尾,也在这个周期内执行完,微队列有上限),接着是浏览器自行决定是否执行UI Rendering,再从宏队列(macrotask)的队首抽一个宏任务执行,最后回到前面微队列步骤。

而NodeJS大致区别是:Node的宏队列是分六个阶段的,从上到下分别是timer、I/O、Idle、poll、check、close,与浏览器不同在于NodeJS执行宏任务时,是执行所在阶段内的所有宏任务。

宏任务:setTimeout、setInterval、setImmediate(node)、I/O、

微任务:promise、process.nextTick(node)、mutationObserver

至于为什么requestAnimationFrame不算宏任务呢,因为它其实应该是发生在浏览器重绘前,也就是UI Rendering前,微任务执行后,所以既不算宏任务也不算微任务。(window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行)

参考文章:

www.cnblogs.com/cangqinglan…

www.jianshu.com/p/9644f1356…

6、关于this的问题

var a = 111
function print() {
    console.log(this.a)
}
print() // 111

但如果把函数引用赋给对象的属性的话

var a = 111
function print() {
    console.log(this.a)
}
var obj = {
    a: 222,
    c: print
}
obj.c() // 222

这是因为当函数引用具有上下文对象的时候,this会隐式绑定到这个上下文。

那如果我们再把对象的属性值赋给一个变量呢?

var a = 111
function print() {
    console.log(this.a)
} 
var obj = {
    a: 222,
    c: print
}
var myfunc = obj.c
myfunc() // 111

这样其实是丢失绑定了,myfunc这个变量存放的是print函数的引用,直接调用myfunc就相当于直接调用print函数。

改变this的方法有很多,比如call、apply、bind,还有JS API里像foreach这样的硬绑定,再就是softbind软绑定。

7、如何实现一个bind

其实bind除了绑定this外,还有一个预设参数的作用

function test(a, b) {
    return a + b
}
console.log(test.bind(null, 1)(2)) // 3

初实现:

Function.prototype.bind = function(context) {
    let that = this
    let args = Array.prototype.slice.call(arguments, 1)
    return function() {
        return that.apply(context, args.concat(Array.prototype.slice.call(arguments)))
    }
}

考虑到bind返回的函数还可以用作构造函数,

再实现:

Function.prototype.bind = function(context) {
    let that = this
    let args = Array.prototype.slice.call(arguments, 1)
    let bound = function() {
        if (this instanceof bound) {
            return that.apply(this, args.concat(Array.prototype.slice.call(arguments))) // 这时候,bound和that的关系就是子类构造与父类构造(参考借用构造继承)
        } else {
            return that.apply(context, args.concat(Array.prototype.slice.call(arguments)))
        }
    }
    if (this.prototype) {
        bound.prototype = this.prototype
    }
    return bound
}

8、如何打平一个数组

什么叫打平一个数组呢?我的理解是数组有可能是多层嵌套的,怎么让这样一个多层的数组变成只有一层。

var array = [1, 2, [3, 4, 5, [6, 7]], 8, 9]

(1)通过concat

function beatArr(arr) {
    let newArr = [].concat(...arr)
    return newArr.some(Array.isArray) ? beatArr(newArr) : newArr
}

(2)通过reduce

function beatArr(arr) {
    return arr.reduce((pre, next) => {
        return pre.concat(Array.isArray(next) ?beat(next) : next)
    }, [])
}

(3)利用数组join和toString方法

arr.join().split(',')
//或者
arr.toString().split(',')

(4)利用ES6的flat方法(flat默认打平一层)

[1, 2, 3, [4, 5, [6, 7]]].flat() // [1, 2, 3, 4, 5, [6, 7]]
[1, 2, 3, [4, 5, [6, 7]]].flat(2) // [1, 2, 3, 4, 5, 6, 7]
[1, 2, 3, [4, 5, [6, 7, [8, 9]]]].flat(Infinity) // [1, 2, 3, 4, 5, 6, 7, 8, 9]
[1, 2, , 4, 5].flat() // [1, 2, 4, 5] (flat默认会跳过空位)

未完待续······