欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

苦战三个月,搞定前端面试必备的八个JS问题

最编程 2024-02-14 07:10:01
...

 1. let/const/var 的区别是什么?

参考答案:

  1. var 定义的变量,没有块的概念,可以跨块访问, 不能跨函数访问,有变量提升。
  2. let 定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问,无变量提升,不可以重复声明。
  3. const 用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,而且不能修改,无变量提升(对象和数组依然修改其中的属性,但不建议这么用),不可以重复声明。

2. 那什么是块级作用域呢?

参考答案:在ES5 的时代, if 或者 for 循环中声明的变量会泄露成全局变量,其次就是 { } 中的内层变量可能会覆盖外层变量。 ES6 中新增了块级作用域。块作用域由 { } 包括起来,if 语句和 for 语句里面的 { } 也属于块作用域。

3. 说说作用域与作用域链

参考答案: 作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。换句话说,作用域决定了代码区块中变量和其他资源的可见性。ES6 之前 JavaScript 没有块级作用域,只有全局作用域和函数作用域。ES6 的到来,为我们提供了块级作用域。

作用域链指的是作用域与作用域之间形成的链条。当我们查找一个当前作用域没有定义的变量(*变量)的时候,就会向上一级作用域寻找,如果上一级也没有,就再一层一层向上寻找,直到找到全局作用域还是没找到,就宣布放弃。这种一层一层的关系,就是作用域链 。

4. 如何判断一个变量是否为数组?

参考答案:

  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. 通过 *isArray( )*方法: Array.isArray( )
Array.isArray([]) //true

5. 可以写一个函数来判断变量类型吗?

参考答案:

function getType(data){
    let type = typeof data;
    if(type !== "object"){
        return type
    }
    return Object.prototype.toString.call(data).replace(/^\[object (\S+)\]$/,'$1')
}
function Person(){}
console.log(getType(1)); // number
console.log(getType(true)); // boolean
console.log(getType([1,2,3])); // Array
console.log(getType(/abc/)); // RegExp
console.log(getType(new Date)); // Date
console.log(getType(new Person)); // Object
console.log(getType({})); // Object

6. 基础数据类型和引用数据类型有什么区别?

参考答案:

在 ECMAScript 规范中,共定义了 7 种数据类型,分为 基本类型 和 引用类型 两大类,如下所示:

  • 基本类型String、Number、Boolean、Symbol、Undefined、Null
  • 引用类型Object

基本类型也称为简单类型,由于其占据空间固定,是简单的数据段,为了便于提升变量查询速度,将其存储在栈中,即按值访问。

引用类型也称为复杂类型,由于其值的大小会改变,所以不能将其存放在栈中,否则会降低变量查询速度,因此,其值存储在堆(heap)中,而存储在变量处的值,是一个指针,指向存储对象的内存处,即按址访问。引用类型除 Object 外,还包括 Function 、Array、RegExp、Date 等等。

7. 深拷贝和浅拷贝的区别?如何实现?

参考答案:

  • 浅拷贝:只是拷贝了基本类型的数据,而引用类型数据,复制后也是会发生引用,我们把这种拷贝叫做浅拷贝。浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。

  • 深拷贝:在堆中重新分配内存,并且把源对象所有属性都进行新建拷贝,以保证深拷贝的对象的引用图不包含任何原有对象或对象图上的任何对象,拷贝后的对象与原来的对象是完全隔离,互不影响。

浅拷贝方法

  1. 直接赋值
  2. Object.assign 方法:可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。当拷贝的 object 只有一层的时候,是深拷贝,但是当拷贝的对象属性值又是一个引用时,换句话说有多层时,就是一个浅拷贝。
  3. ES6 扩展运算符,当 object 只有一层的时候,也是深拷贝。有多层时是浅拷贝。
  4. Array.prototype.concat 方法
  5. Array.prototype.slice 方法

深拷贝方法

  1. JSON.parse(JSON.stringify) :用 JSON.stringify 将对象转成 JSON 字符串,再用 JSON.parse 方法把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。这种方法虽然可以实现数组或对象深拷贝,但不能处理函数。
  2. 手写递归

