• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

JS 面试合集(1 ~ 10)(2W字)

武飞扬头像
墨渊君
帮助1

引言

考虑到 JS 相关面试内容会比较多, 所以这里会将其拆分多个篇幅进行讲解!!! 没篇固定十道题目!!!

现在, 让我们一起开始吧!!

一、原型、原型链

参考: 原型、原型链

  1. 原型本质上是一个对象, 存在于每个 非箭头函数 中, 所以每个 非箭头函数 都有一个属性 prototype 指向 原型对象

学新通

  1. 原型对象、构造函数、实例对象三者之间的关系
  • 构造函数prototype 属性指向 原型对象
  • 原型对象constructor 属性指向 构造函数
  • 实例对象__proto__ 属性指向 原型对象

学新通

  1. 所有 非空类型 数据, 都具有 原型对象, 因为从本质上它们都是通过对应 构造函数 构建出来的, 所以它们都具有 __proto__ 属性, 指向 构造函数原型对象

  2. 要判断某个值其 原型对象, 只需要确认该值是通过哪个 构造函数 构建的即可, 只要确认了 构造函数 那么该值的 __proto__ 必然指向该 构造函数prototype

  3. 原型链: 根据上文, 所有 非空数据, 都可以通过 __proto__ 指向 原型对象, 同时如果 原型对象 非空, 那么必然同样会有 __proto__ 指向它自己的 原型对象, 如此一层层往上追溯, 以此类推, 就形成了一整条链路, 一直到某个 原型对象null, 才到达最后一个链路的最后环节, 原型对象 之间这种 链路关系 被称之为 原型链(prototype chain)

学新通

  1. 原型链 最后都会到 Object.prototype, 因为 原型对象, 本质上就是个对象, 由 Object 进行创建, 其 __proto__ 指向 Object.prototype, 同时约定 Object.prototype.__proto__ 等于 null, 所有原型链的终点都已 null 结束

学新通

  1. 作用:
  • 实现继承: JS 中继承主要就是通过原型、原型链来实现的
  • 为某一类型数据设置共享属性、方法, 将大大 节约内存
  • 查找属性: 当我们试图访问 对象属性 时, 它会先在 当前对象 上进行搜寻, 搜寻没有结果时会继续搜寻该对象的 原型对象, 以及该对象的 原型对象原型对象, 依次层层向上搜索, 直到找到一个名字匹配的属性或到达原型链的末尾
  1. 补充说明: __proto__ 并不是 ECMAScript 语法规范的标准, 它只是大部分浏览器厂商实现或说是支持的一个属性, 通过该属性方便我们访问、修改原型对象, 从 ECMAScript 6 开始, 可通过 Object.getPrototypeOf()Object.setPrototypeOf() 来访问、修改 原型对象

  2. 总结(来自高程): 每个 构造函数 都有一个 原型对象, 原型 有一个属性指回 构造函数, 而 实例 有一个 内部指针 指向 原型; 如果 原型 是另一个类型的实例呢? 那就意味着这个 原型 本身有一个 内部指针 指向另一个 原型, 相应地另一个 原型 也有一个 指针 指向另一个 构造函数

二、常用继承方案

参考: 什么? chatgpt 居然告诉我 JS 中常见的继承方案有十种?

2.1 原型链继承

  • 思路: 通过将 子类原型对象 指向 父类实例对象 来实现 继承
  • 缺点: 原型 如果包含 引用值, 修改 引用值 所有 实例 都会改动到
  • 缺点: 子类实例化 时不能给 父类构造函数 传参

2.2 盗用构造函数

  • 思路: 在 子类构造函数 中, 调用 父类构造函数 来实现继承
  • 优点: 可解决上文提到的 引用值 问题, 每个 实例 都是新建一个 引用值
  • 优点: 支持为父类构造函数传参
  • 缺点: 不能共用属性、方法, 每次都是重新创建
  • 缺点: instanceof 操作符和 isPrototypeOf() 方法无法识别出 合成对象 继承于哪个父类

