八股文

172 阅读7分钟

1.作用域链

内部作用域,寻找变量,如果在当前作用域找不到,则会往上一层的函数内部作用域去寻找,直到全局作用域,这种就近原则,一层一层去寻找变量而连成的链叫做作用域链

2.什么是闭包

在理解闭包之前,有个重要的概念需要先了解一下,就是 js 执行上下文

闭包就是在所有函数中都包含的,只是在一个函数里面返回另一个函数时才显得重要,因为它存储了本地执行上下文的变量,而在函数返回的函数的本地上下文执行里可以获取到这些变量

另外一种解释: 当函数内部需要访问函数外部的变量时,而这些变量存储在闭包里面,因此内部函数可以获取到外部函数的变量

我们把这些变量的集合称为闭包

闭包是怎么产生的?

当前函数作用域要拿到当前函数作用域外的变量,闭包就产生了

闭包的好处

1.保护函数的私有化变量不被外部污染。 2.把函数内部的变量保存下来,形成不销毁栈内存。

闭包的坏处

1.因为闭包的变量不能被销毁,所以同时也会增大内存,在IE下容易造成内存泄漏

什么是内存泄漏

什么是内存?

内存就是程序在运行时,系统分配的一块内存空间,每一块内存都对应的生命周期:

内存分配:在声明变量,函数时,系统分配的内存空间

内存使用:对分配到的内存进行读/写操作,即访问并使用变量,函数

释放内存内存使用完,释放掉不再使用的内存

每个浏览器都有一套自己的回收机制,当分配出去内存不再使用时便会回收,内存泄漏的根本原因就是某一块内存使用后无法被回收,释放,导致未释放的内存空间越积越多,直至用尽全部内存空间。程序将无法正常运行,直观体现就是程序卡死,系统崩溃,这一现象就称为内存泄漏

造成内存泄露的原因:
  • 意外的全局变量(在函数内部没有使用var声明的变量)
  • console.log()打印
  • 闭包
  • 对象的循环引用
  • 未清除的定时器
  • DOM泄露(获取到DOM节点之后,将DOM节点删除,但是没有手动去释放变量,拿对应的DOM节点在变量中还可以访问到,就会造成泄漏)

解决方法 可以使用完变量后手动赋值为null

3.什么是上下文执行

执行代码环境:
全局作用域:(第一次执行代码的默认环境),
函数作用域:(当执行流入函数体时)

上下文执行可以理解为:当前代码执行的环境与作用域

js的类型判断

js的类型判断的全部方法 typeofinstanceofObject.prototype.call()constructorArray.isArray

  • 1. type 可以判断简单数据类型和function

  • 2. instanceof 一般用来判断对象类型 对象 instanceof Object===true ,只能用于判断引用数据类型,不能判断简单数据类型

function Car(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
}
const auto = new Car("Honda", "Accord", 1998);
console.log(auto instanceof Car);
console.log(auto instanceof Object);
  • 3. Object.prototype.call() 可以精准的判断所有类型,常用于判断浏览器的内置对象
console.log(Object.prototype.toString.call("jerry"));//[object String]
console.log(Object.prototype.toString.call(12));//[object Number]
console.log(Object.prototype.toString.call(true));//[object Boolean]
console.log(Object.prototype.toString.call(undefined));//[object Undefined]
console.log(Object.prototype.toString.call(null));//[object Null]
console.log(Object.prototype.toString.call({name: "jerry"}));//[object Object]
console.log(Object.prototype.toString.call(function(){}));//[object Function]
console.log(Object.prototype.toString.call([]));//[object Array]
console.log(Object.prototype.toString.call(new Date));//[object Date]
console.log(Object.prototype.toString.call(/\d/));//[object RegExp]

function Person(){};
console.log(Object.prototype.toString.call(new Person));//[object Object]
  • 4. constructor 实现类型判断的原理是通过构造函数new出来的对象都有一个constructor属性,指向它的构造函数,对undefined和null无效 如new Number(1).constructor===Numer //true

  • 5. Array.isArray 用于判断是不是数组,如果没有这种方法,第三种方法也是可以的

浅拷贝与深拷贝

什么是浅拷贝

把原对象复制到新对象中,如果它还有第二层数据的话,如果修改第二层数据其实还是会相互影响的,因为第二层的数据拷贝的是它的引用地址