8. 那如何手写一个深拷贝递归,写一下思路

参考答案:

function deepCopy(oldObj, newobj) {
    for (var key in oldObj) {
        var item = oldObj[key];
        // 判断是否是对象
        if (item instanceof Object) {
            if (item instanceof Function) {
                newobj[key] = oldObj[key];
            } else {
                newobj[key] = {};  //定义一个空的对象来接收拷贝的内容
                deepCopy(item, newobj[key]); //递归调用
            }
            // 判断是否是数组
        } else if (item instanceof Array) {
            newobj[key] = [];  //定义一个空的数组来接收拷贝的内容
            deepCopy(item, newobj[key]); //递归调用
        } else {
            newobj[key] = oldObj[key];
        }
    }
}

9. 说说浏览器的事件循环机制

参考答案:

在 js 中任务会分为同步任务和异步任务。

如果是同步任务,则会在主线程(也就是 js 引擎线程)上进行执行,形成一个执行栈。但是一旦遇到异步任务,则会将这些异步任务交给异步模块去处理,然后主线程继续执行后面的同步代码。

当异步任务有了运行结果以后,就会在任务队列里面放置一个事件,这个任务队列由事件触发线程来进行管理。

一旦执行栈中所有的同步任务执行完毕,就代表着当前的主线程(js 引擎线程)空闲了,系统就会读取任务队列,将可以运行的异步任务添加到执行栈中,开始执行。

