JavaScript高频面试题

222 阅读7分钟

1)什么是闭包:

闭包是指有权访问另一个函数作用域中的变量的函数

2)闭包的作用:

  1. 在外部访问函数内部的变量

  2. 让函数内的局部变量可以一直保存下去,不会被垃圾回收机制回收

  3. 模块化私有属性和公共属性

3)闭包的原理:

全局变量生存周期是永久,局部变量生存周期随着函数的调用结束而销毁,但是,闭包的情况不一样:在另一个函数内部定义的函数会将包含外部函数的活动对象添加到它的作用域链中,函数执行完毕后,其活动对象也不会被销毁,因为内部函数的作用域链仍然在引用这个活动对象。所以当函数执行完毕后,只是执行的作用域链会被销毁,但它的活动对象仍然保留在内存中,直到内部函数被销毁后才销毁。

4)闭包的优缺点

优点:

保护私有上下文中的"私有变量" 和 外界互不影响

私有上下文中的“私有变量”和“值”都会被保存起来,可以供其下级上下文中使用

缺点:

如果使用闭包,会导致栈内存太大,页面渲染变慢,性能受到影响

优化:

由于闭包会一直占用内存空间,直到页面销毁,我们可以主动将已使用的闭包销毁:

将闭包函数赋值为null 可以销毁闭包

5)闭包遇到的问题

多个子函数的[[scope]]都是同时指向父级,是完全共享的。因此当父级的变量对象被修改时,所有子函数都受到影响。

解决:

变量可以通过 函数参数的形式 传入,避免使用默认的[[scope]]向上查找

使用setTimeout包裹,通过第三个参数传入

使用 块级作用域,让变量成为自己上下文的属性,避免共享

6)闭包的应用场景

1、数组对象进行排序

let lessons = [

{

title: "Nodejs快速入门",

click: 100,

price: 3200

},

{

title: "html+css",

click: 100,

price: 1200

},

{

title: "js入门到精通",

click: 2100,

price: 2000

}

];

function myOrder(field) {

return (a, b) => a[field] - b[field];

}

lessons.sort(myOrder("price"))

2、对列表元素绑定事件

for (var i = 0; i < liList.length; i++) {

liList[i].onclick = (function (i) {

return function () {

console.log(当前点击按钮的索引:${i})

}

})(i)

}

3、闭包实现模块化 保护

var myModule = (function () {

var name = '张三'

function getName() { return name }

return {

getName

}

})()

4、返回值 最常见的一种形式

var fun_1 = function () {

var name = "limo";

return function () {

return name;

}

}

5、 getter和setter

var getValue, setValue;

(function () {

var num = 0

getValue = function () {

return num

}

setValue = function (v) {

if (typeof v === 'number') {

num = v

}

}

})();

console.log(getValue()); //0

setValue(10);

console.log(getValue()) //10

4、原型

原型:

在 js 中我们是使用构造函数来新建一个对象的,每一个构造函数的内部都有一个 prototype 属性值,这个属性值是一个对象,这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法,简单来说,该函数实例化的所有对象的__proto__的属性指向这个对象,它是该函数所有实例化对象的原型。

使用原型对象为多个对象共享属性或方法,这样可以解决通过构建函数创建对象时复制多个函数造成的内存占用问题

在这里插入图片描述

以下函数没有prototyoe:

使用Function.prototype.bind创建的函数对象

function abc(){console.log('abc')}

var binded = abc.bind(null)

binded() //abc

console.log(binded.prototype) //undefined

箭头函数也没有

var abc = ()=>{console.log('abc')}

abc() //abc

console.log(abc.prototype) //undefined

原型链:

原型链是由原型对象组成,每个对象都有 proto 属性,指向了创建该对象的构造函数的原型,proto 将对象连接起来组成了原型链。是一个用来实现继承和共享属性的有限的对象链。

属性查找机制: 当查找对象的属性时,如果实例对象自身不存在该属性,则沿着原型链往上一级查找,找到时则输出,不存在时,则继续沿着原型链往上一级查找,直至最顶级的原型对象Object.prototype,如还是没找到,则输出 undefined;

