js基础要点

85 阅读17分钟

一、数据类型

有哪些?

8种,undefined,null,number,string,object,symbol,bigInt

其中symbol和bigInt是es6新增的,symbol表示创建独一无二的数据类型,解决了变量命名冲突的问题

怎么检测数据类型?

  1. typeof,null会被判断为object,函数会被判断为function
  2. instanceof, 判断对象的类型,其内部运行机制是判断对象的原型链中能否找到该构造函数的原型
  3. Object.prototype.toString.call()

概述隐式类型转换规则?

隐式类型转换发生在运算符左右,运算符只能操作基本类型,如果是对象类型首先就会转为基本类型

  1. 运算符是+,只要有一个string类型,则另外一个转为string类型,否则,都转为number
  2. 运算符是-,*,/,都转为number
  3. 运算符是==,都转为number,null,undefined除外
  4. 运算符是<,>,如果都是string,比较字母顺序,否则,转为number,比较大小

如何从其他类型转为number?

  1. undefined 为 NaN
  2. null 为 0
  3. true为1,false为0
  4. 都是数字的字符串,转为数值,带有非数字的字符串,转为NaN
  5. Object先调用valueOf,后调用toString
  6. Symbol不能转为数字

如何从其他类型转为string?

  1. null和undefined,结果为“null”,“undefined”
  2. true,false,结果为“true”,“false”
  3. number直接转换
  4. symbol,只允许显示强制转换,不允许隐式转换
  5. object,先调用toString,后调用valueOf,转为基本类型,再遵循以上规则

包装类型是什么?

JavaScript中,基本类型没有属性和方法,为了方便处理基本类型,在调用基本类型的属性和方法时,JavaScript会做自动包装为对象, 包装后的类型就是object了

isNaN和Number.isNaN的区别?

isNaN会尝试将这个参数转为数值,任何不能被转为数值的值都会返回true

Number.isNaN会判断传入参数是否为数字,如果是数字再继续判断是否为NaN,不会进行数据类型的转换,这个方法对判断更准确

0.1 + 0.2 为什么不等于0.3?

计算机中数据是用二进制存储的,0.1和0.2的二进制都是无限循环的数,导致相加不等于0.3

可以设置一个误差范围,只要判断差值小于这个精度范围,就可以判断相等

二、闭包

什么是闭包?

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

函数执行时形成的私有上下文,正常情况下,代码执行完会出栈后释放;但是特殊情况下,如果当前私有上下文中的某个东西被上下文以外的事物占用了,则上下文不会出栈释放,从而形成不销毁的上下文。

闭包的用途?

  1. 封装私有化变量
  2. 模仿块级作用域
  3. 创建模块

三、原型和原型链

原型是什么?

在js中,每个函数对象有个属性prototype,这个属性指向的就是函数的原型对象

原型对象的constructor属性指向了函数本身

原型链是什么?

对象上有一个指针__proto__,该指针指向构造函数的原型对象,每一层的原型对象都可以通过指针向上关联,直到指向Object的原型对象,Object的原型对象的上层为null,表示原型链的顶端,这就是原型链,或原型链继承

原型链是通过引用关联的,并没有创建新的副本,当我们修改原型时,与之相关的对象也会继承这一改变

四、作用域和作用域链

作用域是什么?

变量与函数的可访问范围,由当前环境与上层环境的一系列变量对象组成,包括全局作用域和函数作用域

全局作用域中的变量和函数,在程序的任何地方都可以被访问,函数作用域则只有在固定的代码片段中访问

作用域链是什么?

一般情况下,变量到 创建该变量 的函数的作用域中取值。但是如果在当前作用域中没有查到,就会向上级作用域去查,直到查到全局作用域,这么一个查找过程形成的链条就叫做作用域链。

五、this的5种使用场景

1. 作为普通函数执行时

this指向window。

2. 当函数作为对象的方法被调用时

this就会指向该对象。

3. 构造器调用

this指向构造器返回的这个对象。

4. 箭头函数

箭头函数的this绑定取决于该箭头函数定义在哪个this上下文,由词法作用域决定,而不是调用的作用域

5. 基于Function.prototype上的 apply、call 和 bind 调用模式

这三个方法都可以显示的指定调用函数的 this 指向。若为空默认是指向全局对象window。

  1. apply接收参数的是数组
  2. call接受参数列表
  3. bind方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this指向除了使用new 时会被改变,其他情况下都不会改变。

七、new分为几步?

1. 创建一个新的空对象

2. 设置该对象的原型为构造函数的原型对象

3. 将构造函数的this绑定到该对象上

4. 判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。

