前端面试汇总-js篇(基础篇)

122 阅读15分钟

一. 作用域

作用域-表示变量可访问的范围;

1、局部作用域

  • 函数作用域:函数内声明的变量;
  • 块级作用域:{}中的变量;(let与const声明的变量,都会产生块级作用域,var是全局作用域,会有变量升级)

2、全局作用域

  • < script >标签中的变量
  • .js文件中的变量

3、作用域链 一种变量的查找机制,从小到大逐级查找。

4、垃圾回收机制

  1. 生命周期:分配内存、使用内存、回收内存;
  2. 内存未被回收则会导致内存泄漏;
  3. 回收方式:(栈:操作系统自动分配释放;堆:程序员释放或者回收机制释放)
    引用计数法(IE,已不使用):跟踪记录引用次数;
    标记清除法:js全局出发,根部无法访问到的,进行清除(解决了引用计数法无法清除相互调用而无法清除的问题)

5、闭包 内层函数+外层函数的变量组合起来就是闭包


function auter (){
const a ='00'
 function insid(){
 console.log(a)}
 insid()
}

auter()

闭包作用:封闭数据提供操作,是外部也能访问函数内部的变量,实现数据私有;但有内存泄漏风险;

6、变量提升 把所有var的声明提升到当前作用域的最前端,只提升声明,不提升赋值;

函数提升 函数表达式:把所有声明提升到当前作用域的最前端,只提升声明,不提升赋值;

二、函数

1、参数
函数有多个不确定的参数时,如何获取参数?

  • 动态参数:arguments 获取到函数中的所有形参,返回伪数组。
  • 剩余参数:...参数。 获取多余参数,返回真数组(提倡使用)

2、箭头函数 箭头函数仅限于函数表达式可用;
特点:

  • 只有一个参数时可以参略小括号();
  • 函数内只有一行代码时,可以参略大括号{};
  • 函数内只有一行代码时,可以参略return;
//省略前
const fun =(x)=>{
return x+x
}
//省略后
const fun = x => x+x
fun(1) //2
  • 加括号的函数表达体,返回对象字面量表达式
const fn= name => ({name:name})
fn('tangtang')

4、箭头函数与普通函数的区别

  • 参数传递方式。箭头函数没有自己的arguments对象,它使用剩余参数(rest parameters)来获取所有参数的值,而普通函数可以使用arguments对象访问传递给函数的所有参数。

  • 是否可以作为构造函数使用。箭头函数不能用作构造函数,不能通过new关键字来实例化对象,而普通函数可以作为构造函数,以此来创建一个对象的实例。

  • 是否具有prototype和super。箭头函数没有prototype原型对象,也不具有super

三、函数的this指向与改变指向

哪个对象调用函数,函数里面的this指向哪个对象。
1. 普通函数 this 指向window,因为一般情况下普通函数的调用者都是window; 2. 对象的方法 this指向的是对象;
3. 构造函数 this 指向 实例对象 原型对象里面的this 指向的也是实例对象.
4. 定时器函数调用与立即执行函数调用指向的window;
5. 箭头函数没有自己的this,如果在箭头函数中使用this,则沿用上一级指向。 6. 事件绑定函数 this 指向绑定事件对象。

改变指向的方法
1. call

  • 作用:调用函数;指定this指向;
  • 参数:传递的是单个参数;
  • 返回值:函数调用的返回值;
  • 场景:调用函数并传递参数
fn.call(thisArg,arg1,arg2,)
// 1、fn---调用函数
// 2、thisArg---指向参数
// 3、args---其他参数 **(单个)**

2. apply

  • 作用:调用函数,指定this指向;求数组最大值、最小值;
  • 参数:传递的参数是数组;
  • 返回值:函数调用的返回值;
  • 操作:做与数组有关的操作
//用法
fn.apply(thisArg,[args])
// 1、fn---调用函数
// 2、thisArg---指向参数
// 3、args---其他参数**(数组)**

//求数组最大值
cosnt arr=[2,5,8,99]
const max=Math.max.apply(Math,arr)

3.bind
作用:不会调用函数,但能改变this指向;
参数:传递的是单个参数;
返回值:改变指向过后的新函数
操作:不调用函数的情况下改变this指向