2.3 组合继承

  • 思路: 使用 原型链 继承原型上的属性和方法, 通过 盗用构造函数 继承父类属性
  • 优点: 组合继承 弥补了 原型链继承盗用构造函数继承 的不足
  • 优点: 组合继承 也保留了 instanceof 操作符和 isPrototypeOf() 方法识别 合成对象 的能力
  • 缺点: 存在效率问题, 在为 子类 设置 原型 时会额外调用一次 父类构造函数, 会创建无效的属性、方法

2.4 原型式继承

  • 思路: 通过一个 函数, 函数内部会创建一个 临时构造函数, 并将 传入的对象 作为这个构造函数的 原型, 最后返回这个临时类型的一个 实例 来实现继承
  • 适用场景: 基于现有的一个对象, 的基础之上创建新的对象, 并进行适当的修改
  • 缺点: 跟使用 原型模式 类似, 在 原型式继承 中, 引用值属性 始终会在相关对象间 共享
  • 补充: ES5 通过增加 Object.create() 方法将 原型式继承 的概念规范化, 和 原型链继承 的区别在于 原型式继承 不需要自定义类型, 直接通过一个函数来实现继承

2.5 寄生式继承

  • 思路: 寄生式继承原型式继承 很接近, 背后的思路类似于 寄生构造函数模式工厂模式, 通过创建一个实现继承的 函数, 以某种方式增强对象, 然后返回这个对象
  • 适用场景: 寄生式继承 同样适合主要关注对象, 而不在乎 类型构造函数 的场景, 其中 Object.create() 函数不是 寄生式继承必需的, 任何返回新对象的函数都可以在这里使用
  • 缺点: 通过 寄生式继承 给对象添加的函数, 难以被 复用

2.6 寄生式组合继承

  • 思路: 在 组合继承 的思想基础之上进行优化, 修改 子类原型 时不再直接创建父类的实例, 而是通过 寄生式继承继承父类原型, 然后将返回的新对象作为子类原型
  • 优点: 使用 寄生式继承 来弥补 组合继承 的缺点, 设置子类原型时只会对父类原型进行拷贝, 而不是创建父类实例对象, 这样可以避免创建无用是属性、方法, 寄生式组合继承 可以算是 引用类型 继承的最佳模式

2.7 Class 继承

  • 思路: 使用 extends 关键字, 继承任何拥有 [[Construct]]原型 的对象, 很大程度上, 这意味着不仅可以 继承 一个 , 也可以 继承 普通的 构造函数 (向后兼容)
  • 优点: 上文提到的各种继承策略都有自己的问题, 也有相应的妥协, 通过 class 语法糖, 可以轻松实现继承, 避免代码显得非常 冗长混乱

三、New 运算符做了哪些事情

  1. 创建一个新的空对象 A
  2. 往空对象挂载 构造函数 Com原型对象: 对象 A 创建 __proto__ 属性, 并将 构造函数prototype 属性赋值给 __proto__
  3. 执行 构造函数 Com: 改变 构造函数 this 指向, 指向空对象 A, 并执行 构造函数, 往空对象注入属性
  4. 判断 构造函数 是否返回一个对象?
  • 是: 如果 构造函数 也返回了一个对象 B, 则最终 new 出来的对象则为返回的对象 B
  • 否: 最终 new 出来的对象为最初创建的对象 A

因此当我们执行

var o = new Foo();

实际上执行的是:

// 1. 创建一个新的空对象 A
let A = {};

// 2. 往空对象挂载, 挂载构造函数 Com 的原型对象: obj.__proto__ === Con.prototype;
Object.setPrototypeOf(A, Con.prototype);

// 3. 执行构造函数: 改变构造函数 this 指向, 指向对象 A, 往 A 注入属性
let B = Con.apply(A, args);

// 4. 判断构造函数是否返回对象: 是则取返回值、否则取最初创建的对象 A
const newObj = B instanceof Object ? res : A;

手写一个 myNew 函数, 实现上诉操作

const myNew = (Com, ...args) => {
  // 1. 创建一个新的空对象 A
  let A = {};

  // 2. 往空对象挂载, 挂载构造函数 Com 的原型对象: obj.__proto__ === Con.prototype;
  Object.setPrototypeOf(A, Con.prototype);

  // 3. 执行构造函数: 改变构造函数 this 指向, 指向对象 A, 往 A 注入属性
  let B = Con.apply(A, ...args);

  // 4. 判断构造函数是否返回对象: 是则取返回值、否则取最初创建的对象 A
  const newObj = B instanceof Object ? res : A;

  return newObj
}