八、简述事件循环的运行机制

事件循环的运行机制是:

  1. 先会执行栈中的同步任务,当遇到异步任务,加入微任务队列或者宏任务队列
  2. 栈中的内容执行完后执行所有微任务队列
  3. 微任务清空后从宏任务队列中取出最先排队的宏任务并执行
  4. 循环以上几步,直到所有任务都执行完毕

浏览器中的宏任务:

  1. ajax
  2. setTimeout
  3. setInterval
  4. setTmmediate(只兼容ie)
  5. script
  6. requestAnimationFrame
  7. UI渲染

浏览器中的微任务

  1. promise.then
  2. mutationObserver(浏览器提供)
  3. messageChannel

九、JavaScript的内存机制

  1. 栈内存,所有原始数据类型都存储在栈内存中,如果删除一个栈原始数据,遵循先进后出;
  2. 堆内存:引用数据类型会在堆内存中开辟一个空间,并且会有一个十六进制的内存地址,在栈内存中声明的变量的值就是十六进制的内存地址。

十、JavaScript中的模块

es6模块

  1. 在代码编译阶段完成, es6模块不是对象, import可以指定加载某个输出值,而不是加载整个模块
  2. es6模块中,js引擎对脚本静态分析的时候,遇到模块加载命令import就会生成一个只读引用,等到脚本真正执行时,再根据这个只读引用到被加载的模块里取值

commonjs模块

  1. Commonjs模块是一个值的拷贝, 意味着,一旦输出就互不影响了
  2. 在运行时加载,commonjs模块就是对象,在输入时加载整个模块,生成一个对象,然后再从这个对象上读取方法

十一、JavaScript垃圾回收机制

浏览器的Javascript具有自动垃圾回收机制gc,定期(周期性)找出那些不在继续使用的变量,然后释放其内存。

js中垃圾回收的原理是什么?

在js中,最常用的垃圾回收机制是标记清除法,当变量进入执行环境时,被标记为“进入环境”,当变量离开执行环境时,会被标记为“离开环境”。 垃圾回收器会销毁那些带标记的值并回收它们所占用的内存空间。

内存泄露的原因

首先内存泄露的本质是,某些代码操作不能合理释放内存,存在大量不被释放的内存(堆/栈/上下文),导致页面性能会变得很慢

在js中常见的内存泄露原因有:

  1. 大量使用全局变量
  2. 闭包
  3. dom元素的引用
  4. 定时器,没有及时清除,闭包引用变量或者dam元素,

内存泄露的优化手段

  1. 及时清除定时器
  2. 避免不必要的闭包
  3. 避免循环引用,使用WeakMap或者WeakSet来存储引用
  4. 合理使用全局变量
  5. 在定时器的回调中注意及时断开对dom元素的引用

十二、JavaScript的新特性理解

WeakSet

WeakSet是可被垃圾回收的值的集合,包括对象和非全局注册的符号,值只能出现一次,是唯一的

WeakSet 中的值一定是可被垃圾回收的值。大多数原始数据类型可以被任意地创建,并且没有生命周期,所以它们不能被存储。对象和非全局注册的符号可以被存储,因为它们是可被垃圾回收的值。

WeakSet和Set的区别

  1. WeakSet 只能是对象和符号的集合,它不能像 Set 那样包含任何类型的任意值。
  2. WeakSet 持弱引用:WeakSet 中对象的引用为弱引用。如果没有其他的对 WeakSet 中对象的引用存在,那么这些对象会被垃圾回收。 这也意味着集合中没有存储当前值的列表。WeakSet 是不可枚举的。递归调用自身的函数需要一种通过跟踪哪些对象已被处理,来应对循环数据结构的方法。为此,WeakSet 非常适合处理这种情况
// 对传入的 subject 对象内部存储的所有内容执行回调
function execRecursively(fn, subject, _refs = new WeakSet()) {
  // 避免无限递归
  if (_refs.has(subject)) {
    return;
  }

  fn(subject);
  if (typeof subject === "object") {
    _refs.add(subject);
    for (const key in subject) {
      execRecursively(fn, subject[key], _refs);
    }
  }
}

const foo = {
  foo: "Foo",
  bar: {
    bar: "Bar",
  },
};

foo.bar.baz = foo; // 循环引用!
execRecursively((obj) => console.log(obj), foo);

WeakMap