属性修改机制: 只会修改实例对象本身的属性,如果不存在,则进行添加该属性,如果需要修改原型的属性时,则可以用: b.prototype.x = 2;但是这样会造成所有继承于该对象的实例的属性发生改变。

5、继承

(1)原型链继承:

直接让子类的原型对象指向父类实例,当子类实例找不到对应的属性和方法时,就会往它的原型对象,也就是父类实例上找,从而实现对父类的属性和方法的继承

Child.prototype=new Parent()

原型继承的缺点:

由于所有Child实例原型都指向同一个Parent实例, 因此对某个Child实例的父类引用类型变量修改会影响所有的Child实例

在创建子类实例时无法向父类构造传参, 即没有实现super()的功能

(2)构造函数继承:

构造函数继承,即在子类的构造函数中执行父类的构造函数,并为其绑定子类的this,让父类的构造函数把成员属性和方法都挂到子类的this上去;

这样既能避免实例之间共享一个原型实例,又能向父类构造方法传参;

在Child的构造函数中执行

Parent.apply(this, arguments);

构造函数继承的缺点:

继承不到父类原型上的属性和方法

(3)组合继承

组合继承是将原型链和借用构造函数组合起来使用的一种方式。通过借用构造函数的方式来实现类型的属性的继承,通过将子类型的原型设置为父类型的实例来实现方法的继承。这种方式解决了上面的两种模式单独使用时的问题,

组合继承的缺点:

由于我们是以父类型的实例来作为子类型的原型,所以调用了两次父类的构造函数,造成了子类型的原型中多了很多不必要的属性。

(4)原型式继承

原型式继承的主要思路就是基于已有的对象来创建新的对象,实现的原理是,向函数中传入一个对象,然后返回一个以这个对象为原型的对象。这种继承的思路主要不是为了实现创造一种新的类型,只是对某个对象实现一种简单继承,ES5 中定义的 Object.create() 方法就是原型式继承的实现。缺点与原型链方式相同。

(5)寄生式继承

寄生式继承的思路是创建一个用于封装继承过程的函数,通过传入一个对象,然后复制一个对象的副本,然后对象进行扩展,最后返回这个对象。这个扩展的过程就可以理解是一种继承。这种继承的优点就是对一个简单对象实现继承,如果这个对象不是我们的自定义类型时。缺点是没有办法实现函数的复用。

(6)寄生式组合继承

组合继承的缺点就是使用超类型的实例做为子类型的原型,导致添加了不必要的原型属性。寄生式组合继承的方式是使用超类型的原型的副本来作为子类型的原型,这样就避免了创建不必要的属性。

6、柯里化

函数柯里化指的是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。

前端使用柯里化的用途主要就是简化代码结构,提高系统的维护性,一个方法,只有一个参数,强制了功能的单一性,很自然就做到了功能内聚,降低耦合。

反柯里化:

在JavaScript中,当我们调用对象的某个方法时,其实不用去关心该对象原本是否被设计为拥有这个方法,这是动态类型语言的特点。可以通过反柯里化(uncurrying)函数实现,让一个对象去借用一个原本不属于他的方法。

const add = function add(x) {

return function (y) {

return x + y

}

}

const add1 = add(1)

add1(2) === 3

add1(20) === 21

柯里化实现add(1)(2)(3)

function add () {

var args = Array.prototype.slice.call(arguments);

var fn = function () {

var sub_arg = Array.prototype.slice.call(arguments);

// 把全部的参数聚集到参数的入口为一个参数: args.concat(sub_arg)

return add.apply(null, args.concat(sub_arg));

}

fn.valueOf = function () {

return args.reduce(function(a, b) {

return a + b;

})

}

return fn;

}

console.log(add(1,2)) // 3

console.log(add(1)(2)) // 3

console.log(add(1)(2)(3)) // 6

console.log(add(1,2,3)(4)) // 10

7、执行上下文

执行上下文有且只有三类,全局执行上下文,函数上下文,与eval上下文(eval一般不会使用)

  1. 全局执行上下文:

全局执行上下文只有一个,也就是我们熟知的window对象,我们能在全局作用域中通过this直接访问到它