JS、ES6

125 阅读21分钟

JS

1、new实现原理(执行过程)(new在调用构造函数时发生了什么)

  • 创建一个新对象, 将新对象的_proto_指向构造函数的prototype
  • 将构造函数的this指向这个新对象并执行
  • 判断返回,构造函数返回了一个对象则返回该对象, 没有则返回新对象
手写版
const customNew = (constructorFn, ...args) => {
    // 1、创建一个新对象,将新对象的__proto__指向构造函数的prototype
    // Object.create用于创建一个纯对象,传入的参数会指向这个对象的__proto__
    const newObj = Object.create(constructorFn.prototype) 
    
    // 2、将构造函数的this指向这个新对象并执行
    const res = constructorFn.apply(newObj, args);
    // const res = constructorFn.call(newObj, ...args)
    
    // 3、返回 --如果构造函数返回了一个对象则返回该对象, 没有则返回新对象
    return res && (typeof res === 'object' || typeof res === 'function') ? res: newObj
}

// 测试
function Person(name, age) {
    this.name = name;
    this.age = age;
    //  return {name:'Emma', age: 12} // 更改返回结果
}

const person = customNew(Person, 'John', 30);
console.log(person.name); // John
console.log(person.age);  // 30
console.log(person instanceof Person); // true

2、原型与原型链

每个构造函数内部都有一个prototype属性,使用构造函数新建的对象内部包含一个指针,这个指针指向构造函数的prototype,这个指针称作原型_proto_。

当在一个对象上查找某个属性时,如果对象内部没有找到这个属性就会去它的原型对象,原型对象的原型,一直往上查找直到找到为止,这就是原型链的概念。

举例:

function Person(name) {
    this.name = name;
    this.age = 18;
    this.sayName = function() {
        console.log(this.name);
    }
}

创建实例 var person = new Person('person')

每个对象的__proto__都是指向它的构造函数的原型对象prototype

person1.__proto__ === Person.prototype

构造函数是一个函数对象,是通过 Function构造器产生的

Person.__proto__ === Function.prototype

原型对象本身是一个普通对象,而普通对象的构造函数都是Object

Person.prototype.__proto__ === Object.prototype

刚刚上面说了,所有的构造器都是函数对象,函数对象都是 Function构造产生的

Object.__proto__ === Function.prototype

Object的原型对象也有__proto__属性指向nullnull是原型链的顶端

Object.prototype.__proto__ === null

3、this

this是执行上下文中的一个属性,它使得对象的方法能够访问自己的属性和方法;

根据不同的场景this有不同的值:

  • 默认绑定「调用函数的对象是window」「指向window」
  • 显式绑定「通过call、apply、bind绑定」「指向第一个参数」
  • 隐式绑定「函数作为某个对象的方法被调用」「指向最后调用它的那个对象」
  • new绑定「通过new创建一个实例对象」「指向这个实例对象」

优先级:new绑定 > 显示绑定 > 隐式绑定 > 默认绑定

call、apply、bind区别:

三者都是改变函数this的方法,第一个参数都是this指向的对象

  • 传入的第二个参数不同,call传入的是参数列表,apply传入的数组,两者都是一次性传入;bind传入的参数列表可以分多次传入
  • call和apply只会临时改变一次this指向,改变之后会立刻执行原函数;bind会永久改变this指向并且会返回一个新的函数但不执行

4、执行上下文和执行栈

执行上下文是一种js代码执行环境的抽象概念;

执行上下文分为:

  • 全局执行上下文「window对象」
  • 函数执行上下文「调用函数创建的」
  • Eval函数执行上下文「运行在eval函数中的代码」

执行上下文生命周期:

  • 创建阶段
  • 执行阶段
  • 回收阶段

执行栈:

执行栈也称调用栈,调用栈是一个 LIFO(后进先出)的数据结构,用于存储执行上下文。当函数被调用时,其执行上下文(Execution Context)会被创建并推入执行栈的顶部;当函数执行完毕,其执行上下文会从执行栈中弹出,控制权返回到调用该函数的上下文(即执行栈中下一个上下文)

