玩命加载中 . . .

备战大厂前端面试之JavaScript篇


前端面试题系列文章:

JavaScript

JavaScript 有哪些数据类型,它们的区别?

js 数据类型可以分为基本数据类型和引用数据类型。基本数据类型包括 NumberStringBooleanNullUndefinedSymbolBigInt;引用数据类型包括 ObjectFunctionArray

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

  • 基本数据类型存储在栈中,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储;
  • 引用数据类型则存储在堆中,占据空间大、大小不固定。引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。

堆和栈的概念在数据结构中表现为:

  • 栈中数据的存取方式为先进后出
  • 堆是一个优先队列,是按优先级来进行排序的,优先级可以按照大小来规定

数据类型检测的方式有哪些

  1. typeof
console.log(typeof 2);               // number
console.log(typeof true);            // boolean
console.log(typeof 'str');           // string
console.log(typeof function(){});    // function
console.log(typeof undefined);       // undefined
console.log(typeof []);              // object    
console.log(typeof {});              // object
console.log(typeof null);            // object

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

  1. instanceof

内部运行机制是判断在其原型链中能否找到该类型的原型。

console.log(2 instanceof Number);                    // false
console.log(true instanceof Boolean);                // false 
console.log('str' instanceof String);                // false 
 
console.log([] instanceof Array);                    // true
console.log(function(){} instanceof Function);       // true
console.log({} instanceof Object);                   // true

可以看到,instanceof 只能正确判断引用数据类型,而不能判断基本数据类型。

instanceof 运算符可以用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性。

那为什么不能判断基本数据类型呢?
我们知道 instanceof 的原理是根据该实例对象的构造函数是否等于后面一个构造函数,而既然不能判断基本数据类型说明 2 ,'str'等基本数据类型并没有被实例化成对象。

证明:使用 new 实例化基本数据类型

console.log(new Number(2) instanceof Number);       // true
console.log(new Boolean(true) instanceof Boolean);  // true
console.log(new String('str') instanceof String);   // true
  1. constructor

constructor 有两个作用,一是判断数据的类型,二是对象实例通过 constrcutor 对象访问它的构造函数。

