JavaScript 常见面试题速查

2023-05-17 16:43:42 浏览数 (1)

# JavaScript 有哪些数据类型,有什么区别

JavaScript 共 8 种数据类型:

  • Undefined
  • Null
  • Boolean
  • Number
  • String
  • Object
  • Symbol
    • 创建后独一无二且不可变的数据类型
    • 可用于解决可能出现的全局变量冲突
  • BigInt
    • 数字类型,可以表示任意精度格式的整数
    • 使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 的范围

以上数据类型可以分为原始数据类型引用数据类型

  • :原始数据类型 (Undefined, Null, Boolean, Number, String, Symbol, BigInt)
  • 对象:引用数据类型 (对象、数组、函数)

以上两种类型的区别在于存储位置的不同:

  • 原始数据类型直接存储在栈(stack)中的简单数据段
    • 占据空间小、大小固定
    • 属于被频繁使用的数据,所以放入栈中存储
  • 引用数据类型存储在堆(heap)中的对象
    • 占据空间大、大小不固定
    • 如果存储在栈中,会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的妻子地址。当解释器寻找引用值时,会首先检索其栈中的地址,取得地址后从堆中获得实体。

堆和栈的概念存在于数据结构和操作系统内存中:

  • 在数据结构中:
    • 在数据结构中,栈中数据的存取方式为先进后出;
    • 堆是一个优先队列,是按优先级来进行排序的,优先级可以按照大小来规定;
  • 在操作系统中,内存被分为栈区和堆区:
    • 栈区内存由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈
    • 堆区内存一般由开发者分配释放,若开发者不释放,程序结束时可能由垃圾回收机制回收

# 数据类型检查方法有哪些

# typeof

代码语言:javascript复制
typeof 2; // number
typeof true; // boolean
typeof "s"; // string
typeof []; // object
typeof function() {}; // function
typeof {}; // object
typeof undefined; // undefined
typeof null; // object

注意:数组、对象、null 都会被判断为 object,其他判断都正确。

# instanceof

instanceof 可以正确判断对象的类型,其内部运行机制是判断其原型链中能否找到该类型的原型。

代码语言:javascript复制
2 instanceof Number; // false
true instanceof Boolean; // false
"s" instanceof String; // false

[] instanceof Array; // true
function() {} instanceof Function; // true
{} instanceof Object; // true

注意:instanceof 只能正确判断引用数据类型,不能判断基本数据类型。instanceof 可以用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。

# constructor

代码语言:javascript复制
(2).constructor === Number; // true
(true).constructor === Boolean; // true
("s").constructor === String; // true

([]).constructor === Array; // true
(function() {}).constructor === Function; // true
({}).constructor === Object; // true

constructor 有两个作用,一是判断数据的类型,二是对象实例通过 constructor 对象访问它的构造函数。注意,如果创建一个对象来改变它的原型,constructor 就不能用了判断数据类型了。

代码语言:javascript复制
function Fn() {}

Fn.prototype = new Array();

var f = new Fn();

f.constructor === Fn; // false
f.constructor === Array; // true

# Object.prototype.toString.call()

代码语言:javascript复制
var obj2Str = Object.prototype.toString;

obj2Str.call(2); // "[object Number]"
obj2Str.call(true); // "[object Boolean]"
obj2Str.call("s"); // "[object String]"
obj2Str.call([]); // "[object Array]"
obj2Str.call(function() {}); // "[object Function]"
obj2Str.call({}); // "[object Object]"
obj2Str.call(undefined); // "[object Undefined]"
obj2Str.call(null); // "[object Null]"

注意: obj.toString()Object.prototype.toString.call(obj) 结果不一样:

  • 因为 toStringObject 的原型方法,而 ArrayFunction 等类型作为 Object 的实例,都重写了 toString 方法。
  • 不同的对象类型调用 toString 方法时,根据原型链的知识,调用的是对应的重写之后的 toString 方法,而不是去调用 Object 上原型 toString 方法,所以 obj.toString() 不能得到其对象类型,只能将 obj 转换为字符串类型。

# null 和 undefined 的区别

UndefinedNull 都是基本数据类型,分别只有一个值:undefinednull

  • undefined 代表 未定义,一般变量声明了但还没有定义的时候会返回 undefined
  • null 代表 空对象,null 主要用于赋值给一些可能会返回对象的变量,做初始化

undefined 在 JavaScript 中不是一个保留字,即可以使用 undefined 作为一个变量名,但这样很危险,会影响对 undefined 值的判断。可以通过一些方法获得安全的 undefined 值,如 void 0