四、常见的数据类型

4.1 常见类型(8种)

学新通

补充:

  • symbol: 主要用于创建一些唯一标识, 可作为对象的属性名使用, 可通过 Symbol('xxx') 进行声明
  • bigInt: 用于表示比 Number 大的数值 (-2^53 - 1 到 2^53 - 1), 它声明方式有两种:
    • 字面量形式, 通过在数字后面加 n 来进行表示: const bigint = 123n
    • 通过函数 BigInt 来声明: const bigint = BigInt(12312)
  • Symbol BigInt 都不是一个构造函数, 所以 new 关键词的方式来构建实例
  • 创建一个 BigInt 的时候, 参数必须为整数, 否则或报错

4.2 为什么 Symbol 和 bigInt 不支持 new

参考: Symbol、BigInt 不 能 ew, 而 String、Number 可以 new. 为什么?

原因: 我们都知道通过 new 操作符将创建一个对象, 也就是一个引用类型, 但是 SymbolBigInt 应该是要是一个基本类型, 所以它们禁用掉 new 是为了避免创建 SymbolBigInt 时被包装为对象, 因为绝大部分情况下我们并不需要去创建它们值的对象

倔强: 如果一定要创建 SymbolBigInt 原始值的对象, 可以怎么做? 可以使用 Object 包装下

let mySymbol = Symbol() 
mySymbol // Symbol()
typeof mySymbol // "symbol"
let myWrappedSymbol = Object(mySymbol)
myWrappedSymbol // Symbol {Symbol()}
typeof myWrappedSymbol // "object"

怎么做到: 上文我们说到, 使用 new 操作符会创建一个空对象, 然后修改函数的 this 的一个指向并执行, 那么我们可以借用这个特性, 通过判断 this 来确认该函数是否是通过 new 操作符进行调用

// 模拟实现
function A() {
  console.log(this)
  if (this instanceof A) {
    throw new Error('Uncaught TypeError: A is not a constructor')
  }
  return ''
}

A() // window   ''
new A() // A {}  Uncaught TypeError: A is not a constructor

4.3 undefind 和 null

  1. Undefined 类型表示未定义, 该类型只有一个值 undefined, 任何变量在赋值前都是 Undefined 类型数据, 其值为 undefined
  2. 需要注意的是 JavaScript 的代码 undefined 是一个变量, 而并非是一个关键字, 这是 JavaScript 语言公认的设计失误之一
  3. 当我们未给变量赋值时, 变量默认值为 window.undefind(浏览器环境下), window.undefind 该属性值无法被修改
  4. 当我们定义一个变量, 并显性的设置其值为 undefined 时, 该值一般情况指的都是全局变量 undefined, 但由于 undefined 是变量而不是关键字, 在作用域内我们完全可以对 undefined 变量进行篡改, 重新定义, 所以为了避免无意中被篡改, 建议使用 void 0 来获取 undefined
(function () {
  var undefined = 1;
  var a = undefined;
  console.log('[ a ]', a); // 1
})();

(function () {
  var undefined = 1;
  var a = void 0;
  console.log('[ a ]', a); // undefined
})();
  1. UndefinedNull 有一定的表意差别, Null 表示 定义了但是为空, 故而在实际编程时, 我们一般不会把变量直接显性的赋值为 undefined, 这样可以保证所有值为 undefined 的变量, 都是真正意义上的未赋值状态

  2. Null 类型也只有一个值 null, 它的语义表示空值, 与 undefined 不同, nullJavaScript 关键字, 所以在任何代码中都可以放心用 null 关键字来获取 null

五、变量提升、暂时性死区、var、let、const

参考: 彻底搞懂var、let、const之间的区别

  1. 变量都会提升: 在创建变量时, 无论在哪里进行声明, 变量的声明都会被提升 至当前作用域顶部, 就相当于在顶部进行声明, 需要注意的是初始化还是在 原本地方进行初始化
  2. 使用 let cont 在初始化之前变量是无法被操作的, 这一现象被被称为 暂存死区
// 声明全局变量
var a = 1;
var b = 3;