5、作用域

(1)、作用域是变量和函数能被访问的区域;

作用域分为:

  • 全局作用域「任意位置访问」
  • 函数作用域「函数内部访问」
  • 块级作用域「{ }包裹的代码块中访问」

(2)、作用域链

查找一个变量时如果在当前作用域没有找到就会往父级作用域甚至更高层级去查找,直到找到该变量或到达全局作用域window对象,这层关系被称作作用域链。

(3)、词法作用域

词法作用域又称静态作用域;变量在定义的时候就确定好了,而不是执行的地方决定。

6、闭包

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

闭包用途:

  • 创建私有变量
  • 延迟变量生命周期「闭包保留了对变量的引用,不会被垃圾回收

闭包是内存泄露吗?

  • 闭包本身不是内存泄漏,闭包中的数据是不会被垃圾回收的

  • 内存泄漏是指想让它回收但是没有被回收掉

  • 但是闭包中的数据是需要被使用到的

不正确的使用闭包才会造成内存泄漏,比如以下情况会造成内存泄漏:

  • 长时间保持对不需要使用的数据的引用
  • 闭包长时间存在但不被访问

常见的使用场景:

  • 封装函数
  • 回调函数
  • 防抖节流
  • 定时器等

内存泄漏几种情况

  • 意外的全局变量
  • 定时器未清除
  • 自定义事件未清除
  • 不正确的使用闭包【长时间保持对不需要使用的数据的引用、长时间没有访问闭包】

如何查看内存泄漏

  • 排查代码中是否存在以上内存泄漏的几种情况
  • 使用Chrome开发者工具(DevTools)
  1. 打开DevTools

    • 按下快捷键F12或右键点击页面并选择“检查”来打开开发者工具。
  2. 进入内存分析面板

    • 在DevTools中,点击“Memory”选项卡,进入内存分析面板。
  3. 录制内存快照

    • 点击“Take snapshot”按钮开始录制内存快照。在录制过程中,执行可能导致内存泄漏的操作,如长时间运行的功能或复杂的交互。
  4. 分析内存快照

    • 查看内存分配的图表,观察内存使用的趋势。如果发现内存持续增长而没有释放,可能存在内存泄漏问题。
    • 在“Objects”选项卡中,查看当前内存中各种对象的分配情况。注意观察是否有某些对象的数量异常增加或占用大量内存。
  5. 查找引用关系

    • 使用“References”功能来查看对象之间的引用关系,查找可能存在的循环引用或未正确释放的引用。
  6. 重复操作

    • 多次重复操作和录制快照,以确保结果的准确性。

7、JS为什么有变量提升,它造成了什么问题

JS引擎在代码执行前有一个解析过程,它会创建一个执行上下文,初始化执行所需要的变量和函数。

优点:

  • 提高性能 js执行前进行的语法检查和预编译只会进行一次,如果没有这一步骤,之后每次执行都需要解析一遍
  • 容错性更好 var定义变量时,先使用再定义的话不会报错会得到undefined

造成的问题:

  • 使用let、const定义时没有变量提升会造成一些问题
  • 将函数赋值给一个变量并调用
foo(); // 报错:TypeError: foo is not a function
var foo = function() {
  console.log('Hello');
};


上面的代码等效于
var foo;
foo(); // 报错:TypeError: foo is not a function
foo = function() {
  console.log('Hello');
};

9、严格模式特点

  • 全局变量必须先声明
  • 禁止使用with【作用域不明确、JavaScript引擎优化代码时,依赖于作用域链的明确性会造成性能问题】
  • 禁止使用创建eval作用域
  • this不能指向window
  • 函数参数不能同名

10、ajax、axios、fecth区别

11、forEach、map、filter区别

标题是否有返回值是否改变原数组是否可以链式调用
forEach本身不会改变原数组,除非对元素进行修改
map返回一个新的数组
filter返回符合条件的数组元素