WeakMap 是一种键值对的集合,其中的键必须是对象或非全局注册的符号,且值可以是任意的 JavaScript 类型,并且不会创建对它的键的强引用。换句话说,一个对象作为 WeakMap 的键存在,不会阻止该对象被垃圾回收。一旦一个对象作为键被回收,那么在 WeakMap 中相应的值便成为了进行垃圾回收的候选对象,只要它们没有其他的引用存在。唯一可以作为 WeakMap 的键的原始类型是非全局注册的符号,因为非全局注册的符号是保证唯一的,并且不能被重新创建。

WeakMap和Map的区别

在 JavaScript 里,map API 可以通过使其四个 API 方法共用两个数组(一个存放键,一个存放值)来实现。给这种映射设置值时会同时将键和值添加到这两个数组的末尾。从而使得键和值的索引在两个数组中相对应。当从该映射取值的时候,需要遍历所有的键,然后使用索引从存储值的数组中检索出相应的值。 但这样的实现会有两个很大的缺点:

  1. 首先赋值和搜索操作都是 O(n) 的时间复杂度(n 是键值对的个数),因为这两个操作都需要遍历全部整个数组来进行匹配。
  2. 另外一个缺点是可能会导致内存泄漏,因为数组会一直引用着每个键和值。这种引用使得垃圾回收算法不能回收处理他们,即使没有其他任何引用存在了。

相较之下,WeakMap 的键对象会强引用其值,直到该键对象被垃圾回收,但从那时起,它会变为弱引用。因此,WeakMap

  1. 不会阻止垃圾回收,直到垃圾回收器移除了键对象的引用
  2. 任何值都可以被垃圾回收,只要它们的键对象没有被 WeakMap 以外的地方引用
  3. 但因为 WeakMap 不允许观察其键的生命周期,所以其键是不可枚举的。没有方法可以获得键的列表。如果有的话,该列表将会依赖于垃圾回收的状态,这引入了不确定性。如果你想要可以获得键的列表,你应该使用 Map。

Generator,生成器函数

Generator 对象由生成器函数返回并且它符合可迭代协议和迭代器协议

它允许你定义一个非连续执行的函数作为迭代算法。生成器函数使用 function* 语法编写。最初调用时,生成器函数不执行任何代码,而是返回一种称为生成器的特殊迭代器。通过调用 next() 方法消耗该生成器时,生成器函数将执行,直至遇到 yield 关键字。可以根据需要多次调用该函数,并且每次都返回一个新的生成器,但每个生成器只能迭代一次。

function* makeRangeIterator(start = 0, end = Infinity, step = 1) {
  let iterationCount = 0;
  for (let i = start; i < end; i += step) {
    iterationCount++;
    yield i;
  }
  return iterationCount;
}

next方法也可以接收一个参数用于修改生成器内部状态,传递给 next() 的参数值会被 yield 接收

function* fibonacci() {
  let current = 0;
  let next = 1;
  while (true) {
    const reset = yield current;
    [current, next] = [next, next + current];
    if (reset) {
      current = 0;
      next = 1;
    }
  }
}

const sequence = fibonacci();
console.log(sequence.next().value); // 0
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 2
console.log(sequence.next().value); // 3
console.log(sequence.next().value); // 5
console.log(sequence.next().value); // 8
console.log(sequence.next(true).value); // 0
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 2

yield*表达式可用于委托给另一个generator或者可迭代对象

Proxy

Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

const p = new Proxy(target, handler)

// target 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
// handler 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。
// handler.getPrototypeOf()
// Object.getPrototypeOf 方法的捕捉器。

// handler.setPrototypeOf()
// Object.setPrototypeOf 方法的捕捉器。

// handler.isExtensible()
// Object.isExtensible 方法的捕捉器。

// handler.preventExtensions()
// Object.preventExtensions 方法的捕捉器。

// handler.getOwnPropertyDescriptor()
// Object.getOwnPropertyDescriptor 方法的捕捉器。

// handler.defineProperty()
// Object.defineProperty 方法的捕捉器。

// handler.has()
// in 操作符的捕捉器。

// handler.get()
// 属性读取操作的捕捉器。

// handler.set()
// 属性设置操作的捕捉器。

// handler.deleteProperty()
// delete 操作符的捕捉器。

// handler.ownKeys()
// Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器。

// handler.apply()
// 函数调用操作的捕捉器。

// handler.construct()
// new 操作符的捕捉器。

handler.get(target, property, receiver)

  1. target是目标对象
  2. property是被获取的属性名
  3. receiver是Proxy或者继承Proxy的对象

receiver代表捕捉器执行时正确的上下文

const parent = {
	name: 'parent',
	get value() {
		return this.name;
	},
};