// 访问全局变量
console.log(a);  // 1
console.log(window.a);  // 1

// 声明函数作用域内的变量
function fun () {
  console.log(b) // undefined
  var b = 2;  // 只属于 fun函数的变量
  console.log(b);  // 2
};
fun();  // 2
console.log(b);  // 3
  1. let const 定义的全局变量, 并不会挂载在顶层对象(globalThis)上, 而是挂载在 Script 作用域上

ES5 中, var 操作符和 function 操作符声明的全局变量, 会被挂载到顶层对象(globalThis), 也就是说 顶层对象的属性全局变量 是等价的, ES6letconstclass 操作符声明的全局变量, 不再挂载在顶层对象(globalThis)下, 而是会被挂载在 Script 作用域下

学新通

特性 var let const
变量提升
重复声明 X X
是否可以更改变量值 X
块级作用域 X
全局定义是否挂载在顶层对象(globalThis)下 X X

六、事件

6.1 事件流

事件流描述的是页面中接受事件的顺序, 主要分为三个阶段分别是 事件捕获阶段 目标阶段 事件冒泡阶段 事件流首先是经过事件捕获阶段、接着是目标阶段、最后是事件冒泡阶段, 如下图

学新通

  1. 事件冒泡: 事件触发是从内往外冒泡, 比如已知嵌套元素 <A><B><C></C></B></A> 为每个元素绑定点击事件并开启冒泡, 则点击 C 将先后触发 C B A 各个元素的点击事件
  2. 事件捕获: 事件触发是从外往内冒泡, 比如已知嵌套元素 <A><B><C></C></B></A> 为每个元素绑定点击事件并开启捕获, 则点击 C 将先后触发 A B C 各个元素的点击事件

下面代码演示代码, 当点击按钮时, 控制台将输出事件在各个阶段被触发的顺序

<style>
  .first {
    width: 200px;
    height: 200px;
    background-color: #0000cc;
  }

  .second {
    width: 100px;
    height: 100px;
    background-color: yellow;
  }
</style>

<div class="first">
  <div class="second">
    <input type="button" value="click" class="btn" />
  </div>
</div>
<script type="text/javascript">
  window.onload = function () {
    register();
  };

  function register() {
    // 获取div以及button
    let div1 = document.getElementsByClassName('first')[0];
    let div2 = document.getElementsByClassName('second')[0];
    let btn = document.getElementsByClassName('btn')[0];

    // 为三个目标标签注册点击事件 冒泡
    div1.addEventListener('click', func);
    div2.addEventListener('click', func);
    btn.addEventListener('click', func);

    // 为三个目标标签注册点击事件 捕获
    div1.addEventListener('click', func, true);
    div2.addEventListener('click', func, true);
    btn.addEventListener('click', func, true);
  }

  function func() {
    let dict = { '1': '捕获阶段', '2': '处于目标阶段', '3': '冒泡阶段' };
    console.log(
      '[ 事件流 ]', 
      `${this.className.toLowerCase()} 的事件在 ${dict[event.eventPhase]} 被触发`
    );
  }
</script>

6.2 事件委托

利用冒泡的原理, 将需要绑定在子元素上的事件, 绑定在父元素或祖先元素上, 当子元素触发事件时, 事件冒泡到父元素上或祖先元素上, 在事件中如果需要可根据事件对象(event)来根据不同事件类型、事件目标进行逻辑处理

<style>
  .p {
    padding: 20px;
    border: 1px solid yellow;
  }

  .c {
    margin: 5px;
    height: 40px;
    border: 1px solid red;
  }
</style>

<div class="p"> 
  <div class="c">0</div>
  <div class="c">1</div>
  <div class="c">2</div>
  <div class="c">3</div>
  <div class="c">4</div>
  <div class="c">5</div>
  <div class="c">6</div>
  <div class="c">7</div>
  <div class="c">8</div>
  <div class="c">9</div>
</div>

<script>
  // 点击事件
  const onClick = e => {
    console.log('[ 点击 ]', e)
  };

  // 绑定事件
  const bindEvent = () => {
    const dom = document.getElementsByClassName('p')[0];
    dom.addEventListener('click', onClick);
  };

  window.onload = () => {
    bindEvent();
  };