拷贝数组或对象的第一层到新的数组或者对象里面,如果有第二层,那么它们还是会相互影响

1.什么时候需要用到浅拷贝

修改数组/对象时,影响另一个数组/对象,砍断它们的联系,不让它们相互影响

2.怎么实现浅拷贝
  • 1.使用for in 或者for循环
  • 2.使用拓展运算符[...arr]
  • 3.Object.assign({},obj} //对象 Object.assign([],obj}//数组

什么是深拷贝

把旧对象复制到新对象去,然后不会影响到原对象,因为它复制的不是对象引用地址,而是会从堆内存从开辟一个新的引用地址去存放这个新对象

怎么实现深拷贝

1.手写递归深拷贝

递归每层引用类型,遇到对象/数组,创建一个新的,把基础值复制过来

let obj={
    name: "kk",
    age: 20,
    grad: [100, 200],
    family: {
     fName: "刘",
        }
}

let obj2={}
function fn(newobj,oldobj){
 console.log(newobj);//第一次是obj2{} 第二次是n[k]=[],第三次是n[k]={}
        // 就是传的都是新的空的{}或[]然后再给这些[]{}添加属性和值
for(k in oldobj){
let value=oldobj[k] //为什么需要这一段 因为需要判断值的类型
if(value instanceof Array){//如果值是数组类型的话,就给这个新对象添加grad属性和[]
newobj[k]=[]
fn(newobj[k],value)//再调用函数是因为要给第二层数据拷贝一个新的引用地址,
//会进入for in遍历重新分配引用地址并添加旧对象原来的值,所以函数传了一个属性和值
}
else if(value instanceof Object){
newobj[k]=value
fn(newobj[k],value)//把这个空的数组属性传了过去,把值传了过去
}
else{
newobj[k]=value  //如果是简单数据类型就给新对象添加这个属性和值
}
}
}
fn(obj2,obj)
2.JSON.parse(JSON.stringify())

利用 JSON.stringify 将js对象序列化(JSON字符串),再使用JSON.parse来反序列化(还原)js对象

      let arr = [12, 23, [23, 23]];
      let brr = JSON.parse(JSON.stringify(arr));
      arr[2].push(20);

this的指向

什么场景会有this指向

  • 1.箭头函数: 指向的是外层this
let name='kk'
let obj={
name:'张三',
say:()=>{
conlose.log(this.name)
}
eay:function (){
conlose.log(this.name)
}
obj.say()//kk
obj.eay()//张三(对象调用)
}
  • 2.new(构造函数): 指向实例对象
  • 3.bind:

bind方法和call很相似,第一参数也是this的指向,后面传入的也是一个参数列表(但是这个参数列表可以分多次传入,call必须一次性传入所有参数),但是它改变this执行后不会立即执行,而是返回一个永久改变this指向的函数 bind()方法创建一个新的函数在bind()被调用时,这个新函数的this被指定为bind()的第一个参数,而其余参数则将作为新函数的参数,供调用时使用

let arr=[1,20,0,4,12]
let max=Math.max.bind(null,arr[0],arr[1],arr[2],arr[3])
console.log(max(arr[4])//分2次传参,调用时使用
  • 4.applycall

apply接受两个参数,第一个参数是this的指向,第二个参数是函数接受的参数,以数组的形式传入,当第一个参数为null、undefined的时候,默认指向window(在浏览器中)使用apply方法改变this指向后原函数会立即执行,此方法只是临时改变this指向一次

let name='kk'
let obj={
name:'luck'
say:function(year,place){
console.log(this.name,year,place)
}
}
let say=obj.say
setTimeout(()=>{//定时器是宏任务,即使没有设置秒数,也会优先执行微任务
say.apply(obj,[1996,'master'])//使用apply方法会立即执行函数
})
say(1996,'china')//this变回了全局this,所以apply只要使用时候临时一次有用

改变参数的传入方式
求最大值
let arr=[1,4,50,4,6]
console.log(Math.max.apply(null,arr))//50

call方法第一个参数也是this执行,第二个参数的传参方法不是数组而是列表,当第一个参数为null或undefined的时候,this执行window,和apply一样,call也只是临时改变一次this执行,并立即执行

console.log(Math.max.call(null,2,3,45,68))//68
  • 5.obj.对象调用直接指向对象
  • 6.直接调用、7.不在函数里 setTimeout()函数