const proxy = new Proxy(parent, {
	// get陷阱中target表示原对象 key表示访问的属性名
	get(target, key, receiver) {
		console.log(target === parent) // target是被代理的对象parent
		console.log(receiver === obj); // receiver是当前正确的上下文对象obj
		console.log(this); // 是这个handler对象
		console.log(target[key]) // 返回的是parent的name: parent
		console.log(Reflect.get(target, key, receiver))  // 返回的是当前上下文里的正确的属性值
		return Reflect.get(target, key, receiver);
	},
});

const obj = {
	name: 'child',
};

// 设置obj继承与parent的代理对象proxy
Object.setPrototypeOf(obj, proxy);

// log: false
obj.value

Reflect

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与 proxy handler 的方法相同。Reflect 不是一个函数对象,因此它是不可构造的。

与大多数全局对象不同 Reflect 并非一个构造函数,所以不能通过 new 运算符对其进行调用,或者将 Reflect 对象作为一个函数来调用。Reflect 的所有属性和方法都是静态的(就像 Math 对象)

Reflect主要是和Proxy配对使用,提供对象语义的默认行为

async

async function 声明创建一个绑定到给定名称的新异步函数。函数体内允许使用 await 关键字,这使得我们可以更简洁地编写基于 promise 的异步代码,并且避免了显式地配置 promise 链的需要。

异步函数总是返回一个 promise。如果一个异步函数的返回值看起来不是 promise,那么它将会被隐式地包装在一个 promise 中。

异步函数的函数体可以被看作是由零个或者多个 await 表达式分割开来的。从顶层代码直到(并包括)第一个 await 表达式(如果有的话)都是同步运行的。因此,不包含 await 表达式的异步函数是同步运行的。然而,如果函数体内包含 await 表达式,则异步函数就一定会异步完成。

每个 await 表达式之后的代码可以被认为存在于 .then 回调中。通过这种方式,可以通过函数的每个可重入步骤来逐步构建 promise 链。而返回值构成了链中的最后一个环。

  1. foo 函数的第一行将会同步执行,其中 await 配置了待定的 promise。然后 foo 的进程将被暂停,并将控制权交还给调用 foo 的函数。
  2. 一段时间后,当第一个 promise 被兑现或拒绝的时候,控制权将重新回到 foo 内。第一个 promise 的兑现结果(如果没有被拒绝的话)将作为 await 表达式的返回值。在这里 1 被赋值给 result1。程序继续执行,并计算第二个 await 表达式。同样的,foo 的进程将被暂停,并交出控制权。
  3. 一段时间后,当第二个 promise 被兑现或拒绝的时候,控制权将重新回到 foo。第二个 promise 的兑现结果将作为第二个 await 表达式的返回值。在这里 2 被赋值给 result2。程序继续执行到返回表达式(如果有的话)。默认的返回值 undefined 将作为当前 promise 的兑现值被返回。
async function foo() {
  const result1 = await new Promise((resolve) =>
    setTimeout(() => resolve("1")),
  );
  const result2 = await new Promise((resolve) =>
    setTimeout(() => resolve("2")),
  );
}
foo();

async function* 声明创建一个绑定到给定名称的新异步生成器函数。

异步生成器函数兼具异步函数和生成器函数的特性,可以在函数体内使用await和yield,这使你能够使用 await 优雅的地处理异步任务,同时利用生成器函数的惰性。

async function* foo() {
  yield await Promise.resolve('a');
  yield await Promise.resolve('b');
  yield await Promise.resolve('c');
}

let str = '';

async function generate() {
  for await (const val of foo()) {
    str = str + val;
  }
  console.log(str);
}

generate();
// Expected output: "abc"

await

await 操作符用于等待一个 Promise 兑现并获取它兑现之后的值。

await 通常用于拆开 promise 的包装,使用方法是传递一个 Promise 作为 expression。使用 await 总会暂停当前异步函数的执行,在该 Promise 敲定(settled,指兑现或拒绝)后继续执行。函数的执行恢复(resume)时,await 表达式的值已经变成了 Promise 兑现的值。

await 总会同步地对表达式求值并处理,处理的行为与 Promise.resolve() 一致,不属于原生 Promise 的值全都会被隐式地转换为 Promise 实例后等待。处理的规则为,若表达式:

是一个原生 Promise(原生Promise 的实例或其派生类的实例,且满足 expression.constructor === Promise),会被直接用于等待,等待由原生代码实现,该对象的 then() 不会被调用。 是 thenable 对象(包括非原生的 Promise 实例、polyfill、Proxy、派生类等),会构造一个新 Promise 用于等待,构造时会调用该对象的 then() 方法。 不是 thenable 对象,会被包装进一个已兑现的 Promise 用于等待,其结果就是表达式的值。