</script>
  1. 使用场景: 需要为许多同级元素绑定相同事件函数时, 就可以利用 事件委托, 将它们用一个父元素包裹, 并为父元素绑定事件来统一处理
  2. 优点: 统一处理时间大大提高了 JS 性能、可以动态添加 DOM 元素, 不需要因为元素的变动而修改事件绑定
  3. 注意: 事件委托绑定的元素, 最好是被监听元素的父元素, 因为 事件冒泡 的过程也要耗时, 越接近顶层也就越耗时

七、事件循环、宏任务、微任务

7.1 简述

Event loop 也就是所谓事件循环:

  • 不是 JS 本身的一个机制, 而是 JS 所运行环境的一个机制
  • 作用是为了协调事件、用户交互、脚本、渲染、网络等等
  • 存在一个 消息队列 用于存储任务, 每当用户进行交互时, 比如用户点击、资源加载等, IO 线程 就会往队列中添加任务
  • 同时它将所有任务又可分为 宏任务微任务, 当然对应的也就有了 宏任务列表微任务列表
  • 任务队列是由 event loop 来管理, 然后任务的执行则由 js 引擎进行执行
  • 那么执行顺序: 主线程则不断的从消息队列中查询、获取任务并进行执行, 它会先 执行一个宏任务, 然后 执行所有微任务、 再 执行一个宏任务 …… 如此循环往复
  • 当然在事件循环中, 在检查任务时如果发现 没有 宏任务了, 那么将跳过宏任务的执行, 直接 执行所有微任务
  • 需要注意的是, 在执行任务过程中也会不断的往队列中新增任务
  • 一个宏任务标志了一个 loop(循环) 的开始

学新通

7.2 宏任务包含哪些

  • JS 代码 (可以理解为外层同步代码或主线程代码)
  • 用户交互事件(如鼠标点击、滚动页面、放大缩小等)
  • setTimeout/setInterval (计时器)
  • 页面渲染(解析DOM、计算布局、绘制)
  • 页面跨源通信(postMessage)、与子进程间通信(MessageChannel)
  • 网络请求回调、文件读写回调
  • Promis: 主体部分, 也就是 Promise(fun) 中的函数参数 fun

7.3 微任务包含哪些

  • Promise: 所有涉及到状态变更后才被执行的回调都算微任务, 比如说 thencatchfinally
  • MutationObserver(监听 DOM 变化)
  • process.nextTick(Node.js)

7.4 async 和 await 是如何处理异步任务

实际上 async await 只是 Promise 的语法糖

  1. async 函数返回一个 Promise
  2. await 后面的函数等同于 New Promise() 里的参数(函数)
  3. await 下一行语句, 等同于 .then() 函数里待执行语句
// async await 语法如下
async function async1() {
  await async2()
  console.log('async1 end')
}
async function async2() {
  console.log('async2 end')
}
async1()

// 等价于
new Promise((resolve, reject) => {
  // 执行 await 后的语句
  async2();
  // .....
}).then(() => {
 // 执行a sync1() 函数 await 之后的语句
  console.log('async1 end')
});

7.5 几个题目

  1. 题目一: 最终打印输出 code promise1 promise2 timeout
/* 
1. 最开始微任务列表为 [], 宏任务列表为: [主线程(整个代码块)]
2. 基于事件循环(不断从任务列表中获取任务), 先执行执宏任务(整块代码): 
  - 执行 setTimeout 将回调函数 `() => console.log('timeout')` 添加到宏任务列表
  - 分别执行 promise 将两个回调函数 `() => console.log('promise1')`  `() => console.log('promise2')` 加入微任务列表
  - 最后打印出 code
3. 基于事件循环(不断从任务列表中获取任务), 先执行所有微任务, 打印出 `promise1` `promise2`, 再执行下一个宏任务, 打印出 `timeout`
4. 继续事件循环, 不断查询任务列表、只要有任务则继续执行
*/
setTimeout(() => console.log('timeout'));

Promise.resolve()
  .then(() => console.log('promise1'));

Promise.resolve()
  .then(() => console.log('promise2'));

console.log('code');
  1. 题目二: 最终打印输出 1 3 4 7 5 promise in setTimeout1 then in setTimeout1 setTimeout2