4、防抖与节流

我们在平时开发的时候,会有很多场景会频繁触发事件,比如说搜索框实时发请求,onmousemove、resize、onscroll 等,有些时候,我们并不能或者不想频繁触发事件,这时候就应该用到函数防抖和函数节流。

函数防抖(debounce),指的是短时间内多次触发同一事件,只执行最后一次,或者只执行最开始的一次,中间的不执行。(王者荣耀回程)
实际使用场景:input输入、验证码、手机号等;
手写loadsh debounce防抖函数

//1、申明定时器变量;
//2、当鼠标移动时,先判断是否有定时器,如果定时器变量有值则清除之前的定时器;
//3、没有如果没有定时器则开启,并存入定时器变量中;
//4、定时器变量中写入执行函数
const box =document.querySelector(".square")
let i=1
function onMoseMOVE(){
    box.innerHTML=i++
}

function debounce(fn,t){
    let timer
    return function(){
        if(timer) clearTimeout(timer)
        timer=setTimeout(
              function(){
                fn()
              },t)
    }
}
box.addEventListener("mousemove",debounce(onMoseMOVE,500)) 

函数节流(throttle),指连续触发事件但是在 单位时间内中只执行一次函数。节流如字面意思,会稀释函数的执行频率;(王者荣耀技能CD)
**实际使用场景:**进度条拖动、页面resize、滚动条、鼠标移动等;
手写loadsh throttle节流函数

//1、申明定时器变量;
//2、当鼠标移动时,先判断是否有定时器,如果定时器变量有值则清除之前的定时器;
//3、没有如果没有定时器则开启,存入定时器变量中;
//3.1定时器中调用执行函数
//3.2在定时器中将定时器清掉
const box =document.querySelector(".square")
let i=1
function onMoseMOVE(){
    box.innerHTML=i++
}

function throttle(fn,t){
    let timer=null
    return function(){
        if(!timer){ 
            timer=setTimeout(
              function(){
                fn()
                timer=null
            },t)}
    }
}
box.addEventListener("mousemove",throttle(onMoseMOVE,500)) 

四、解构赋值

展开运算符
...arr,将数组展开;
常使用场景:合并数组;求数组最大值;

数组、对象解构
将数组\对象元素快速的批量赋值

//可进行多维解构
const [a,b,c]=[1,[2,3]]