console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(('str').constructor === String); // true
console.log(([]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true
console.log(({}).constructor === Object); // true

上文我们讲到,基本数据类型不是实例化对象,所以不能使用 instanceof 判断类型,但这里为什么可以用 constructor 来判断呢?constrcutor 不是对象实例访问它的构造函数而调用的吗?

当我们使用基本数据类型的字面量创建一个变量时,JavaScript 会自动将其包装成对应的包装对象。这些包装对象具有一个特殊属性叫做 constructor ,它指向相应的构造函数。通过检查变量的 constructor 属性,我们可以判断该变量是否是基本数据类型的包装对象。
例如,对于基本数据类型的数值 5,JavaScript 会将其自动包装成一个 Number 对象。所以 5 这个变量的 constructor 属性指向 Number 构造函数。如果我们使用 5 创建了一个变量 num,那么可以通过 num.constructor === Number 来判断 num 是否是 Number 类型的包装对象。

这些包装对象具有与基本数据类型对应的构造函数,例如:

  • Number 对象是通过 Number 构造函数创建的,用于包装数值类型。
  • String 对象是通过 String 构造函数创建的,用于包装字符串类型。
  • Boolean 对象是通过 Boolean 构造函数创建的,用于包装布尔类型。

注意:如果创建一个对象来改变它的原型,constructor就不能用来判断数据类型了:

function Fn(){};
 
Fn.prototype = new Array();
 
var f = new Fn();
 
console.log(f.constructor===Fn);    // false
console.log(f.constructor===Array); // true
  1. Object.prototype.toString.call()
var a = Object.prototype.toString;
 
console.log(a.call(2));             // [object Number]
console.log(a.call(true));          // [object Boolean]
console.log(a.call('str'));         // [object String]
console.log(a.call([]));            // [object Array]
console.log(a.call(function(){}));  // [object Function]      
console.log(a.call({}));            // [object Object]
console.log(a.call(undefined));     // [object Undefined]   
console.log(a.call(null));          // [object Null]

同样是检测对象 obj 调用 toString 方法,obj.toString() 的结果和 Object.prototype.toString.call(obj) 的结果不一样,这是为什么?
这是因为 toStringObject 的原型方法,而 Array、function 等类型作为 Object 的实例,都重写了 toString 方法。不同的对象类型调用 toString 方法时,根据原型链的知识,调用的是对应的重写之后的 toString 方法( function 类型返回内容为函数体的字符串, Array 类型返回元素组成的字符串…),而不会去调用 Object 上原型 toString 方法(返回对象的具体类型),所以采用 obj.toString() 不能得到其对象类型,只能将 obj 转换为字符串类型;因此,在想要得到对象的具体类型时,应该调用 Object 原型上的 toString 方法。

判断数组的方式有哪些

  1. 通过 instanceof 做判断
obj instanceof Array
  1. 通过原型链做判断
obj.__proto__ === Array.prototype;
  1. 通过 Object.prototype.toString.call() 做判断
Object.prototype.toString.call(obj).slice(8,-1) === 'Array';
  1. 通过 ES6 的 Array.isArray() 做判断
Array.isArrray(obj);
  1. 通过 Array.prototype.isPrototypeOf 做判断
Array.prototype.isPrototypeOf(obj)

null 和 undefined 区别

首先 UndefinedNull 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefinednull

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

typeof null 的结果是什么,为什么?

typeof null 的结果是 Object

在 JavaScript 第一个版本中,所有值都存储在 32 位的单元中,每个单元包含一个小的 类型标签(1-3 bits) 以及当前要存储值的真实数据。类型标签存储在每个单元的低位中,共有五种数据类型:

  • 000: object ,当前存储的数据指向一个对象。
  • 1: int ,当前存储的数据是一个 31 位的有符号整数。
  • 010: double ,当前存储的数据指向一个双精度的浮点数。
  • 100: string ,当前存储的数据指向一个字符串。
  • 110: boolean,当前存储的数据是布尔值。

有两种特殊数据类型:

  • undefined 的值是 (-2)30(一个超出整数范围的数字);
  • null 的值是机器码 NULL 指针(null 指针的值全是 0)

那也就是说 null 的类型标签也是 000 ,和 Object 的类型标签一样,所以会被判定为 Object

0.1 + 0.2 === 0.3 ?

究竟是否相等,只需测试一下即可知晓:

console.log( 0.1 + 0.2 == 0.3);

这里输出的结果是 false,说明两边是不相等的,这是浮点数运算的精度问题导致的。

计算机使用二进制存储小数,而二进制无法精确表示 0.1 和 0.2 这样的十进制小数。

0.1的二进制是0.0001100110011001100…(1100循环),0.2的二进制是:0.00110011001100…(1100循环)

在 IEEE754 中,规定了四种表示浮点数值的方式:单精确度(32位)、双精确度(64位)、延伸单精确度、与延伸双精确度。像 ECMAScript 采用的就是双精确度,也就是说,会用 64 位来储存一个浮点数。

所以实际上,这里错误的不是结论,而是比较的方法,正确的比较方法是使用 JavaScript 提供的最小精度值:

console.log( Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON); // Number.EPSILON == 2.220446049250313e-16

检查等式左右两边差的绝对值是否小于最小精度,才是正确的比较浮点数的方法。这段代码结果就是 true 了。

ES6

for…in… for…of…的区别, 遍历数组用什么

let、const、var 的区别

区别 var let const
是否具有块级作用域
是否存在暂时性死区(声明之前不可使用)
是否存在变量提升
是否添加全局属性
能否重复声明变量
是否必须设置初始值
能否改变指针指向(重新赋值)

块级作用域解决了ES5中存在的两个问题:

  • 内层变量可能覆盖外层变量
  • 用来计数的循环变量泄露为全局变量

const对象的属性可以修改吗?
const 保证的并不是变量的值不能改动,而是变量指向的那个内存地址不能改动。
对于基本数据类型的数据,其值就保存在变量指向的那个内存地址,因此等同于常量。
但对于引用类型的数据来说,变量指向数据的内存地址,保存的只是一个指针,const 只能保证这个指针是固定不变的,至于指针指向的数据结构是不是可变的,就完全不能控制了。

声明变量和声明函数的提升有什么区别?

相同点

只要变量/函数在代码中声明了,无论是在哪个位置声明的,js 引擎都会将它的声明放在范围作用域的顶部,即提升声明。

不同点

  • 变量声明提升:变量声明在进入执行上下文就完成了。
  • 函数声明提升:执行代码之前会先读取函数声明,意味着可以把函数声明放在调用它的语句后面

注意

函数声明会覆盖变量声明,但不会覆盖变量赋值。例如:
同一个名称表示 a ,既有变量声明 var a ,又有函数声明 function a() {}不管二者声明的顺序如何,函数声明始终会覆盖变量声明,也就是说此时 a 的值是 function a() {}

如果在变量声明的同时初始化a,或是之后对a进行赋值,此时a的值是变量的值:

var a; 
a = 1; 
function a() { 
    return true; 
} 
console.log(a);

对 Promise 的理解

Promise 是异步编程的一种解决方案,他的出现大大改善了异步编程的困境,避免了回调地狱。所谓 Promise ,简单来说就是一个容器,里面保存着异步操作,从语法上说,Promise 是一个对象,可以获取异步操作的消息,并提供统一的 API ,各种异步操作都可以用同样的方法进行处理。

Promise 的特点

  • 对象的状态不受外界影响。promise对象代表一个异步操作,有三种状态:Pending 表示进行中、Resolved 表示已完成、Rejected 表示已拒绝。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
  • 一旦状态改变就不会再变,任何时候都可以得到这个结果。promise对象的状态改变,只有两种可能:①pending -> fulfilled: Resolved (已完成);②pending -> rejected: Rejected (已拒绝)。这时就称为 Resolved(已定型)。

Promise 的缺点

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

Promise 解决了什么问题

  1. 回调地狱问题,代码可读性问题。

把函数作为参数层层嵌套请求,造成代码可读性差。Promise 的链式调用的写法就处理好了这样的问题。

  1. 由于回调存在着依赖反转,在使用第三方提供的方法时,存在信任问题。

因为回调函数不能保证什么时候去调用回调,以及使用什么方式去调用回调,而 Promise 一旦确认成功或失败,就不能再被更改。

Promise 成功之后仅调用一次 resolve() ,不会产生回调多次执行的问题。除非 Promise 再次调用。所以 Promise 很好地解决了第三方工具导致的回调多次执行(控制反转)的问题,这个问题也称为信任问题。

  1. 当我们不写错误的回调函数时,会存在异常无法捕获

  2. 导致我们的性能更差,本来可以一起做的但是使用回调,导致多件事导致我们的性能更差,本来可以一起做的但是使用回调,导致多件事情顺序执行,用的时间更多

Promise 的方法

  1. Promise.resolve

Promise.resolve 的返回值也是一个 Promise 对象,会让 Promise 对象进入 Resolve 状态,并将参数传递给后面 then 方法所指定的 onFulfilled 函数。

  1. Promise.reject

Promise.reject 也创建一个 Promise 对象

  1. Promise.then

then 方法接收两个回调函数作为参数。第一个回调函数是 Promise 对象的状态变为 Resolved 时调用,第二个回调函数是 Promise 对象的状态变为 Rejected 时调用。其中第二个参数可以省略。then方法返回的是一个新的 Promise 实例,因此可以采用链式写法(then 方法后面再调用另一个 then 方法)

  1. Promise.catch

该方法相当于 then 方法的第二个参数,指向 reject 的回调函数。不过catch方法还有一个作用,就是在执行resolve回调函数时,如果出现错误,抛出异常,不会停止运行,而是进入catch方法中。

  1. Promise.all

all 方法接受一个数组,数组的每一项都是一个 Promise 对象。当数组中所有的 Promise 的状态都达到 Resolved 的时候, all 方法的状态才会变成 Resolved ,如果有一个 Promise 对象的状态变成 Rejected ,那么 all 方法的状态就会变成 Rejected

  1. Promise.race

race 方法和 all 一样,接受的参数是一个每项都是 Promise 的数组,但是与 all 不同的是,当最先执行完的事件执行完之后,就直接返回该 Promise 对象的值。
如果第一个 Promise 对象状态变成 Resolved ,那自身的状态变成了 Resolved ;反之第一个 Promise变成 Rejected ,那自身状态就会变成 Rejected

race 方法实际应用于:当要做一件事,超过多长时间就不做了,可以用这个方法来解决,如

Promise.race([promise1,timeOutPromise(5000)]).then(res=>{})
  1. Promise.finally

finally 方法不管 Promise 对象最后状态如何,都会执行 finally 方法指定的回调函数。

对 async/await 的理解

js 为什么要进行变量提升,它导致了什么问题?

JavaScript 中的变量提升是指在代码执行前将变量的声明移动到作用域顶部的行为。这意味着可以在声明之前访问变量,尽管其实际赋值在声明之后。

变量提升的主要原因是 JavaScript 解释器在执行代码之前会进行两个步骤:解析(Parsing)和执行(Execution)。

解析阶段会对代码进行扫描,将变量和函数声明提升到作用域顶部,而实际的赋值操作留在原来的位置。这意味着在变量声明之前就可以引用它们。

变量提升可能导致以下问题:

  1. 意外的全局变量:如果在函数内部忘记使用 var、let 或 const 来声明变量,它会被提升到全局作用域,成为一个全局变量。这可能导致命名冲突和意外的变量覆盖。
  2. 变量覆盖:如果在同一作用域中多次声明同名变量,后面的声明将覆盖前面的声明。这可能会导致意外的行为和错误。
  3. 代码可读性:由于变量的声明和实际赋值位置不同,代码的执行顺序可能与其表象不符,给代码的阅读和理解带来困扰。

为了避免变量提升可能导致的问题,推荐的最佳实践是:

  • 在函数的顶部或块级作用域的开始处使用 let 或 const 显式声明变量。
  • 尽可能避免在函数内部使用未声明的变量。
  • 使用严格模式(’use strict’)可以禁用变量提升,并在使用未声明的变量时抛出错误。

总的来说,变量提升是 JavaScript 语言的一种特性,它可以提供一定的灵活性,但也会增加代码的复杂性和难以理解性。因此,在编写 JavaScript 代码时,应注意合理地使用变量声明,并且避免出现变量覆盖和意外的全局变量。

箭头函数与普通函数的区别

  1. 箭头函数比普通函数更加简洁
  • 如果没有参数,就直接写一个空括号即可
  • 如果只有一个参数,可以省去参数的括号;如果有多个参数,就用逗号分隔
  • 如果函数体的返回值只有一句,可以省略大括号;如果函数体不需要返回值,且只有一句话,可以给这个语句前面加一个void关键字,最常见的就是调用一个函数:let fn = () => void doesNotReturn();
  1. 箭头函数没有自己的 this

箭头函数之所以没有自己的 this 值,是因为它们采用了词法作用域绑定的方式。

在 JavaScript 中,函数的 this 值表示当前执行函数的上下文对象。对于普通函数, this 的值在函数被调用时动态确定,取决于调用函数的方式。例如,当函数作为对象的方法调用时, this 将引用该对象。而当函数作为普通函数调用时, this 的值通常是全局对象(在非严格模式下)。

而箭头函数的行为不同。箭头函数在定义时就绑定了外部的 this 值,并且它使用了词法作用域来确定 this 的值。换句话说,箭头函数捕获了定义时的外部作用域的 this 值,并在整个函数生命周期内保持不变。

总结起来,箭头函数不会创建自己的 this ,它只会在自己作用域的上一层继承 this 。所以箭头函数中的 this 的指向在它定义时就已经确定了,之后不会更改。

  1. call()、apply()、bind() 等方法不能改变箭头函数中 this 的指向
var id = 'Global';
let fun1 = () => {
  console.log(this.id)
};
fun1();                     // 'Global'
fun1.call({id: 'Obj'});     // 'Global'
fun1.apply({id: 'Obj'});    // 'Global'
fun1.bind({id: 'Obj'})();   // 'Global'
  1. 箭头函数不能作为构造函数使用

原因有两个主要方面:

  • 没有原型(prototype):

构造函数在 JavaScript 中通常与原型(prototype)相关联,用于创建新对象并继承属性和方法。然而,箭头函数没有自己的原型属性,因此无法像普通函数那样用作构造函数创建对象。

  • 不能改变自身的 this 值:

箭头函数的 this 值是由外部的词法作用域决定的,并且在整个函数生命周期中保持不变。而构造函数通常会使用 new 关键字调用,并将 this 绑定到新创建的对象上。由于箭头函数无法改变自身的 this 值,因此不能与 new 关键字一起使用来创建对象。

  1. 箭头函数没有自己的 arguments

在箭头函数中访问 arguments 实际上获得的是它外层函数的 argument 值。

  1. 箭头函数没有 prototype

  2. 箭头函数不能用作 Generator 函数,不能使用 yeild 关键字

new 操作符具体干了什么?

  1. 创建一个对象
var obj = new object(); 
  1. 设置原型链

将对象的原型设置为函数的 prototype 对象

obj._proto_ = fn.prototype; 
  1. 让函数的 this 指向该对象,并执行构造函数的代码
var result = fn.call(obj); 
  1. 判断fn的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象
if (typeof(result) == "object") { 
  fnObj = result; 
} else { 
  fnObj = obj;
} 

具体实现:

function objectFactory() {
  let newObject = null;
  let constructor = Array.prototype.shift.call(arguments);
  let result = null;
  // 判断参数是否是一个函数
  if (typeof constructor !== "function") {
    console.error("type error");
    return;
  }
  // 新建一个空对象,对象的原型为构造函数的 prototype 对象
  newObject = Object.create(constructor.prototype);
  // 将 this 指向新建对象,并执行函数
  result = constructor.apply(newObject, arguments);
  // 判断返回对象
  let flag = result && (typeof result === "object" || typeof result === "function");
  // 判断返回结果
  return flag ? result : newObject;
}
// 使用方法
objectFactory(构造函数, 初始化参数);

什么是原型和原型链?原型链的问题?

我们创建的每个函数都有一个 prototype(原型) 属性,这个属性是一个指针,指向一个对象,而这个对象的用途是存储可以让所有实例共享的属性和方法。

构造函数、实例、实例原型这三者的关系可以用一张图表示:构造函数、实例、实例原型的关系

JavaScript 中所有的对象都是由它的原型对象继承而来。而原型对象自身也是一个对象,它也有自己的原型对象,这样层层上溯,就形成了一个类似链表的结构,这就是原型链。

当我们在访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去 prototype 里找这个属性,这个 prototype 又会有自己的 prototype ,于是就这样一直找下去,直到 Object._proto_ 为止,找不到就返回 undefined

由于原型链的存在,我们可以让很多实例去共享原型上面的方法和属性,方便了我们的很多操作。但是原型链并非是十分完美的:

  • 引用类型,变量保存的就是一个内存中的一个指针。所以,当原型上面的属性是一个引用类型的值时,我们通过其中某一个实例对原型属性的更改,结果会反映在所有实例上面,这也是原型 共享 属性造成的最大问题
  • 另一个问题就是我们在创建子类型(比如上面的 person)时,没有办法向超类型( Person )的构造函数中传递参数

闭包

什么是闭包

函数嵌套函数,内部函数被外部函数返回并保存下来时,就会产生闭包。

闭包就是在函数内部可以访问到其外部所以能访问到外部的参数和变量,是因为它是顺着作用域链向上查找的。

闭包的应用

  1. 维护函数内的变量安全,避免全局变量的污染。
  2. 维持一个变量不被回收。
  3. 封装模块

闭包的优缺点

优点

  1. 减少全局环境的污染生成独立的运行环境
  2. 可以通过返回其他函数的方式突破作用域链

*缺点

  1. 常驻内存会增大内存使用量,且有可能造成内存泄露。
  2. 闭包会影响脚本性能,包括处理速度和内存消耗。

什么是内存泄露

概念:不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak),会导致内存溢出。

内存溢出:指程序申请内存时,没有足够的内存供申请者使用。例如,给一块存储int类型数据的存储空间,但却存储long类型的数据,那么结果就是内存不够用,此时就会报错,即所谓的内存溢出。

常见的内存泄露

  1. 意外的全局变量:①未定义的变量会在全局对象创建一个新变量;②通过 this 的方法在全局定义了一个新变量。
  2. 闭包引起的内存泄漏(闭包可以读取函数内部的变量,然后让这些变量始终保存在内存中。如果在使用结束后没有将局部变量清除,就可能导致内存泄露)。
  3. 没有清理 DOM 元素的引用(如在一个对象中定义了一个值为 DOM 的键值对)。
  4. 没有移除计时器或回调函数。
  5. 循环引用。

垃圾回收

基本思路:先确定哪个变量不会再使用,然后释放它占用的内存。这个过程是周期性的,即垃圾回收程序每隔一定时间(或者说在代码执行过程中某个预定的收集时间)就会自动运行。

回收策略

  1. 引用计数:跟踪记录每个值被引用的次数,每次引用的时候加一,被释放的时候就减一,当一个值的引用次数变为零,就可以将其内存空间回收。
  • 优点:①发现垃圾时立即回收;②能最大限度减少程序暂停,让空间不会有被占满的时候。
  • 缺点:①无法回收循环引用的对象;②由于要对所有对象进行数值的监控和修改,资源消耗开销大。
  1. 标记清除:分为标记和清除两个阶段。当变量进入执行上下文时,这个变量会被加上 存在于上下文中 的标记,此为标记阶段;当变量离开上下文时,也会被加上离开上下文的标记,此为清除阶段。于是当垃圾回收时就会销毁那些带标记的值并回收他们的内存空间。
  • 优点:①解决了对象循环引用的问题;②回收速度较快。
  • 缺点:①空间碎片化,因为清除那些被标记为离开上下文状态的变量时,是不管删除的这个位置的;②不会立即回收垃圾对象,清除的时候程序是停止工作的。
  1. 标记整理:是标记清除的加强版。在标记和清除中间,添加了内存空间的整理,其实就是在执行清除阶段前,移动对象位置,使他们在地址上保持连续。
  • 优点:相比较与标记清除回收策略,这个方法减少了碎片化的空间。
  • 缺点:不会立即回收垃圾对象,清除的时候程序是停止工作的。
  1. 分代回收:又分为新生代对象回收和老生代对象回收。
  • 新生代对象回收

概念:新生代就是指存活时间较短的对象,例如:一个局部作用域中,只要函数执行完毕之后变量就会回收。

空间分布:新生代内存区分为两个等大小的空间,分别是 使用空间 From空闲空间 To

回收过程

  • 首先会将所有活动对象存储于 From 空间,这个时候 To 空间是空闲状态。
  • 当 From 空间使用到一定程度后就会对活动对象进行 标记整理 回收策略,将使用空间 From 变得连续。
  • 然后将活动对象拷贝至 To 空间,就可以把 From 空间全部释放了,回收完成。
  • 对 From 和 To 名称进行调换,继续重复之前的操作。

缺点:只能使用堆内存的一半。

  • 老生代对象回收

概念:老生代就是指存活时间较长的对象,例如:全局对象,闭包变量数据。

什么情况下对象会出现在老生代空间中

  • 一轮GC之后还存活的新生代对象 就需要晋升。
  • 在拷贝过程中,To 空间的使用率超过 25% ,就将这次的活动对象都移动至老生代空间。

    Q:为什么设置25%这个阈值?
    A:当这次回收完成后,这个To空间会变为From空间,接下来的内存分配将在这个空间中进行。如果占比过高,会影响后续的内存分配。

在这个阶段中,会遍历堆中所有的对象,然后标记活的对象,在标记完成后,销毁所有没有被标记的对象。在标记大型对内存时,可能需要几百毫秒才能完成一次标记。这就会导致一些性能上的问题。为了解决这个问题,2011 年,V8 从 stop-the-world 标记切换到增量标志。在增量标记期间,GC 将标记工作分解为更小的模块,可以让 JS 应用逻辑在模块间隙执行一会,从而不至于让应用出现停顿情况。但在 2018 年,GC 技术又有了一个重大突破,这项技术名为并发标记。该技术可以让 GC 扫描和标记对象时,同时允许 JS 运行。
清除对象后会造成堆内存出现碎片的情况,当碎片超过一定限制后会启动压缩算法。在压缩过程中,将活的对象向一端移动,直到所有对象都移动完成然后清理掉不需要的内存。
Chrome 浏览器垃圾回收机制与内存泄漏分析

宏任务和微任务

采纳 JSC 引擎的术语,我们把宿主发起的任务称为宏观任务,把 JavaScript 引擎发起的任务称为微观任务。

异步执行的顺序:

  • 首先我们分析有多少个宏任务;
  • 在每个宏任务中,分析有多少个微任务;
  • 根据调用次序,确定宏任务中的微任务执行次序;
  • 根据宏任务的触发规则和调用次序,确定宏任务的执行次序;
  • 确定整个顺序。

讲讲 Babel

当我们在编写 JavaScript 代码时,我们希望能够使用最新的语言特性和语法来提高开发效率和代码质量。然而,不同的浏览器和环境对于 JavaScript 的支持程度并不相同,有些浏览器可能不支持最新的 ECMAScript 标准,这就导致了浏览器兼容性的问题。

这时候,Babel 就发挥了作用。Babel 是一个 JavaScript 编译器,它能够将最新版本的 JavaScript 代码转换为向后兼容的版本,以便在不支持新特性的浏览器和环境中运行。

Babel 的工作流程如下:

  1. 解析:Babel 首先会将源代码解析成抽象语法树(AST),这样可以分析和理解代码的结构和语法。
  2. 转换:Babel 使用插件来进行代码转换。每个插件负责一个特定的转换任务。例如,有插件用于将 ES6 的箭头函数转换为 ES5 的函数表达式,或将 ES6 的模块语法转换为 CommonJS 或 AMD 的模块格式。
  3. 生成:在完成代码转换后,Babel 将根据配置生成兼容目标浏览器和环境的代码。

通过配置文件(如 .babelrc 或 babel.config.js),我们可以指定要使用的插件和转换规则,以及目标浏览器的版本和环境。

Babel 不仅仅用于将最新的 ECMAScript 版本转换为兼容的代码,它还支持其他功能,如语法扩展、代码优化和类型检查等。例如,Babel 可以使用 TypeScript 插件来转换 TypeScript 代码为 JavaScript 代码。

Babel 在现代前端开发中非常常用,它使得开发者能够在不同浏览器和环境中使用最新的 JavaScript 特性,同时确保代码的兼容性。许多前端框架和工具链都使用 Babel 作为构建过程的一部分,确保项目能够运行在广泛的浏览器和环境中。

setTimeout 和 Promise 的执行顺序

函数作用域怎么模拟块级作用域


文章作者: hcyety
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 hcyety !
评论
  目录