map不会对空数组进行检测

12、事件循环

JavaScript 的事件循环(Event Loop)是处理异步操作的机制

1、核心概念
  • 调用栈(Call Stack)

    • 调用栈是一个 LIFO(后进先出)的数据结构,用于存储执行上下文。每当一个函数被调用时,它的执行上下文就被压入调用栈,函数执行完毕后,从调用栈中弹出。
  • 消息队列(Message Queue)

    • 消息队列是一个 FIFO(先进先出)的数据结构,用于存储各种消息或回调函数。当调用栈为空时,事件循环会从消息队列中取出第一个消息,并将其对应的回调函数压入调用栈,开始执行。
  • 事件循环(Event Loop)

    • 事件循环不断地检查调用栈是否为空。如果调用栈为空,它会从消息队列中取出下一个消息,并将其对应的回调函数压入调用栈中执行。这个过程不断循环,确保异步操作的回调函数得以执行。
  • 微任务队列(Microtask Queue) : 微任务队列中的任务优先于消息队列中的任务执行。常见的微任务包括:

    • Promise.then, Promise.catch, Promise.finally

    • MutationObserver:用于监听 DOM 变化的回调函数会被添加到微任务队列中。

    • process.nextTick(仅限 Node.js)

事件循环在每次执行完一个宏任务(如从消息队列中取出的 setTimeout 回调)后,都会检查并执行所有的微任务,直到微任务队列为空。

  • 宏任务(Macro Tasks) : 宏任务包括以下常见操作:

    • setTimeout、setInterval

    • I/O 操作:包括文件读取、网络请求(如 XMLHttpRequestfetch 等)等 I/O 操作

    • 事件监听:DOM 事件(如 clickload 等)

    • 浏览器的 UI 渲染和重绘

    • setImmediate(仅限 Node.js)

2、工作流程
  • 进入script则开始第一次宏任务
  • 同步代码
    • 直接执行,所有的同步代码会按顺序压入调用栈并执行,直到调用栈清空。
  • 异步操作
    • 当遇到异步操作(如宏任务 setTimeout、网络请求;微任务Promise等)时,异步操作会在后台执行,其回调函数被放入消息队列中等待执行。
  • 回调函数执行
    • 事件循环检查调用栈是否为空。如果为空,则从消息队列中取出下一个回调函数压入调用栈并执行。

14、前端攻击几种情况

  • XSS攻击(跨站脚本攻击 Cross Site Script)

    手段:将js代码注入网页中,渲染时执行js,如果用户输入的是script标签包裹的代码就会执行恶意脚本

    预防:特殊字符替换(〈、〉等) vue的{{ }}、react的{ }默认屏蔽xss攻击

    除非vue使用v-html、react使用dangerouslySetInnerHTML

  • CSRF(跨站请求伪造 Cross Site Request Forgery)

    手段:诱导用户点击另一个网站接口获取了原网站cookie从而来伪造请求

    预防:严格的跨域机制 + 验证码机制

  • Click Jacking(点击劫持)

    手段: 诱导界面上蒙上一层透明的iframe蒙层

    预防:

if (top.localtion.host !== seft.localtion.host) {
    alter(...);
    top.localtion.host = seft.localtion.host
}

Headers里面设置
X-iframe-option: sameorigin
  • DDos

    Distribute denial-of-service 分布式拒绝服务

    手段:分布式的、大规模的流量访问,是服务器瘫痪

    预防:软件层不好做,需要硬件预防(如阿里云WAF)

  • SQL注入

    手段:黑客提交内容时写入SQL语句,破坏数据库

    预防:特殊字符替换

15、防抖节流

  • 防抖

    防止抖动、‘先抖完再执行下一步’,比如一个输入框,无论你持续输入多少次,等你输入完成之后再处理,或者resize事件

    限制执行次数,多次密集的触发至只执行一次,关注结果