在使用 typeof 进行判断时,Null 类型会返回 object,这是一个历史遗留问题。

代码语言:javascript复制
null == undefined; // true

null === undefined; // false

# instanceof 原理及实现

instanceof 用于判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。

代码语言:javascript复制
function myInstanceof(left, right) {
  // 获取对象的原型
  let proto = Object.getPrototypeOf(left);
  // 获取构造函数的 prototype 对象
  let prototype = right.prototype;

  // 检查构造函数的 `prototype` 对象是否在对象的原型链中
  while (true) {
    // 如果已经到达原型链的最顶端 null,则返回 false
    if (!proto) return false;
    if (proto === prototype) return true;
    // 沿着原型链向上查找
    proto = Object.getPrototypeOf(proto);
  }
}

# 获取安全的 undefined 值

因为 undefined 是一个标识符,所以可以被当做变量来使用和赋值,这会影响 undefined 的判断。表达式 void 没有返回值,因此返回结果是 undefinedvoid 并不改变表达式的结果,只是让表达式返回值。因此可以用 void 0 来获得 undefined

# Object.is()===== 的区别

  • 使用 == 进行判断时,如果两边类型不一致,会进行强制类型转换
  • 使用 === 进行判断时,如果两边类型不一致,不会做强制类型转换,直接返回 false
  • 使用 Object.is() 进行判断时,一般情况下和 === 相同,不过处理了一些特殊情况,如 -0 0 不再相等,两个 NaN 是相等的

# 什么是 JavaScript 中的包装类型

在 JavaScript 中,基本类型是没有属性和方法的,但为了便于操作基本类型的值,在调用基本类型的属性或方法时 JavaScript 会在后台隐式地将基本类型转换为对象。

代码语言:javascript复制
const s = 'cellinlab'; // 后台转换成 String('cellinlab')
s.length; // 7
s.toUpperCase(); // 'CELLINLAB'

JavaScript 也能使用 Object 函数显示地将基本类型转换为包装类型:

代码语言:javascript复制
var s = 'cellinlab';
Object(s); // String {'cellinlab'}

也可以使用 valueOf 方法将包装类型倒转成基本类型:

代码语言:javascript复制
var s = 'cellinlab';
var s1 = Object(s);
var s2 = s1.valueOf(); // 'cellinlab'

注意:false 包装成类型后变为对象,其非值为 false

# 为什么会有 BigInt

JavaScript 中 Number.MAX_SAFE_INTEGER 表示最大安全数字,计算结果是 9007199254740991,即在这个数范围内不会出现精度丢失(小数除外)。一旦超过这个范围,JavaScript 就会出现计算不准确的情况,在大数计算时不得不依靠一些三方库来解决,因此官方提出了 BigInt 来解决这个问题。

# 如何判断一个对象是空对象

使用 JSON.stringify() 判断

代码语言:javascript复制
JSON.stringify({}) === '{}';

使用 Object.keys() 判断

代码语言:javascript复制
Object.keys({}).length === 0;

# const 对象的属性可以修改吗

const 保证的并不是变量的值不能改动,而是变量指向的那个内存地址不能改动。对于基本类型的数据(数值、字符串、布尔值),其值就保存在变量指向的那个内存地址,因此等同于常量。

但对于引用类型数据(对象、数组),变量指向数据的内存地址,保存的只是一个指针,const 只能保证这个指针是固定不变的,至于它指向的数据结构是不是可变的,就完全不能控制了。

# new 一个箭头函数会发生什么

箭头函数是 ES6 中提出来的,它没有 prototype,也没有自己的 this 指向,更不可以使用 arguments 参数,所以不能 new 一个箭头函数。