//变量名可修改
const {name:uname,age,family:{sister}=
      {name:'tang',age:18,family:{sister:"josh"}

五、创造对象与构造函数

构造函数是一种用来初始化对象的特殊函数;可以通过抽取公共的参数,快速创建多个的对象
new 开头函数的函数被称为实例化函数
构造函数不需要写return ,返回的是创建的新对象
创建对象的三种方式:

//1、创建字面量对象;
const obj={a:1}

//2、new Object;
const obj=new Object({a:1})

//3、利用构造函数;
function Pig(name,age){
this.name=name
this.age=age
}

new Pig('zhzuhu',13)

六. new 操作符都做了哪些事?

new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。

new 关键字会进行如下的操作:
步骤 1:创建一个空的简单 JavaScript 对象,即 { } ;
步骤 2:构造函数this指向新对象;
步骤 3:执行构造函数代码,修改this指向,添加属性;
步骤 4:返回新对象(如果该函数没有返回对象,则返回 this)。

构造函数的实例成员与静态成员
实例成员是实例对象上面的方法与属性
静态成员是构造函数上的方法与属性(Object.keys()等)

七.为什么基础类型(number、string、布尔值)会有属性和方法?

因为js底层将基础类型进行了包装;将简单数据类型包装为引用数据类型。

八、内置构造函数

image.png

十、原型与原型链

参考答案:

  • 每个对象都有一个 __proto__ 属性,该属性指向自己的原型对象
  • 每个构造函数都有一个 prototype 属性,该属性指向实例对象的原型对象
  • 原型对象和__proto__里的 constructor 指向构造函数本身

如下图:

image-20210812161401493

每个对象都有自己的原型对象,而原型对象本身,也有自己的原型对象,从而形成了一条原型链条。

当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。

九、面向对象与面向过程

面向过程:分析解决问题的步骤,实现步骤,然后依次调用;以过程为导向解决问题;
js属于面向过程,js面向对象可以通过构造函数来实现;以对象功能来解决问题

面向对象:将食物分解成对象,然后对象之间分工合作
面向对象特点:多态、继承、封装;

image.png

二十、class类与对象

类就是一个函数,是构造函数的语法糖; es6之前通过构造函数与原型实现面向对象,es6之后通过class实现面向对象; 类没有变量提升,所以需要先声明再实例化使用; 类里面的共有属性与方法需要加上this使用;

class Star{
//构造器
    constructor(name,age){
     this.name=name
     this.age=age
     //内部调用
      this.sing(s)
    }
    sing(s){
        console.log(this.name+s)
    }
}
//实例化
const start1=new Star("LDH",18)
start1.sing('bingyue')

//继承
class Son extent Star {
constructor(name,age,wife){
//将参数传递给父类
 super(name,age)
 this.wife=wife
 }
}

131. ES6 能写 class 么,为什么会出现 class 这种东西?

在 ES6 中,可以书写 class。因为在 ES6 规范中,引入了 class 的概念。使得 JS 开发者终于告别了直接使用原型对象模仿面向对象中的类和类继承时代。

但是 JS 中并没有一个真正的 class 原始类型, class 仅仅只是对原型对象运用语法糖。

之所以出现 class 关键字,是为了使 JS 更像面向对象,所以 ES6 才引入 class 的概念。

十一、JS 中继承实现的几种方式

参考答案:

JS 的继承随着语言的发展,从最早的对象冒充到现在的圣杯模式,涌现出了很多不同的继承方式。每一种新的继承方式都是对前一种继承方式不足的一种补充。

  1. 原型链继承

  • 重点:让新实例的原型等于父类的实例。

  • 特点:实例可继承的属性有:实例的构造函数的属性,父类构造函数属性,父类原型的属性。(新实例不会继承父类实例的属性!)

  • 缺点:

    • 1、新实例无法向父类构造函数传参。

    • 2、继承单一。

    • 3、所有新实例都会共享父类实例的属性。(原型上的属性是共享的,一个实例修改了原型属性,另一个实例的原型属性也会被修改!)

  1. 借用构造函数继承

  • 重点:用 call( )  和 apply( )  将父类构造函数引入子类函数(在子类函数中做了父类函数的自执行(复制))

  • 特点:

    • 1、只继承了父类构造函数的属性,没有继承父类原型的属性。

    • 2、解决了原型链继承缺点1、2、3。

    • 3、可以继承多个构造函数属性(call多个)。

    • 4、在子实例中可向父实例传参。

  • 缺点:

    • 1、只能继承父类构造函数的属性。

    • 2、无法实现构造函数的复用。(每次用每次都要重新调用)

    • 3、每个新实例都有父类构造函数的副本,臃肿。

  1. 组合模式(又被称之为伪经典模式)

  • 重点:结合了两种模式的优点,传参和复用

  • 特点:

    • 1、可以继承父类原型上的属性,可以传参,可复用。
    • 2、每个新实例引入的构造函数属性是私有的。
  • 缺点:调用了两次父类构造函数(耗内存),子类的构造函数会代替原型上的那个父类构造函数。

  1. 寄生组合式继承(圣杯模式)

  • 重点:修复了组合继承的问题

十二、如何判断数组或对象

参考答案:

  1. 通过 instanceof 进行判断
var arr = [1,2,3,1];
console.log(arr instanceof Array) // true
  1. 通过对象的 constructor 属性
var arr = [1,2,3,1];
console.log(arr.constructor === Array) // true
  1. Object.prototype.toString.call(arr)
console.log(Object.prototype.toString.call({name: "jerry"}));//[object Object]
console.log(Object.prototype.toString.call([]));//[object Array]
  1. 可以通过 ES6 新提供的方法 Array.isArray( )
Array.isArray([]) //true

十三、深浅拷贝

  • 直接赋值:拷贝只要是对象就会相互影响;因为值直接赋值对象栈中的地址;
  • 浅拷贝:拷贝基本数据类型时,不受任何影响,当拷贝引用类型时,源对象也会被修改。
  • 深拷贝:深拷贝就是完完全全拷贝一份新的对象,它会在内存的堆区域重新开辟空间,修改拷贝对象就不会影响到源对象

浅拷贝方式:

  1. Object.assinc();
  2. es6扩展运算符;
  3. 数组的 slice 和 concat 方法;

深拷贝方法:

  1. JSON.parse(JSON.stringify(待拷贝对象));
  2. 手写递归的方式来实现深拷贝[递归是自己调用自己];
  3. lodash的deepClone 手写递归

 //origin表示待拷贝对象,target表示目标对象
            function deepClone(origin, target) {
                var target = target || {}, //容错处理,防止用户不传target值
                    toStr = Object.prototype.toString,
                    arrAtr = "[object Array]";
                for (var prop in origin) {
                    //遍历对象
                    if (origin.hasOwnProperty(prop)) {
                        //防止拿到原型链属性
                        if (
                            origin[prop] !== "null" &&
                            typeof origin[prop] == "object"
                        ) {
                            //判断是不是原始值
                            target[prop] =
                                toStr.call(origin[prop]) == arrAtr ? [] : {}; //建立相对应的数组或对象
                            deepClone(origin[prop], target[prop]); //递归,为了拿到引用值里面还有引用值
                        } else {
                            target[prop] = origin[prop]; //是原始值,直接拷贝
                        }
                    }
                }
                return target;
            }

十四、异常处理

  • throw 抛异常预估代码执行过程中的异常,最大程度的避免错误发生导致整个程序无法运行;
    特点:throw抛出错误信息,程序也会终止;
    使用 throw “错误信息提示” 或者 throw new Error('错误信息提示')

-try catch finally捕获错误信息 特点:是浏览器提供的方,:可用于判断浏览器原罪与代码非法性 用法:预估可能发生错误的代码写在try中,如果try中发生错误,则会被catch捕获(程序不不会主动终止),不管try有没有发生错误,finally都会执行

十五、Web Api之DOM与BOM

  • DOM:文档对象模型----是浏览器提供的一套专门操作网络内容的功能;开发网络特效、实现用户交互;
  • DOM树:文档树,体现标签与标签的关系;
  • DOM对象:浏览器根据html标签设置的js对象;
  • BOM:浏览器对象模型
  • image.png

十六、回调函数

将函数当作参数使用;不立即调用函数

十七、重绘、重排(回流)

重排:渲染树中部分元素的尺寸、元素、布局发生改变时,浏览器将重新渲染该布局文档;
重绘:只改变样式不影响节点在文档中的位置和布局则为重绘;
重绘不一定引起回流,回流一定会重绘;

十八、事件流

事件流就是事件完整执行过程的路径;
路径一:事件捕获--事件从文档根节点向下传播到目标元素的过程;
路径二:目标阶段--事件到达目标元素后触发的过程
路径三:事件冒泡--事件从目标元素向上传播到文档根节点的过程;
阻止事件流动方法:
// 阻止冒泡与捕获都可 event.stopPropagation()
阻止默认事件事件方法:
// a链接跳转等 event.preventDefault()

事件委托
事件委托就是把原本需要绑定在子元素上的事件(onclick、onkeydown 等)委托给它的父元素,让父元素来监听子元素的冒泡事件,并在子元素发生事件冒泡时找到这个子元素。
利用 JS 事件冒泡动态为元素绑定事件的方法称为事件委托;
可以通过event.target获取真正触发的元素

十九、js执行机制event loop**

js执行机制即为事件循环。
js为单线程,同一时间只能做一件事;
单线程就意味着事件需要排队,如果js执行时间过长,则会影响页面渲染连贯性,造成页面阻塞; 为此H5允许脚本的添加多个线程,即同步与异步;
同步与异步的区别则是执行顺序的不同。
同步任务: 所有的同步任务都在主线程上执行,形成一个执行栈;
异步任务: 异步任务通过回调函数实现;异步任务将添加到任务队列中;
异步任务类型:
1.普通事件:click、resize;
2.资源加载:error、loading;
3.定时器:setInterval\setTimeout;

执行顺序

  • 1、先执行栈中的同步任务;
  • 2、再将异步任务放入消息队列;
  • 3、执行栈中的同步任务全部完成后,系统会依次读取消息队列中的异步任务,异步任务结束等待进入执行栈,进行执行;