/**
1. 最开始微任务列表为 [], 宏任务列表为: [主线程(整个代码块)]
2. 基于事件循环(不断从任务列表中获取任务), 先执行执宏任务(整块代码):
    - 打印 1
    - 执行 setTimeout 10 秒后将回调函数加到宏任务
    - 执行 Promise, 执行 new Promis() 执行参数函数 打印 3 4, 后将回调函数 .then 部分加到微任务
    - 执行 setTimeout 10 秒后, 将回调函数加到宏任务
    - 打印 7
3. 基于事件循环(不断从任务列表中获取任务), 先执行所有微任务: 打印 5, 执行下一个宏任务: 执行 Promise, 执行 new Promis() 执行参数函数, 打印 promise in setTimeout1, 后将回调函数 .then 部分加到微任务
4. 基于事件循环(不断从任务列表中获取任务), 执行所有微任务: 打印 then in setTimeout1, 执行下一个宏任务: 打印 setTimeout2
5. 继续事件循环, 不断查询任务列表、只要有任务则继续执行
*/

console.log(1)

// setTimeout1
setTimeout(function () {
  new Promise(function (resolve) {
    console.log('promise in setTimeout1')
    resolve()
  }).then(function () {
    console.log('then in setTimeout1')
  })
}, 10)

new Promise(function (resolve) {
  console.log(3)
  for (var i = 100000; i > 0; i--) {
    i == 1 && resolve()
  }
  console.log(4)
}).then(function () {
  console.log(5)
})

// setTimeout2
setTimeout(function () {
  console.log('setTimeout2')
}, 10)

console.log(7)
  1. 题目二: 最终打印输出 script start async2 end Promise script end async1 end promise1 promise2 setTimeout
/*
1. 最开始微任务列表为 [], 宏任务列表为: [主线程(整个代码块)]
2. 基于事件循环(不断从任务列表中获取任务), 先执行宏任务(整块代码):
    - 打印 script start
    - 执行 async1, async1 本质上是一个 Promise, 先执行 await 后的函数, 打印出 async2 end, 后将 await 后的代码等同于 Promise.then 的回调函数, 添加到微任务中
    - 执行 setTimeout, 将回调函数加到宏任务
    - 执行 Promise, 先执行参数函数打印 Promise, 后将 .then 部分加入微任务, 注意这里有两个 .then 都需要加入任务
    - 打印 script end
3. 基于事件循环(不断从任务列表中获取任务), 先执行所有微任务: 打印 async1 end、promise1、promise2, 执行下一个宏任务: 打印 setTimeout
5. 继续事件循环, 不断查询任务列表、只要有任务则继续执行
*/

console.log('script start')

async function async1() {
	await async2()
	console.log('async1 end')
}

async function async2() {
	console.log('async2 end')
}

async1()

setTimeout(function() {
	console.log('setTimeout')
}, 0)

new Promise(resolve => {
  console.log('Promise')
  resolve()
})
.then(function() {
	console.log('promise1')
})
.then(function() {
	console.log('promise2')
})

console.log('script end')

7.6 requestAnimationFrame 既不是宏任务也不是微任务

特性:

  • 当开始执行它的回调时, 在此刻之前注册的所有该类回调, 会一次性执行完(一个 loop 内, 这点很关键)
  • 当该类任务执行完后, 也会执行所有微任务(其实不能称为特性, 毕竟所有脚本任务执行完都要执行所以微任务)

执行时机: 触发时机总是与浏览器的 渲染频率 保持一致

其实, 从规范中, 至始至终找不到宏任务的描述。宏任务的概念, 应该是社区为了区别微任务, 而创造出来的。那么 宏任务 这个概念是否还有意义呢?我认为还是有意义的。它的意义在于前述的执行时机: 一个 loop 的起始阶段, 且一个宏任务标志了一个 loop, 所以我们提宏任务时, 就是指那些在 loop 开始时会去检查的任务, 如此这个名称是有其独特意义的

7.7 参考

八、this 指向问题

所谓 this 其实指的是当前代码所处的上下文, 执行的环境

8.2 全局上下文

非严格模式和严格模式中, 全局 this 始终指向顶层对象 (浏览器中是 window)

this === window // true
'use strict'
this === window;
this.name = 'myj';
console.log(this.name); // myj