new 操作符的实现步骤如下:

  1. 创建一个对象
  2. 将构造函数的作用域赋给新对象(即将对象的 __proto__ 指向构造函数的 prototype
  3. 执行构造函数中的代码,构造函数中的 this 指向该对象(即为这个对象添加属性和方法)
  4. 返回新对象

# 箭头函数的 this 指向哪里

箭头函数不同于传统 JavaScript 中的函数,箭头函数并没有属于自己的 this,它所谓的 this 是捕获其所在上下文的 this 值,作为自己的 this 值。并且由于没有属于自己的 this,所以不会被 new 调用。

可以用 Babel 理解一下箭头函数:

代码语言:javascript复制
// ES6 
const obj = {
  getArrow () {
    return () => {
      console.log(this === obj);
    }
  }
}

转化后:

代码语言:javascript复制
// ES5
var obj = {
  getArrow: function() {
    var _this = this;
    return function() {
      console.log(_this === obj);
    }
  }
}

# 扩展运算符作用及其使用场景

# 对象扩展运算符

对象扩展运算符...用于取出参数对象中所有可遍历属性,拷贝到当前对象中。

代码语言:javascript复制
let bar = { a: 1, b: 2 };
let baz = {...bar}; // { a: 1, b: 2 }

相当于:

代码语言:javascript复制
let bar = { a: 1, b: 2 };
let baz = Object.assign({}, bar); // { a: 1, b: 2 }

Object.assign() 用于对象的合并,将源对象的所有可枚举属性,复制到目标对象:

  • 第一个参数是目标对象,后面的参数都是源对象
  • 如果目标对象与源对象有同名属性,或多个源对象有同名属性,这后面的属性会覆盖前面的属性

同样,如果用户自定义的属性,放在扩展运算符后面,则扩展运算符内部的同名属性会被覆盖掉。

代码语言:javascript复制
let bar = { a: 1, b: 2 };
let baz = {...bar, ...{a: 2, b: 4}}; // { a: 2, b: 4 }

利用上述特性可以很方便地修改对象的部分属性。

注意:扩展运算符对对象的实例的拷贝属于浅拷贝。

# 数组扩展运算符

数据扩展运算符可以将一个数组转为逗号分隔的参数序列,且每次只能展开一层数组。

代码语言:javascript复制
console.log(...[1, 2, 3]); // 1 2 3

console.log(...[1, [2, 3], 4]); // 1 [2, 3] 4

应用:

将数组转换为参数序列

代码语言:javascript复制
function add (x, y) {
  return x   y;
}
const number = [1, 2];
console.log(add(...number)); // 3

复制数组

代码语言:javascript复制
const arr = [1, 2, 3];
const arr2 = [...arr]; // [1, 2, 3]

合并数组

代码语言:javascript复制
const arr1 = [1, 2, 3];
const arr2 = [4, ...arr1, 5, 6]; // [4, 1, 2, 3, 5, 6]

扩展运算符与解构赋值结合起来,用于生成数组

代码语言:javascript复制
const [first, ...rest] = [1, 2, 3, 4, 5];
// first: 1
// rest: [2, 3, 4, 5]

  • 注意:扩展运算符用于数组赋值时,只能放在参数的最后一位,否则会报错

将字符串转为真正的数组

代码语言:javascript复制
[...'hello']; // ['h', 'e', 'l', 'l', 'o']

任何 Iterator 接口的对象,都可以用扩展运算符转为真正的数组

代码语言:javascript复制
function foo () {
  // 用于替换 ES5 中 `Array.prototype.slice.call(arguments)` 写法
  const args = [...arguments];
}

使用 Math 函数获取数组中特定的值

代码语言:javascript复制
const arr = [1, 2, 3, 4, 5];
Math.max(...arr); // 5
Math.min(...arr); // 1

# Proxy 可以实现什么功能

在 Vue3 中通过 Proxy 来替换原本的 Object.defineProperty 实现数据响应式。

Proxy 是 ES6 新增 API,用于自定义对象中的操作。

代码语言:javascript复制
// target 为要代理的对象
// handler 用来定义对象中的操作,可以用来定义 set 或 get 函数
let p = new Proxy(target, handler);

通过 Proxy 实现一个数据响应式:

代码语言:javascript复制
let onWatch = (obj, setBind, getLogger) => {
  let handler = {
    get (target, property, receiver) {
      getLogger(target, property);
      return Reflect.get(target, property, receiver);
    },
    set (target, property, value, receiver) {
      setBind(value, property);
      return Reflect.set(target, property, value);
    }
  };
  return new Proxy(obj, handler);
};

let obj = { a: 1 };
let p = onWatch(
  obj,
  (v, property) => {
    console.log(`set ${property} to ${v}`);
  },
  (target, property) => {
    console.log(`get ${property} = ${target[property]}`);
  }
);
p.a = 2; // set a to 2
p.a; // get a = 2

# 对 JSON 的理解

JSON 是一种基于文本的轻量级数据交换格式。可以被任何的编程语言读取或作为数据格式来传递。

在项目开发中,使用 JSON 作为前后端数据交换的方式,在前端通过将一个符合 JSON 格式的数据序列化为 JSON 字符串,然后将其传递给后端,后端通过 JSON 格式的字符串解析后生成对应的数据结构,一次实现前后端数据传递。

因为 JSON 语法是基于 JavaScript 的,很容易将 JSON 和 JavaScript 中的对象弄混,但是应该注意 JSON 和 JavaScript 中的对象不是一回事,JSON 中对象格式更加严格,如 JSON 中属性值不能为函数,不能出现 NaN 属性值等。

JavaScript 提供了 JSON.stringifyJSON.parse 方法来实现 JSON 的序列化和反序列化。

# JavaScript 脚本延迟加载的方法有哪些

延迟加载即等页面加载完成后再加载 JavaScript 文件,JavaScript 延迟加载有助于提高页面加载速度。常见方法有:

  • defer 属性
    • 该属性可以让脚本加载与文档的解析同步进行,然后在文档解析完成后再执行脚本,这样可以使页面的渲染不再被阻塞
    • 多个设置了 defer 属性的脚本按规范是最后顺序执行的,不过一些浏览器可能实现不同
  • async 属性
    • 该属性会使脚本异步加载,不会阻塞页面的解析过程,但当脚本加载完成后立即执行 JavaScript,这时如果文档没有解析完成的话同样会阻塞
    • 多个 async 属性的脚本的执行顺序不可预测
  • 动态创建 DOM 方法
    • 动态创建 DOM 标签的方法,可以对文档的加载事件监听,当文档加载完成后再动态创建 <script> 标签引入 JavaScript 脚本
  • 使用 setTimeout 延迟方法
    • 设置定时器来延迟加载 JavaScript 脚本
  • 让 JavaScript 最后加载
    • 将 JavaScript 脚本放在文档的底部,来使脚本尽可能在最后来执行

# DOM 和 BOM

DOM (Document Object Model),文档对象模型,指将文档当做一个对象,这个对象主要定义了处理网页内容的方法和接口。

BOM (Browser Object Model),浏览器对象模型,指将浏览器当做一个对象来对待,主要定义了与浏览器进行交互的方法和接口。

  • BOM 的核心是 window 对象,window 对象具有双重角色,既是通过 JavaScript 访问浏览器窗口的接口,又是一个 Global(全局)对象,即网页中定义的任何对象,变量和函数,都作为全局对象的一个属性或方法存在。
  • window 对象含 location 对象、navigator 对象、screen 对象等,且 DOM 的根本对象 document 也是 window 对象的子对象。

# escape、encodeURI、encodeURIComponent 的区别

encodeURI

  • 对整个 URI 进行转义,将 URI 中的非法字符转换为合法字符,所以对于一些在 URI 中有特殊意义的字符不会被转义

encodeURIComponent

  • 对 URI 的组成部分进行转义,所以对一些特殊字符也会进行转义

escape

  • encodeURI 作用相同,不过对于 Unicode 编码为 0xff 之外的字符有时会有区别
  • escape 是直接在字符的 Unicode 编码前加上 %u,而 encodeURI 是先将字符转为 UTF-8 格式,再在每个字节前加 %

# Ajax 的理解,实现一个 Ajax 请求

Ajax (Asynchronous JavaScript and XML),指通过 JavaScript 的异步通信从服务器获取 XML 文档,从中提取数据,再更新当前网页的对应部分,不用刷新网页。

创建 Ajax 请求的步骤:

  1. 使用 open 方法创建 HTTP 请求,该方法需要参数是请求的方法、地址和是否异步及用户认证信息;
  2. 发起请求前,可以添加一些信息和监听函数;
  3. 最后调用 send 向服务器发起请求,可以传入参数作为发送的数据体;
代码语言:javascript复制
const SERVER_URL = `http://localhost:3000/api/users`;

const xhr = new XMLHttpRequest();

xhr.open(`GET`, SERVER_URL, true);

xhr.onreadystatechange = function () {
  if (xhr.readyState === 4) {
    if (xhr.status === 200) {
      const data = JSON.parse(xhr.responseText);
      console.log(data);
    } else {
      console.log(`Error: ${xhr.status}`);
    }
  }
  return;
};

xhr.onerror = function () {
  console.log(`Error: ${xhr.status}`);
};

xhr.responseType = `json`;
xhr.setRequestHeader('Accept', 'application/json');

xhr.send();

# ES6 模块和 CommonJS 模块 异同

  • 区别
    • CommonJS 对模块是浅拷贝,ES6 Module 是对模块的引用,即 ES6 Module 只存只读,不能改变其值,即指针指向不能变,类似 const;
    • import 的接口是 read-only (只读状态),不能修改其变量值,即不能修改其变量的指针指向,当可以改变变量内部指针指向
    • 可以对 CommonJS 重新赋值,但是对 ES6 Module 赋值会编译报错
  • 共同点
    • CommonJS 和 ES6 Module 都可以对引入的对象进行赋值,即对对象内部属性的值进行改变

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

for...of 是 ES6 新增的遍历方式,允许遍历一个含有 iterator 接口的数据结构(数组、对象等)并且返回各项的值,和 ES3 中的 for...in 的区别:

  • for...of 遍历获取的是对象的键值,而 for...in 遍历获取的是对象的键名;
  • for...of 只遍历当前对象不会遍历原型链,for...in 会遍历对象的整个原型链,性能非常差,不推荐使用
  • 对于数组的遍历,for...in 会返回数组中所有可以枚举的属性(包括原型链上可枚举的属性),for...of 只返回数组的下标对应的属性值;

总结:

  • for...in 循环主要是为了遍历对象而生,不适合遍历数组
  • for...of 循环可以用来遍历数组、类数组对象,字符串、Set、Map 及 Generator 对象

# Ajax、 axios 和 fetch 的区别

# Ajax

Asynchronous JavaScript and XML 指一种创建交互式网页应用的网页开发技术。是在一种无需重新加载整个网页的情况下,能够更新部分网页的技术。通过在后台与服务器进行少量数据交换,Ajax 可以使网页实现异步更新。

传统的网页如果需要更新内容,需要重载整个页面。其缺点如下:

  • 本身是针对 MVC 编程,不符合前端 MVVM 的浪潮
  • 基于原生的 XHR 开发,XHR 本身的架构不清晰
  • 不符合关注分离的原则
  • 配置和调用方式非常混乱,而且基于事件的异步模型不友好

# Fetch

Fetch 号称 Ajax 的替代品,是在 ES6 出现的,使用了 ES6 中的 Promise 对象。Fetch 是基于 Promise 设计的,其代码结构比 Ajax 简单多,它不是对 Ajax 的进一步封装,而是原生的 JavaScript,没有使用 XMLHttpRequest 。

其优点有:

  • 语法简洁,更加语义化
  • 基于标准 Promise 实现,支持 async/await
  • 更加底层,提供的 API 丰富(request, response)
  • 脱离了 XHR ,是 ES 规范里新的的实现方式

缺点有:

  • 只对网络请求报错,对 400,500 都当做成功的请求,服务器返回 400,500 错误码不会 reject,只有网络错误导致请求不能完成时,fetch 才会 reject
  • fetch 默认不会带 cookie,需要添加配置项:fetch(url, {credentials: 'include'})
  • fetch 不支持 abort,不支持超时控制,使用 setTimeoutPromise.reject 的实现的超时控制并不能阻止请求过程继续在后台运行,造成了浏览浪费
  • fetch 没有办法原生监测请求的进度,而 XHR 可以

# Axios

Axios 是一种基于 Promise 封装的 HTTP 客户端,其特点如下:

  • 浏览器端发起 XMLHttpRequest 请求
  • Node.js 端发起 HTTP 请求
  • 支持 Promise API
  • 监听请求和返回
  • 对请求和返回进行转化
  • 取消请求
  • 自动转换 JSON 数据
  • 客户端支持 XSRF 攻击

# 对原型、原型链的理解

在 JavaScript 中使用构造函数来新建一个对象的,每一个构造函数内部都有一个 prototype 属性,属性值是一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。当使用构造函数新建一个对象后,在这个对象的内部将包含一个指针,指向构造函数的 prototype 属性对应的值,在 ES5 中这个指针称为对象的原型,可以通过 __proto__ 属性来访问,但最好不要在实践中使用,因为他不是规范中规定的。ES5 中新增了 Object.getPrototypeOf() 方法,可以通过这个方法来获取对象的原型。

当访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里去找这个属性,这个原型对象又会有自己的原型,于是就一直找下去,即原型链。原型链的尽头一般来说都是 Object.prototype 所以这就是新建的对象为什么能使用 toString 方法的原因。

特点:JavaScript 对象是通过引用来传递的,创建的每个新对象实例中并没有一份属于自己的原型副本。当修改原型时,与之相关的对象也会继承这一改变。

# 原型链的终点是什么?如何打印出原型链的终点?

由于 Object 是构造函数,原型链终点 Object.prototype.__proto__,而 Object.prototype.__proto__ === null,所以,原型链的终点是 null

原型链上的所有原型都是对象,所有的对象最终都是由 Object 构造的,而 Object.prototype 的再上一层是 Object.prototype.__proto__

# 作用域 和 作用域链

  • 全局作用域
    • 最外层函数和最外层函数外面定义的变量拥有全局作用域
    • 所有未定义直接赋值的变量自动声明为全局作用域
    • 所有 window 对象的属性拥有全局作用域
    • 全局作用域由很大的弊端,过多的全局作用域变量会污染全局命名空间,引起命名冲突
  • 函数作用域
    • 声明在函数内部的变量,一般只有固定的代码片段可以访问到

作用域是分层的,内层作用域可以访问外层,反之不行

  • 块作用域
    • ES6 中新增 letconst 指令可以声明块级作用域
    • 块级作用域可以在函数中创建,也可以在一个代码块({})中创建
    • letconst 声明的变量不会有变量提升,也不可以重复声明
    • 在循环中比较适合绑定块级作用域,可以将声明的计数器变量限制在循环内
  • 作用域链
    • 在自己作用域中找不到变量就去父级作用域查找,依次向上级作用域查找,直到访问到全局作用域就终止,这一层层关系就是作用域链
    • 作用域链保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,可以访问到外层环境的变量和函数
    • 本质上是一个指向变量对象的指针列表,变量对象是一个包含了执行环境中所有变量和函数的对象
    • 作用域链的前端始终都是当前执行上下文的变量对象,全局执行上下文的变量对象始终是作用域链的最后一个对象
    • 当查找一个变量时,如果当前执行环境中没有找到,可以沿着作用域链向后查找

# this

this 是执行上下文中的一个属性,指向最后一次调用这个方法的对象。在实际开发中,this 的指向可以通过四种调用模式来判断:

  1. 函数调用模式:当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象
  2. 方法调用模式:如果一个函数作为一个对象的方法来调用时,this 指向调用这个方法的对象
  3. 构造器调用模式:如果一个函数用 new 调用时,函数执行前会新创建一个对象,this 指向这个新创建的对象
  4. applycallbind 调用模式:显式指定调用函数的 this 指向。
    • apply,接收两个参数,一个是 this 的绑定对象,一个是参数数组
    • call,第一个参数是 this 的绑定对象,后面的其余参数是传入函数执行的参数
    • bind,传入一个对象,返回一个 this 绑定了传入对象的新函数,这个函数的 this 指向除了使用 new 时会被改变,其他情况下都不会改变

以上四种方式,使用构造器调用模式的优先级最高,然后是显式指定方法,然后是方法调用模式,然后是函数调用模式。

# 异步编程的实现方式

JavaScript 中异步机制可以分以下几种:

  • 回调函数
    • 多个回调函数嵌套的时候会造成回调函数地狱,上下两层的回调函数间的代码耦合度太高,不利于代码的可维护
  • Promise
    • 使用 Promise 可以将嵌套的回调函数转为链式调用
    • 使用这种方法,有时会造成多个 then 的链式调用,可能会造成代码的语义不够明确
  • Generator
    • 可以在函数的执行过程中,将函数的执行全转移出去,在函数外部还可以将执行权转移回来
    • 当遇到异步函数执行的时候,将函数执行权转移出去,当异步函数执行完毕时再将执行权给转移回来
    • Generator 内部对于异步操作的方式,可以以同步的顺序来书写,使用这种方式需要考虑何时将函数的控制权转移回来,需要一个自动执行 Generator 的机制
  • Async/Await
    • async 函数是 GeneratorPromise 实现的一个自动执行的语法糖
    • 内部自带执行器,当函数内部执行到一个 await 语句时,如果语句返回一个 Promise 对象,那么函数将会等待 Promise 对象的状态变为 resolve 后在继续向下执行。因此,可以将异步逻辑转化为同步的顺序来书写,并且这个函数可以自动执行

# 对 Promise 的理解

Promise 是异步编程的一种解决方案,它是一个对象,可以获取异步操作的消息,它的出现大大改善了异步编程的困境,避免了地狱回调,它比传统的解决方案回调函数和事件更合理和更强大。

Promise 简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作可以用同样的方法进行处理。

Promise 实例有三种状态:

  • pending,进行中
  • resolved,已完成
  • rejected,已拒绝

当把一件事交给 Promise 时,其状态就是 pending,任务完成了就变成 resolved,任务失败了就变成 rejected

Promise 实例有两个过程:

  • pending -> fulfilled: resolved,已完成
  • pending -> rejected: rejected,已拒绝

注意:一旦从进行状态变为其他状态就永远不能更改状态了。

Promise 的特点:

  • 对象状态不受外界影响
    • Promise 对象代表一个异步操作,有三种状态:pending, resolvedrejected
    • 只有异步操作的结果可以决定当前是哪一种状态,任何其他操作都无法改变这个状态,这也是其名称的由来
  • 一旦状态改变就不会再变,任何时候都可以得到这个结果
    • Promise 对象的状态改变,只有两种可能:从 pending 变为 resolved 或从 pending 变为 rejected
    • 如果改变已经发生了,再对 Promise 对象添加回调函数,也会立即得到这个结果
    • 与事件(Event)完全不同,事件的特点是:如果你错过了它,再去监听是得不到结果的

Promise 的缺点:

  • 无法取消 Promise,一旦新建它就会立即执行,无法中途取消
  • 如果不设置回调函数,Promise 内部错误抛出,不会反应到外部
  • 当处于 pending 状态时,无法得知目前进展到哪一阶段(是刚刚开始还是即将完成)

总结: Promise 对象是异步编程的一种解决方案,最早由社区提出。Promise 是一个构造函数,接收一个函数作为参数,返回一个 Promise 实例。一个 Promise 实例有三种状态:pendingresolvedrejected。实例的状态只能由 pending 变为 resolvedrejected,并且状态一经改变,就固定了,无法再被改变了。

状态的改变时通过 resolve()reject() 来实现,可以在异步操作结束后调用这两个函数改变 Promise 实例的状态,它的原型上定义了一个 then 方法,使用这个 then 方法可以为两个状态的改变注册回调函数。这个回调函数属于微任务,会在本轮时间循环的末尾执行。

注意:在构造 Promise 的时候,构造函数内部的代码是立即执行的。

# Promise 解决了什么问题

传统的多任务异步写法:

代码语言:javascript复制
let fs = require('fs');

fs.readFile('./test.txt', 'utf8', function(err, data1) {
  fs.readFile(data1, 'utf8', function(err, data2) {
    fs.readFile(data2, 'utf8', function(err, data3) {
      console.log(data3);
    });
  });
});

上面的代码有如下缺点:

  • 后一个异步任务的参数依赖于前一个异步任务成功后,将数据往下传递,会导致多个异步函数嵌套的情况,代码不够直观
  • 如果前后两个异步任务不需要传递参数的情况下,那后一个异步任务也需要前一个成功后再执行下一步操作,这种情况下,也需要嵌套,代码不够直观

Promise 的写法:

代码语言:javascript复制
let fs = require('fs');

function read(url) {
  return new Promise((resolve, reject) => {
    fs.readFile(url, 'utf8', (err, data) => {
      if (err) {
        reject(err);
      } else {
        resolve(data);
      }
    });
  });
}

read('./test.txt').then(data1 => {
  return read(data1);
}).then(data2 => {
  return read(data2);
}).then(data3 => {
  console.log(data3);
}

使用 Promise 链式调用,可以顺利解决地狱回调的问题

# 对 Async/Await 的理解

async / await 其实是 Generator 的语法糖,它能实现的洗锅都能用 then 链来实现,它是为了优化 then 链出现的。从字面上看,async 即 “异步”,await 即 “等待”,所以很好地理解 async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。语法上强制规定 await 只能出现在 async 函数中。

代码语言:javascript复制
async function f() {
  return 1;
}

let result = f();
console.log(result); // Promise { 1 }

async 函数(包括函数语句、函数表达式、Lambda 表达式)返回的是一个 Promise 对象,如果函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。

async 函数返回的是一个 Promise 对象,所以在最外层不能用 await 获取其返回值的情况下,当然应该用原来的方式: then() 链来处理这个 Promise 对象。

代码语言:javascript复制
async function f() {
  return 1;
}
let result = f();
result.then(data => {
  console.log(data); // 1
};

如果 async 函数没有返回值,会返回 Promise.resolve(undefined)

在没有 await 的情况下执行 async 函数,它会立即执行,返回一个 Promise 对象,并且不会阻塞后面的语句,这和普通返回 Promise 对象的函数没有区别。

Promise.resolve(x) 可以看做是 new Promise(resolve => resolve(x)) 的简写,可以用于快速封装字面量对象或其他对象,将其封装成 Promise 实例。

# async / await 的优势

单一的 Promise 链并不能发现 async / await 的优势,但是,如果需要处理多个 Promise 组成的 then 链的时候,优势就能体现出来了。(Promise 通过 then 链来解决多层回调的问题,async / await 又进一步优化 then 链的问题)

相对于 Promise 有以下优势:

  • 代码读起来更加同步,Promise 虽然摆脱了回调地狱,但是 then 的链式调用也会带来额外的阅读负担
  • Promise 传递中间值非常麻烦,而 async / await 几乎是同步的写法,非常优雅
  • 错误处理友好,async / await 可以用成熟的 try / catch 方式处理错误,Promise 的错误处理非常冗余
  • 调试友好,Promise 的调试很差,由于没有代码块,不能在一个返回表达式的箭头函数中设置断点
    • 如果你启图在 .then 代码块中使用调试器的 Step-Over 功能,调试器并不会进入后续的 .then 代码块,因为调试器只能跟踪同步代码的每一步

# 对象创建的方式有哪些

一般使用字面量的形式直接创建对象,但是这种创建方式对于创建大量相似对象的时候,会产生大量的重复代码。JavaScript 和一般的面向对象的对象的语言不同,在 ES6 之前它没有类的概念。但是可以使用函数来进行模拟,从而产生可以复用的对象创建方式,常见:

  • 工厂模式
    • 主要工作原理是用函数来封装创建对象的细节,从而通过调用函数来达到复用的目的
    • 有个问题就是创建出来的对象无法和某个类型联系起来,只是简单封装了代码,没有建立对象和类型间的关系
  • 构造函数模式
    • JavaScript 中每一个函数都可以作为构造函数,只要一个函数通过 new 来调用,就可以称其为构造函数
    • 执行构造函数首先会创建一个对象,然后将对象的原型指向构造函数的 prototype 属性,然后将执行上下文的 this 指向这个对象,最后再执行整个函数,如果返回值不是对象,则返回新建的对象。因为 this 的值指向了新建的对象,所以可以使用 this 给对象赋值。
    • 构造函数模式相对于工厂模式
      • 优点:创建的对象和构造函数建立了联系,可以通过原型来识别对象的类型
      • 缺点:造成了不必要的函数对象的创建,因为 JavaScript 中函数也是一个对象,如果对象属性中如果包含函数的话,那么每次都会新建一个函数对象,浪费了不必要的内存空间
  • 原型模式
    • 每一个函数都有一个 prototype 属性,这个属性是一个对象,它包含了通过构造函数创建的所有实例都能共享的属性和方法
    • 可以通过使用原型对象来添加公共属性和方法,从而实现代码的复用,解决了函数复用的问题
    • 也存在一些问题
      • 没有办法通过传入参数来初始化值
      • 如果存在一个引用类型如 Array 这样的值,那么所有的实例将共享一个对象,一个实例对引用类型值的改变会影响所有的实例
  • 组合使用构造函数模式和原型模式
    • 这是创建自定义类型最常见的方式
    • 通过构造函数来初始化对象的属性,通过原型对象来实现函数方法的复用
    • 这种方法很好解决了两种模式单独使用时的缺点,但是也有缺点,因为使用了两种不同的模式,所以对于代码的封装性不够好
  • 动态原型模式
    • 将原型方法赋值的创建过程移动到了构造函数的内部,通过对属性是否存在的判断,可以实现仅在第一次调用函数时对原型对象赋值一次的效果
    • 很好地对上面的混合模式进行了封装
  • 寄生构造函数模式
    • 和工厂模式的实现相似
    • 缺点是无法对对象识别

# 对象继承的方式有哪些

  • 原型链继承
  • 借用构造函数继承
  • 组合继承:将原型链和借用构造函数组合起来使用
  • 原型式继承
  • 寄生式继承
  • 寄生组合继承

# 哪些情况会导致内存泄露

  • 意外的全局变量
    • 由于使用未声明的变量,而意外创建了一个全局变量,而使这个变量一直留在内存中无法被回收
  • 被遗忘的计时器或回调函数
    • 设置了 setInterval() 定时器,忘记取消,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存,而无法被回收
  • 脱离 DOM 的引用
    • 获取一个 DOM 元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以其无法被回收
  • 闭包
    • 不合理使用闭包,从而导致某些变量一直被留在内存当中

0 人点赞