在 js 中,任务队列中的任务又可以被分为 2 种类型:宏任务(macrotask)与微任务(microtask

但这两种分类已经无法满足现如今复杂的浏览器环境了,所以后续舍弃了宏任务,用一种更加灵活的方式取代了它。

根据W3C的官方解释,每个任务都有不同的类型,同类型的任务必须在一个队列,不同任务队列有不同的优先级,在一次事件循环中,由浏览器自行决定取哪一个队列的任务。但浏览器必须有一个微队列,微队列任务的优先级仅次于主线程,必须先调度执行,其次是延时队列(主要用来处理延时器setTimeOut),最后是交互队列(处理与用户的交互,如点击事件等)。

所以任务的优先级为:微队列 > 延时队列 > 交互队列

10. 那说说哪些属于微队列、延时队列、交互队列分别有哪些任务呢?

参考答案:

微队列任务:

  • Promise.then();(Promise是同步的,then方法是微任务)
  • async/await
  • Object.observe
  • MutationObserver
  • process.nextTick(Node.js 环境)

延时任务队列:

  • setTimeout

交互队列:

  • 各种事件回调

11. 那下面这段代码的执行顺序是什么?

const first = () => (new Promise((resolve, reject) => {
    console.log(3);
    let p = new Promise((resolve, reject) => {
        console.log(7);
        setTimeout(() => {
            console.log(1);
        }, 0);
        setTimeout(() => {
            console.log(2);
            resolve(3);
        }, 0)
        resolve(4);
    });
    resolve(2);
    p.then((arg) => {
        console.log(arg, 5); // 1 bb
    });
    setTimeout(() => {
        console.log(6);
    }, 0);
}))
first().then((arg) => {
    console.log(arg, 7); // 2 aa
    setTimeout(() => {
        console.log(8);
    }, 0);
});
setTimeout(() => {
    console.log(9);
}, 0);
console.log(10);

参考答案: 3 7 10 4 5 2 7 1 2 6 9 8

12. 说说 Promise 的作用是什么

参考答案:

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理且更强大。主要用于解决异步回调地狱的问题,它最早由社区提出并实现,ES6将其写进了语言标准,统一了用法,并原生提供了Promise对象。

Promise有三种状态

  • Pending 状态(进行中)
  • Fulfilled 状态(已成功)
  • Rejected 状态(已失败)

Promise的状态变化一旦改变,就不能再次改变,只能由进行中变成成功/失败。

  • Pending -> Fulfilled
  • Pending -> Rejected

13. ES6箭头函数有什么新特性?

参考答案:

  1. 更简洁的语法,例如
    • 只有一个形参就不需要用括号括起来
    • 如果函数体只有一行,就不需要放到一个块中
    • 如果 return 语句是函数体内唯一的语句,就不需要 return 关键字
  2. 箭头函数没有自己的 thisargumentssuper
  3. 箭头函数 this 只会从自己的作用域链的上一层继承 this

14. 箭头函数与普通函数的区别 ?

参考答案:

  1. 普通函数可以有匿名函数,也可以有具体名函数,但是箭头函数都是匿名函数。
  2. 箭头函数不能用于构造函数,不能使用 new 关键字,普通函数可以用于构造函数,以此创建对象实例。
  3. 箭头函数中 this 的指向不同,在普通函数中,this 总是指向调用它的对象,如果用作构造函数,this 指向创建的对象实例。 箭头函数本身不创建 this,也可以说箭头函数本身没有 this,但是它在声明时可以捕获其所在上下文的 this 供自己使用。
  4. 每一个普通函数调用后都具有一个 arguments 对象,用来存储实际传递的参数。但是箭头函数并没有此对象。
  5. 箭头函数不能用于 Generator 函数,不能使用 yeild 关键字。
  6. 箭头函数不具有 prototype 原型对象。而普通函数具有 prototype 原型对象。

15. 什么是防抖和节流?

参考答案:

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

函数防抖(debounce),指的是短时间内多次触发同一事件,只执行最后一次,或者只执行最开始的一次,中间的不执行。

函数节流(throttle),指连续触发事件但是在 n 秒中只执行一次函数。即 2n 秒内执行 2 次... 。节流如字面意思,会稀释函数的执行频率。

16. 那具体如何实现防抖和节流呢?

参考答案:

防抖实现:

function debounce(func, wait){
    // 设置变量,记录 setTimeout 得到的 id
    let timerId = null;
    return function(...args){
        if(timerId){
            // 如果有值,说明目前正在等待中,清除它
            clearTimeout(timerId);
        }
        // 重新开始计时
        timerId = setTimeout(() => {
            func(...args);
        }, wait);
    }
}

节流实现:

function throttle(func, wait) {
    let context, args;
    let previous = 0;
    return function () {
        let now = +new Date();
        context = this;
        args = arguments;
        if (now - previous > wait) {
            func.apply(context, args);
            previous = now;
        }
    }
}

17. 介绍下 Set、Map、WeakSet、WeakMap 的区别

参考答案:

Set

  • 成员唯一、无序且不重复
  • 键值与键名是一致的(或者说只有键值,没有键名)
  • 可以遍历,方法有 add, delete,has

WeakSet

  • 成员都是对象
  • 成员都是弱引用,可以被垃圾回收机制回收,可以用来保存 DOM 节点,不容易造成内存泄漏
  • 不能遍历,方法有 add, delete,has

Map

  • 本质上是健值对的集合,类似集合
  • 可以遍历,方法很多,可以跟各种数据格式转换

WeakMap

  • 只接受对象作为健名(null 除外),不接受其他类型的值作为健名
  • 键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾机制回收,此时键名是无效的
  • 不能遍历,方法有 get、set、has、delete

 18. 实现一个 sleep 函数

参考答案:

function sleep(delay) {
    var start = (new Date()).getTime();
    while ((new Date()).getTime() - start < delay) {
        continue;
    }
}

function test() {
    console.log('111');
    sleep(2000);
    console.log('222');
}

test()

这种实现方式是利用一个伪死循环阻塞主线程。因为 JS 是单线程的。所以通过这种方式可以实现真正意义上的 sleep

19. 如何改变函数内部的 this 指针的指向?

参考答案:

可以通过 call、apply、bind 方法来改变 this 的指向。 call 和 apply 的功能相同,区别在于传参的方式不一样:

  • fn.call(obj, arg1, arg2, ...)  调用一个函数, 具有一个指定的 this 值和分别地提供的参数(参数的列表)。
  • fn.apply(obj, [argsArray])  调用一个函数,具有一个指定的 this 值,以及作为一个数组(或类数组对象)提供的参数。

bind 和 call/apply 有一个很重要的区别,一个函数被 call/apply 的时候,会直接调用,但是 bind 会创建一个新函数。当这个新函数被调用时,bind( )  的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。

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

参考答案:

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

new 关键字会进行如下的操作:

  1. 创建一个空的简单 JavaScript 对象,即 { }
  2. 链接该对象到另一个对象(即设置该对象的原型对象)
  3. 将步骤 1 新创建的对象作为 this 的上下文
  4. 如果该函数没有返回对象,则返回 this

21. 说说原型与原型链

参考答案:

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

22. async 与 await 的作用

参考答案:

async 是一个修饰符,async 定义的函数会默认的返回一个 Promise 对象 resolve 的值,因此对 async 函数可以直接进行 then 操作,返回的值即为 then 方法的传入函数。

await 关键字只能放在 async 函数内部, await 关键字的作用就是获取 Promise 中返回的内容, 获取的是 Promise 函数中 resolve 或者 reject 的值。

23. JS的垃圾回收机制

参考答案:

JS 具有自动垃圾回收机制。垃圾收集器会按照固定的时间间隔周期性的执行。

JS 常见的垃圾回收方式:标记清除、引用计数方式。

1、标记清除方式:

  • 工作原理:当变量进入环境时,将这个变量标记为“进入环境”。当变量离开环境时,则将其标记为“离开环境”。标记“离开环境”的就回收内存。
  • 工作流程:
  • 垃圾回收器,在运行的时候会给存储在内存中的所有变量都加上标记;
  • 去掉环境中的变量以及被环境中的变量引用的变量的标记;
  • 被加上标记的会被视为准备删除的变量;
  • 垃圾回收器完成内存清理工作,销毁那些带标记的值并回收他们所占用的内存空间。

2、引用计数方式:

  • 工作原理:跟踪记录每个值被引用的次数。
  • 工作流程:
  • 声明了一个变量并将一个引用类型的值赋值给这个变量,这个引用类型值的引用次数就是 1
  • 同一个值又被赋值给另一个变量,这个引用类型值的引用次数加1;
  • 当包含这个引用类型值的变量又被赋值成另一个值了,那么这个引用类型值的引用次数减 1
  • 当引用次数变成 0 时,说明没办法访问这个值了;
  • 当垃圾收集器下一次运行时,它就会释放引用次数是0的值所占的内存。

24. 说说严格模式的限制

参考答案:

什么是严格模式?

严格模式对 JavaScript 的语法和行为都做了一些更改,消除了语言中一些不合理、不确定、不安全之处;提供高效严谨的差错机制,保证代码安全运行;禁用在未来版本中可能使用的语法,为新版本做好铺垫。在脚本文件第一行或函数内第一行中引入"use strict"这条指令,就能触发严格模式,这是一条没有副作用的指令,老版的浏览器会将其作为一行字符串直接忽略。

例如:

"use strict";//脚本第一行
function add(a,b){
	"use strict";//函数内第一行
	return a+b;
}

进入严格模式后的限制

  • 变量必须声明后再赋值
  • 不能有重复的参数名,函数的参数也不能有同名属性
  • 不能使用with语句
  • 不能对只读属性赋值
  • 不能使用前缀 0表示八进制数
  • 不能删除不可删除的属性
  • eval 不会在它的外层作用域引入变量。
  • evalarguments不能被重新赋值
  • arguments 不会自动反应函数的变化
  • 不能使用 arguments.callee
  • 不能使用 arguments.caller
  • 禁止 this 指向全局对象
  • 不能使用 fn.caller 和 fn.arguments 获取函数调用的堆栈
  • 增加了保留字

25. for...in 和 for...of 的区别

参考答案:

JavaScript 原有的 for...in 循环,只能获得对象的键名,不能直接获取键值。ES6 提供 for...of 循环,允许遍历获得键值。

例如:

var arr = ['a', 'b', 'c', 'd'];

for (let a in arr) {
  console.log(a); // 0 1 2 3
}

for (let a of arr) {
  console.log(a); // a b c d
}

26. js 中 arguments 是什么?接收的是实参还是形参?

参考答案:

JS 中的 arguments 是一个伪数组对象。这个伪数组对象将包含调用函数时传递的所有的实参。

与之相对的,JS 中的函数还有一个 length 属性,返回的是函数形参的个数。