function debounce(fn, delay = 200) {
    let timer = null;
    
    return function(...args) {
        const context = this
        if (timer) clearTimeout(timer); // 当前存在任务则清除当前任务,delay事件后再执行新任务
        timer = setTimeout(() => {
            fn.apply(context, args);
            timer = null; // 重置定时器
        }, delay)
    }
}

const debouncedFunction = debounce(() => {
    console.log('Debounced function executed');
}, 300);

window.addEventListener('resize', debouncedFunction);

  • 节流

    节省相互沟通,‘一个一个来,按时间节奏,插队无效’,比如drag和scroll期间触发回调,要设置一个时间间隔

    限制执行频率,有节奏的执行,关注过程

  function throttle(fn, delay = 200) {
    let timer;
    return function(...args) {
      const context = this;
      if (timer) return; //如果当前存在任务则不执行新任务,delay事件后再执行新任务
      timer = setTimeout(() => {
        fn.apply(context, args);
        timer = null; // 重置定时器
      }, delay);
    };
  }
  
  const throttledFunction = throttle(() => {
    console.log('Throttled function executed');
}, 1000);

window.addEventListener('scroll', throttledFunction);

16、单点登录

  • 是什么

    单点登录是指多个应用系统中,只需要登录其中一个系统就可以访问其他所有信任的应用系统

  • 如何实现

(1) 相同域名下

  • 将domain设为当前域的父域,父域默认共享cookie,将cookie的path设为跟路径,token保存在父域中

(2)不同域名下

  • 部署一个单独的认证中心,专门用来负责登录,子系统在认证中心登录之后会颁发一个令牌给所有子系统,子系统跨域拿着令牌获取各自的资

  • 利用iframe+postMessage;前端将cookie通过localStorage存储起来,通过iframe+postMessage将同一个cookie存入多个域名下的localStorage,发送请求时即可携带

17、深拷贝、浅拷贝

拷贝对于基本数据类型来说是拷贝值,对引用数据类型来说是拷贝引用地址;

  • 浅拷贝

    浅拷贝拷贝对象时,对,拷贝的值和原数据共享同一份内存地址,原数据变化拷贝的数据也会随之发生变化;

    实现浅拷贝的方法有Object.assign()和扩展运算实现的复制等

const obj = {x: 1, y: [1, 2, 3]};  
const newObj = {...obj};
`newObj.x` 会获得 `obj.x` 的值的一个拷贝,即 `1`(因为 `1` 是基本类型)。
`newObj.y` 会获得 `obj.y` 的引用地址的一个拷贝,而不是数组 `[1, 2, 3]` 的一个新实例。
因此,`newObj.y` 和 `obj.y` 指向的是同一个数组对象。
const shallow1 = (obj) => {
// 非对象或null则直接返回
    if (!obj || typeof obj !== 'object') return obj

    const newObj = {}
    for (let key in obj) {
        // 确保只复制对象自身的属性,而不是原型链上的属性
        if (obj.hasOwnProperty(key)) {
            newObj[key] = obj[key]
        }
    }
    return newObj
}
  • 深拷贝

    深拷贝会开辟一个新的内存地址,两个对象的属性都相同,但是内存地址不同,原数据改变不会影响拷贝的数据 实现深拷贝的方法有loadash实现克隆、JSON.stringify()(不含有 undefined和函数方法)、手写递归实现等

  function deepClone(obj, hash = new WeakMap()) {
    if (typeof obj !== "object" || obj === null) return obj; // 判断是否为对象或null
    if (obj instanceof Date) return new Date(obj); // 判断date
    if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags); // 判断reg
    
    // 如果对象已存在于hash中,则直接返回克隆对象
    if (hash.has(obj)) return hash.get(obj); 
    
    // 对于数组或普通对象,创建新的实例
    let deepCloneObj = Array.isArray(obj) ? [] : {};

    // 将当前对象存入hash中,值暂时为undefined(稍后会被克隆的对象覆盖)
    hash.set(obj, deepCloneObj);

    for (let key in obj) {
    // 确保只复制对象自身的属性,而不是原型链上的属性
      if (obj.hasOwnProperty(key)) {
        clone[key] = deepClone(obj[key], hash); // 递归调用deepClone并传入hash
      }
    }

    return deepCloneObj; // 返回克隆的对象
  }