8.1 普通函数

普通函数的 this 在调用时绑定的, 完全取决于函数的调用位置(也就是函数的调用方法), this 总是指向调用该函数的对象

  1. 全局调用(直接调用), 在非严格模式下指向顶层对象(浏览器中是 window), 在严格模式下等于 undefind
// 非严格模式
function fun () {
  console.log('this', this)
}

fun() // this Window {}
// 严格模式
"use strict";

function fun () {
  console.log('this', this)
}

fun() // this undefined
  1. 通过对象调用, 函数内 this 指向该对象
const obj = {
  name: 'myj',
  getName(){
    console.log(this.name)
  }
}

obj.getName() // myj
  1. 对象内方法被重新赋值声明, 那么其实等价于全局调用, 这里将沿用全局调用规则
const obj = {
  name: 'myj',
  getName(){
    console.log(this.name)
  }
}

const fun = obj.getName

fun() // undefined
  1. 函数被另一个函数被调用, 本质上还是被直接调用, 也将沿用全局调用规则
function a () {
  console.log('this', this)
}

const obj = {
  name: 'myj',
  getName(){
    console.log(this.name)
    a() // this Window {}
  }
}

obj.getName() // myj
  1. 到这里, 我们很容易知道: 匿名函数、定时器、大部分回调函数它们基本都是全局调用的, 所以沿用全局调用规则
(function () {
  console.log('匿名函数: ', this) // 匿名函数: window {}
})()

setTimeout(() => {
  console.log('定时器: ', this) // 定时器: window {}
}, 0)

Promise.resolve().then(() => {
  console.log('Promise: ', this) // Promise: window {}
})

9.2 箭头函数

箭头函数没有自己的 this, 或者说箭头函数中 this 的值 始终 等于 声明时 所处上下文的 this 指向

const obj = {
  name: 'myj',
  getName: () => {
    console.log(this.name)
  }
}

obj.getName() // undefinde

九、箭头函数和普通函数的区别

9.1 语法(写法)

箭头函数在函数声明上更加简洁

// 箭头函数
const fun = () => {}

// 普通函数
const fun = function () {}

9.2 参数差异: 箭头函数没有 arguments 绑定

规定严格模式下不允许使用 arguments

  • 普通函数里 arguments 代表了调用时传入的参数列表
const arguments = { name: 'myj' }

const fun = function(){
  console.log(arguments);
}
fun(1, 2, 3); // { '0': 1, '1': 2, '2': 3 }
  • 箭头函数会把 arguments 当成一个普通的变量, 顺着作用域链由内而外地查询
const arguments = { name: 'myj' }
const fun2 = () => {
  console.log(arguments);
}

fun2(1, 2, 3); // { name: 'myj' }
  • ES6arguments 可以用 ...rest 取代, 所以完全没必要追求 argument
const fun = function(...rest){
  console.log(rest)
}
fun(1, 2, 3); // [1, 2, 3]


const fun2 = (...rest) => {
  console.log(rest);
}

fun2(1, 2, 3); // [1, 2, 3]

9.3 this 指向不同

不像普通函数, 箭头函数没有自己的 this, 或者说箭头函数中 this 的值 始终 等于 声明时 所处上下文的 this 指向

const obj1 = {
  name: 'obj1',
  getThis(){
    console.log('obj1', this)
  }
}

const obj2 = {
  name: 'obj2',
  getThis: () => {
    console.log('obj2', this)
  }
}

obj1.getThis() // obj1 {name: 'obj1', getThis: ƒ}
obj2.getThis() // obj2 Window {...}
  • 使用 call apply bind 都是无法直接修改 this 指向, 因为箭头函数中 this 指向 始终等于声明时所处上下文的 this 指向
const obj = { name: 'myj' }

const fun1 = function () {
  console.log('fun1', this)
}

const fun2 = () => {
  console.log('fun2', this)
}

fun1.call(obj) // fun1 { name: 'myj' }
fun2.call(obj) // fun2 Window {...}
  • 当然我们可以通过一些特殊手段来修改箭头函数的 this 指向: 通过改变封包环境
function closure(){() => {
  // code
}}
closure.call(another)

9.4 没有原型 prototype

我们都知道普通函数中, 都会有一个 prototype 指向原型对象