18、CommonJS模块和ES6模块的区别

标题加载方式返回值引入位置导出导入this指向
CommonJS运行时加载值的拷贝按需module.export/require当前模块
ES6编译时输出接口值的引用顶部export/importundefined

19、垃圾回收机制

在JavaScript中,垃圾回收(Garbage Collection)是一种自动内存管理机制,它可以自动地识别不再使用的变量和对象并将它们从内存中清除,以释放内存空间。

JavaScript中的垃圾回收器会定期扫描内存中的对象,标记那些可达对象和不可达对象。

可达对象指的是当前代码中正在被使用的对象 不可达对象指的是已经不再被引用的对象。 垃圾回收器会将不可达对象标记为垃圾对象,并将它们从内存中清除。

JavaScript中的垃圾回收机制主要有两种:标记清除(Mark-and-Sweep)和引用计数(Reference Counting)。 标记清除是JavaScript中主流的垃圾回收算法,而引用计数则已经很少被使用。

  • 标记清除

    标记清除(Mark-and-Sweep)的工作原理是:垃圾回收器会定期扫描内存中的对象,从根对象开始遍历内存中的所有对象,对于可达对象,通过标记它们来标识它们是可达对象;对于未被标记的对象,就说明它们是不可达对象,需要被清除。该算法的优点是可以处理循环引用的情况,但在执行时间上可能会比较长,影响程序的性能。

    例如,有一个对象A,其中包含一个指向对象B的引用,而对象B也包含一个指向对象A的引用。此时,如果我们不手动清除这两个对象,垃圾回收器就会通过标记清除算法自动识别这两个对象并清除它们。

    实现标记清除(Mark-and-Sweep)算法的主要步骤如下:

创建一个根对象,例如window对象;
遍历根对象及其所有引用的对象,并标记它们是可达对象;
遍历内存中所有对象,如果发现某个对象未被标记,就将其清除。
在JavaScript中,标记清除算法是由浏览器自动完成的,开发者无需手动实现。
  • 引用计数

    引用计数(Reference Counting)的工作原理是:垃圾回收器会记录每个对象被引用的次数,当对象被引用的次数为0时,就将该对象清除。该算法的优点是实现较为简单,但无法处理循环引用的情况,可能会导致内存泄漏。

    例如,有一个对象A,其中包含一个指向对象B的引用,而对象B也包含一个指向对象A的引用。此时,由于对象A和B互相引用的次数不为0,垃圾回收器就无法清除这两个对象,导致内存泄漏。

    实现引用计数(Reference Counting)算法的主要步骤如下:

给每个对象添加一个引用计数器,初始值为0;
当对象被引用时,引用计数器加1;
当对象不再被引用时,引用计数器减1;
当引用计数器为0时,就将该对象清除。
在JavaScript中,引用计数算法也是由浏览器自动完成的,开发者无需手动实现。不过需要注意的是,由于引用计数无法处理循环引用的情况,因此现代浏览器一般采用标记清除算法。

ES6

1、var let const

var 全局变量、变量提升、可以重复定义、可以重新赋值

let const 块级变量、无变量提升、不可以重复定义、可以重新赋值、先定义再使用否则出现暂时性死区

const 定义常量(不可以重新赋值);定义引用数据类型时,指针不变情况下元素可以更改属性或内容、先定义再使用否则出现暂时性

varletconst
作用域函数作用域+全局作用域块级作用域块级作用域
变量提升
重新声明可以不可以不可以
重新赋值可以可以不可以
暂时性死区(未先声明就使用)不存在存在存在

2、箭头函数和普通函数区别

语法格式new 和原型argumentsthis指向call、apply、bind
普通函数function() {}动态可以修改this指向
箭头函数() => {}指向父级的this不可以修改this指向

3、 for..in 和 for..of区别

可遍历数据结构举例是否可以遍历对象
for..in键名key可枚举enumable为true的数据如数组、对象、字符串可以
for..of键值value可迭代有[Symbol.interator]的数据如数组、字符串、Map、Set不可以
重写Object实例的[Symbol.iterator]方法来使用for of
obj[Symbol.iterator] = function*(){ 
    let values = Object.values(obj) 
    // yield加上*表示挨个取出里面的元素 yield * values 
}

4、promise

promise是一种异步编程解决方案;解决了经典的回调地狱问题

  • 三种状态,pending、fulfilled和reject

他的状态由异步操作结果决定,状态一旦改变就不会再变化,(pending->fulfilled or pending->reject)

  • 用法:

promise通过new操作符生成实例,接受resolve和reject两个参数,这两个函数会将状态变成成功或者失败

有then(状态发生改变时调用)、catch、finally实例方法

还有all(都成功则成功,否则失败)、race(第一个成功or失败的状态)、allSetlled(都返回结果。返回新的实例)、reslove、reject、try构造方法

  • 缺点:

    • 无法取消promise,一旦新建它就会立刻执行,中途无法取消
    • 如果不设置回调函数,promise内部抛出错误不会反应到外部
    • 当处于pending状态时无法得知目前发展到哪个阶段

5、async/await

async/await是Generator语法糖,它返回一个promise(如果直接返回一个直接量,会被封装成Promise.reslove())

await的运算结果取决于它等待的是什么:

  • 等待的是promise,则会阻塞后面的代码,等到Promise对象resolve,得到reslove的结果作为await运算值
  • 等待的不是promise则将表达式的运算结果作为await运算值
async/await与promise优势对比

1async/await写法更像同步代码,虽然promise解决了回调地狱但多个链式调用也会增加阅读负担
2async/awaittry catch配合时更好的进行错误处理,promise相比更加冗余
异步解决方案:
1setTimeout
2Generator
3Promise
4async/await

6、Set和Map数据结构

Set

Set数据结构类似于数组,但里面的元素都是唯一的;

方法: add(value)、delete(value)、has(value)、clear()

属性: size获取长度

Map

通过键值对方式来存储值,键可以是任意类型;

方法: get(key)、set(key, value)、has(key)、delete(key)、clear()

属性: size获取长度

一些区别
Set和Map的异同点

相同点:

  • 都会自动按照某种规则(如元素的自然顺序或指定的比较器)进行排序
  • 都支持快速查找、插入和删除操作
  • 都有迭代器(Iterator)接口,可以通过for of进行遍历

不同点:

  • Set存储的是值,Map存储的是键值对
  • Set存储的元素都是唯一的、Map存储的键是唯一的,值可以重复
  • 添加元素方法不同, Set是通过add(value), Map通过set(key,value)
Set与数组区别
  • 前者里面的元素是唯一的【Set 能够去重的原因在于它内部实现了唯一性检查机制,确保集合中的每个元素都是唯一的】;后者元素不唯一
  • 前者是类数组,长度通过size获取; 后者通过length获取长度
  • 增删改查方法不一样
  • 声明方式不同
  • 前者不能通过索引来获取值
Map与对象的区别
  • 前者的键可以是任意数据类型且是唯一的;后者的键只能是字符串或者Symbol
  • 前者长度可以通过size获取;后者需要自行获取(Object.keys()等)
  • 前者的键值顺序是有序的;后者的键是无序的
  • 前者可以轻松完成迭代;后者不能通过for .. of进行迭代
  • 增删改查方法不一样,前者增删改查表现更好
  • 声明方式不同
Set与WeakSet、Map与WeakMap区别

WeakSet的成员必须是对象;WeakMap的key必须是对象;通常用于数据暂存

WeakSet和WeakMap的成员都是弱引用、没有size属性、没有clear()方法、不可以遍历【与垃圾回收机制有关,元素数量是动态变化】