function fun () {}
console.log(fun.prototype) // { constructor: ƒ }

特别的是, 箭头函数是没有 prototype 属性的

const fun = () => {}
console.log(fun.prototype) // undefined

那么为什么没有 prototype 属性? 因为 this 指向问题(具体看下面解答👇🏻), 所以箭头函数不应该被作为构造函数使用, 所以也就没必要有该属性(这一块纯属个人猜测)

9.5 不可作为构造函数使用

我们都知道普通函数可以被作为构造函数进行使用

function A () {
  this.name = 'myj'
  this.age = 18
}

new A() // { name: 'myj', age: 18 }

但是呢由于, 箭头函数 this 始终 等于声明时所处上下文, 并且无法被直接修改, 所以是不应该被作为构造函数进行使用的, 同时它没有 prototype 属性所以在使用 new 操作符的情况下将会抛出错误

const A =  () => {}
new A()

学新通

9.6 箭头函数不能有重复的参数命名

在普通函数只有在严格模式下才不允许使用重复的参数命名, 但是对于箭头函数来说, 不论在严格模式还是非严格模式参数重复命名都会报错

// 严格模式下才会报错
function add(x, x) {}

// 箭头函数不论在严格模式还是非严格模式重复命名都会报错
const fun = (x, x) => {}

学新通

十、call apply bind

10.1 相同点

  1. 都可改变 this 指向
  2. 第一参数都是要修改的 this 指向
  3. 第一参数如果是 null 或者 undefined 则默认为 window

10.2 差异

  1. call apply 都会立即执行函数, bind 则是会返回一个新的函数
  2. callbind 第二个参数开始, 依次接收多个参数, apply 第二参数是个数组(个人理解: 毕竟人家是 a 开头的, 表示 array)

10.2 箭头函数 this 指向

箭头函数是 JS 中的一种语法糖, 它具有简洁的语法, 同时箭头函数它的 this 上下文是固定的, 不能直接被改变, 可以间接被修改(上文有提到!)

10.3 源码实现

// call 方法实现: 重点是将当前方法(this) 挂载到 context 后执行
// 1. 在函数原型 (Function.prototype) 上挂载方法
Function.prototype.myCall = function(context, ...args) {
  // 2. 处理函数执行上下文, 判断传入的上下文是否是一个对象, 如果不是默认为 window
  context = (context === undefined || context === null) ? window : Object(context)

  // 3. 将当前方法挂载到上下文中, 使用 symbol 作为 key 避免属性冲突
  const funKey = Symbol();
  context[funKey] = this;
  
  // 4. 执行上下文中挂载的方法, 并透传参数
  var result = context[funKey](...args);
  
	// 5. 删除上下文中挂载的方法
  delete context[funKey];
  
	// 6. 返回执行结果
	return result;
}

// apply 方法实现: 重点是将当前方法(this) 挂载到 context 后执行, 和 call 基本一样, 只是处理参数部分有点区别
// 1. 在函数原型(Function.prototype)上挂载方法
Function.prototype.myApply = function(context, args) {
  // 2. 处理函数执行上下文, 判断传入的上下文是否是一个对象, 如果不是默认为 window
	context = (context === undefined || context === null) ? window : Object(context)

  // 3. 将当前方法挂载到上下文中, 使用 symbol 作为 key 避免属性冲突
  const funKey = Symbol();
	context[funKey] = this;

	// 4. 处理参数并执行上下文中挂载的方法
	const result = context[funKey](Array.isArray(args) ? ...args : void 0);;

	// 5. 删除上下文中挂载的方法
	delete context[funKey];

	// 6. 返回执行结果
	return result;
}

// bind 方法实现: 生成函数, 函数内调用 apply
// 1. 在函数原型(Function.prototype)上挂载方法
Function.prototype.myBind = function(context, ...args) {
  // 2. 声明变量, 存储原方法
  const self = this;

  // 3. 返回新方法, 新方法内使用 apply 调用了原方法, 改变了 this 指向、修改参数
  return function(...args2) {
    return self.apply(context, args.concat(args2));
  }
}

未完待续, 敬请期待!!!!!

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgahaji
系列文章
更多 icon
同类精品
更多 icon
继续加载