前端八股之JavaScript

Web前端八股文JavaScript
2025-03-12 - 12:49
四季柚子
2025-03-12 - 12:49

数据类型

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

JavaScript共有八种数据类型,分别是undefined, null, boolean, string, symbol, number, objectbigint

其中symbolbigint是ES6新增的数据类型:

  • symbol代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题
  • bigint是一种数字类型的数据,它可以表示任意精度格式的整数,使用bigint可以安全地存储和操作大整数,即使这个数已经超出了number能够表示的安全整数范围

这些数据可以分为原始数据类型和引用数据类型:

  • 栈:原始数据类型(undefined, null, boolean, number, string)
  • 堆:引用数据类型(object, array, function)

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

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

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

  1. instanceof 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属性。

  1. constructor
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

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

  1. Object.prototype.toString.call() Object.prototype.toString.call()使用Object对象的原型方法toString来判断数据类型:
const 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的原型方法,而ArrayFunction等类型作为Object的实例,都重写toString方法。不同的对象类型调用toString方法时,根据原型链的知识,调用的是对应的重写之后的toString方法(Function类型返回内容为函数体的字符串,Array类型返回元素组成的字符串…),而不会去调用Object上原型toString方法(返回对象的具体类型),所以采用obj.toString()不能得到其对象类型,只能将obj转换为字符串类型;因此,在想要得到对象的具体类型时,应该调用Object原型上的toString方法。

3. 判断数组的方式有哪些

  1. 通过Object.prototype.toString.call()做判断
    Object.prototype.toString.call(obj) === '[object Array]'
  2. 通过原型链做判断
    obj.__proto__ === Array.prototype
    或者
    Object.getPrototypeOf(obj) === Array.prototype
  3. 通过ES6的Array.isArray()做判断 Array.isArray(obj)
  4. 通过instanceof做判断 obj instanceof Array

4. nullundefined区别

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

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

undefined在JavaScript中不是一个保留字,这意味着可以使用undefined来作为一个变量名,但是这样的做法是非常危险的,它会影响对undefined值的判断。我们可以通过一些方法获得安全的undefined值,比如说void 0

当对这两种类型使用typeof进行判断时,null类型化会返回“object”,这是一个历史遗留的问题。当使用双等号对两种类型的值进行比较时会返回true,使用三个等号时会返回false。

5. isNaNNumber.isNaN函数的区别?

函数isNaN接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回true,因此非数字值传入也可能返回true ,会影响NaN的判断。

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

6. == 操作符的强制类型转换规则?

  1. 首先会判断两者类型是否相同,相同的话就比较两者的大小
  2. 类型不相同的话,就会进行类型转换
  3. 会先判断是否在对比​null​undefined​,是的话就会返回​true
  4. 判断两者类型是否为​string​和​number​,是的话就会将字符串转换为​number
  5. 判断其中一方是否为​boolean​,是的话就会把​boolean​转为​number​再进行判断
  6. 判断其中一方是否为​object​且另一方为​string​、​number​或者​symbol​,是的话就会把​object​转为原始类型再进行判断

7. Object.is()与比较操作符 “===”、“==” 的区别?

使用双等号(==)进行相等判断时,如果两边的类型不一致,则会进行强制类型转化后再进行比较。

使用三等号(===)进行相等判断时,如果两边的类型不一致时,不会做强制类型准换,直接返回 false。

使用Object.is来进行相等判断时,一般情况下和三等号的判断相同,它处理了一些特殊的情况,比如-0和+0不再相等,两个NaN是相等的。

ES6

1. letconstvar的区别

  1. 块级作用域:块作用域由{ }包括,letconst具有块级作用域,var不存在块级作用域。块级作用域解决了ES5中的两个问题:
    • 内层变量可能覆盖外层变量
    • 用来计数的循环变量泄露为全局变量
  2. 变量提升var存在变量提升,letconst不存在变量提升,即在变量只能在声明之后使用,否在会报错
  3. 给全局添加属性:浏览器的全局对象是window,Node的全局对象是globalvar声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是letconst不会
  4. 重复声明var声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。constlet不允许重复声明变量
  5. 初始值设置:在变量声明时,varlet可以不用设置初始值。而const声明变量必须设置初始值
  6. 指针指向let创建的变量是可以更改指针指向(可以重新赋值)。但const声明的变量是不允许改变指针的指向

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

  1. 箭头函数比普通函数更加简洁
  2. 箭头函数没有自己的this只会在自己作用域的上一层继承this。所以箭头函数中this的指向在它在定义时已经确定了,之后不会改变
  3. call()apply()bind()等方法不能改变箭头函数中this的指向
  4. 箭头函数不能作为构造函数使用
  5. 箭头函数没有自己的argumentsprototype

3. 扩展运算符的作用及使用场景

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

const bar = { a: 1, b: 2 };
const baz = { ...bar }; // { a: 1, b: 2 }

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

const bar = {a: 1, b: 2};
const baz = {...bar, ...{a:2, b: 4}};  // {a: 2, b: 4}

扩展运算符对对象实例的拷贝属于浅拷贝

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

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

下面是数组的扩展运算符的应用:

  1. 将数组转换为参数序列
  2. 复制数组
  3. 合并数组
  4. 扩展运算符与解构赋值结合起来,用于生成数组.需要注意:如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错
  5. 将字符串转为真正的数组,相当于String.prototype.split('')
  6. 任何实现Iterator接口的对象,都可以用扩展运算符转为真正的数组

4. Proxy可以实现什么功能?

Proxy是ES6中新增的功能,它可以用来自定义对象中的操作

const p = new Proxy(target, handler)

下面来通过Proxy来实现一个数据响应式

function watch(obj, setBind, getLogger) {
    const 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)
}
const obj = { 
    a: 1 
}
const p = watch(
    obj,
    (value, property) => console.log(`监听到属性${property}改变为${value}`),
    (target, property) => console.log(`${property} = ${target[property]}`)
)
p.a = 2 // 监听到属性a改变为2
p.a // 'a' = 2

JavaScript基础

1. new操作符的实现原理

  1. 首先创建了一个新的空对象
  2. 设置原型,将对象的原型设置为函数的prototype对象。
  3. 让函数的this指向这个对象,执行构造函数的代码(为这个新对象添加属性)
  4. 判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象

手写代码见前端八股之手写代码篇

2. MapObject的区别

Map Object
意外的键 Map默认情况不包含任何键,只包含显式插入的键。 Object有一个原型, 原型链上的键名有可能和自己在对象上的设置的键名产生冲突。
键的类型 Map的键可以是任意值,包括函数、对象或任意基本类型。 Object的键必须是String或是Symbol
键的顺序 Map中的key是有序的。因此,当迭代的时候, Map对象以插入的顺序返回键值。 Object的键是无序的
Size Map的键值对个数可以轻易地通过size属性获取 Object的键值对个数只能手动计算
迭代 Mapiterable的,所以可以直接被迭代。 迭代Object需要以某种方式获取它的键然后才能迭代。
性能 在频繁增删键值对的场景下表现更好。 在频繁添加和删除键值对的场景下未作出优化。

3. MapWeakMap的区别

WeakMap对象也是一组键值对的集合,其中的键是弱引用的。其键必须是对象,原始数据类型不能作为key值,而值可以是任意的。

WeakMap的设计目的在于,有时想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。一旦不再需要这两个对象,就必须手动删除这个引用,否则垃圾回收机制就不会释放对象占用的内存。

WeakMap的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。

4. JavaScript有哪些内置对象

  1. 值属性,这些全局属性返回一个简单值,这些值没有自己的属性和方法
    例如InfinityNaNundefinednull字面量
  2. 函数属性,全局函数可以直接调用
    例如eval()parseFloat()parseInt()
  3. 基本对象,基本对象是定义或使用其他对象的基础。基本对象包括一般对象、函数对象和错误对象
    例如ObjectFunctionBooleanSymbolError
  4. 数字和日期对象,用来表示数字、日期和执行数学计算的对象
    例如NumberMathDate
  5. 字符串,用来表示和操作字符串的对象
    例如StringRegExp
  6. 可索引的集合对象,这些对象表示按照索引值来排序的数据集合,包括数组和类型数组,以及类数组结构的对象
    例如Array
  7. 使用键的集合对象,这些集合对象在存储数据时会使用到键,支持按照插入顺序来迭代元素
    例如MapSetWeakMapWeakSet
  8. 控制抽象对象
    例如PromiseGenerator
  9. 反射
    例如ReflectProxy
  10. WebAssembly
  11. 其它
    例如arguments

5. 数组有哪些原生方法?

  • 数组和字符串的转换方法:toString()toLocalString()join()其中 join()方法可以指定转换为字符串时的分隔符
  • 数组尾部操作的方法pop()push(),push()方法可以传入多个参数
  • 数组首部操作的方法shift()unshift()
  • 数组重排序的方法reverse()sort()sort()方法可以传入一个函数来进行比较,传入前后两个值,如果返回值为正数,则交换两个参数的位置。
  • 数组连接的方法concat(),返回的是拼接好的数组,不影响原数组
  • 数组截取办法slice(),用于截取数组中的一部分返回,不影响原数组
  • 数组插入方法splice(),影响原数组查找特定项的索引的方法
  • 数组查找索引方法indexOf()lastIndexOf()
  • 数组迭代方法every()some()filter()map()forEach()
  • 数组归并方法reduce()reduceRight()

6. 对AJAX的理解,实现一个AJAX请求

AJAX是Asynchronous JavaScript and XML的缩写,指的是通过JavaScript的异步通信,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。

创建AJAX请求的步骤:

  1. 创建一个XMLHttpRequest对象
  2. 在这个对象上使用open方法创建一个HTTP请求,open方法所需要的参数是请求的方法、请求的地址、是否异步和用户的认证信息
  3. 在发起请求前,可以为这个对象添加一些信息和监听函数。比如说可以通过 setRequestHeader方法来为请求添加头信息。
    还可以为这个对象添加一个状态监听函数。一个XMLHttpRequest对象一共有 5 个状态,当它的状态变化时会触发onreadystatechange事件,可以通过设置监听函数,来处理请求成功后的结果。
    当对象的readyState变为4的时候,代表服务器返回的数据接收完成,这个时候可以通过判断请求的状态,如果状态是2xx或者304的话则代表返回正常。这个时候就可以通过 response中的数据来对页面进行更新了
  4. 当对象的属性和监听函数设置完成后,最后调用send方法来向服务器发起请求,可以传入参数作为发送的数据体
const SERVER_URL = "/server";
const xhr = new XMLHttpRequest();
// 创建 Http 请求
xhr.open("GET", url, true);
// 设置状态监听函数
xhr.onreadystatechange = function() {
  if (this.readyState !== 4) return;
  // 当请求成功时
  if (this.status === 200) {
    handle(this.response);
  } else {
    console.error(this.statusText);
  }
};
// 设置请求失败时的监听函数
xhr.onerror = function() {
  console.error(this.statusText);
};
// 设置请求头信息
xhr.responseType = "json";
xhr.setRequestHeader("Accept", "application/json");
// 发送 Http 请求
xhr.send();

7. ES6模块与CommonJS模块有什么异同?

ES6 Module和CommonJS模块的区别:

  • CommonJS是对模块的浅拷⻉,ES6 Module是对模块的引⽤,即ES6 Module只存只读,不能改变其值,也就是指针指向不能变,类似const
  • import的接⼝是read-only,不能修改其变量的指针指向,但可以改变变量内部指针指向,可以对commonJS对重新赋值(改变指针指向),但是对ES6 Module赋值会编译报错
  • CommonJS是单个值导出,ES6 Module可以导出多个
  • CommonJS模块是运行时加载,ES6模块是编译时输出接口
  • CommonJs是动态语法可以写在判断里,ES6 Module静态语法只能写在顶层

8. 常见的DOM操作有哪些

  1. DOM节点获取
getElementById // 按照 id 查询
getElementsByTagName // 按照标签名查询
getElementsByClassName // 按照类名查询
querySelectorAll // 按照 css 选择器查询
querySelector // 按照 css 选择器查询

// 按照 id 查询
const imooc = document.getElementById('imooc') // 查询到 id 为 imooc 的元素
// 按照标签名查询
const pList = document.getElementsByTagName('p')  // 查询到标签为 p 的集合
// 按照类名查询
const moocList = document.getElementsByClassName('mooc') // 查询到类名为 mooc 的集合
// 按照 css 选择器查询
const pList = document.querySelectorAll('.mooc') // 查询到类名为 mooc 的集合
  1. DOM 节点的创建
<html>
  <head>
    <title>DEMO</title>
  </head>
  <body>
    <div id="container"> 
      <h1 id="title">我是标题</h1>
    </div>   
  </body>
  <script>
    const container = document.getElementById('container')
    const targetSpan = document.createElement('span')
    targetSpan.innerHTML = 'hello world'
    container.appendChild(targetSpan)
  </script>
</html>
  1. DOM 节点的删除
const container = document.getElementById('container')
const targetNode = document.getElementById('title')
container.removeChild(targetNode)

或者

const container = document.getElementById('container')
const targetNode = container.childNodes[1]
container.removeChild(targetNode)
  1. 修改 DOM 元素
    调换位置,可以考虑insertBefore或者appendChild

9. for...infor...of的区别

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

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

10. 如何使用for...of遍历对象

如果需要遍历的对象是类数组对象,用Array.from转成数组即可

const obj = {
    0:'one',
    1:'two',
    length: 2
};
obj = Array.from(obj);
for(const k of obj){
    console.log(k)
}

如果不是类数组对象,就给对象添加一个[Symbol.iterator]属性,并指向一个迭代器即可

const obj = {
    a:1,
    b:2,
    c:3
};

obj[Symbol.iterator] = function(){
    const keys = Object.keys(this);
    let count = 0;
    return {
        next(){
            if(count < keys.length){
                return {
                    value: obj[keys[count++]],
                    done:false
                };
            }else{
                return {
                    value:undefined,
                    done:true
                };
            }
        }
    }
};

for(const k of obj){
    console.log(k);
}

原型与原型链

1. 对原型、原型链的理解

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

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

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

2. 原型修改、重写

function Person(name) {
    this.name = name
}
// 修改原型
Person.prototype.getName = function() {}
var p = new Person('hello')
console.log(p.__proto__ === Person.prototype) // true
console.log(p.__proto__ === p.constructor.prototype) // true
// 重写原型
Person.prototype = {
    getName: function() {}
}
var p = new Person('hello')
console.log(p.__proto__ === Person.prototype)        // true
console.log(p.__proto__ === p.constructor.prototype) // false

可以看到重写原型的时候p的构造函数不是指向Person了,因为直接给Person的原型对象直接用对象赋值时,它的构造函数指向的了根构造函数Object,所以这时候p.constructor === Object,而不是p.constructor === Person。要想成立,就要用constructor指回来

Person.prototype = {
    getName: function() {}
}
var p = new Person('hello')
p.constructor = Person
console.log(p.__proto__ === Person.prototype)        // true
console.log(p.__proto__ === p.constructor.prototype) // true

3. 原型链指向

p.__proto__ === Person.prototype
Person.prototype.__proto__ === Object.prototype
p.constructor === Person
p.__proto__.constructor === Object
Object.prototype.__proto__ === null
Function.prototype.__proto__ === Object.prototype

4. 如何获得对象非原型链上的属性?

使用Object.prototype.hasOwnProperty()方法

function iterate(obj){
   const res=[];
   for(const key in obj){
        if(obj.hasOwnProperty(key))
           res.push(key+': '+obj[key]);
   }
   return res;
} 

使用Object.keys()方法,返回自身可枚举属性

function iterate(obj){
    return Object.keys(obj).map(key=>key+': '+obj[key]);
}

使用Relect.ownKeys()方法。和Object.prototype.hasOwnProperty()方法的区别在于,返回包括包括Symbol属性和不可枚举属性的数组。

function iterate(obj){
    return Reflect.ownKeys(obj).map(key=>key+': '+obj[key]);
}

执行上下文/作用域链/闭包

1. 对闭包的理解

闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。

闭包有两个常用的用途:

  • 闭包的第一个用途是使我们在函数外部能够访问到函数内部的变量。通过使用闭包,可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量
  • 闭包的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收

2. 对作用域、作用域链的理解

作用域

  1. 全局作用域
    • 最外层函数和最外层函数外面定义的变量拥有全局作用域
    • 所有未定义直接赋值的变量自动声明为全局作用域
    • 所有window对象的属性拥有全局作用域
    • 全局作用域有很大的弊端,过多的全局作用域变量会污染全局命名空间,容易引起命名冲突
  2. 函数作用域
    • 函数作用域声明在函数内部的变量,一般只有固定的代码片段可以访问到
    • 作用域是分层的,内层作用域可以访问外层作用域,反之不行
  3. 块级作用域
    • 使用ES6中新增的letconst指令可以声明块级作用域,块级作用域可以在函数中创建也可以在一个代码块中的创建(由​{ }​包裹的代码片段)
    • 在循环中比较适合绑定块级作用域,这样就可以把声明的计数器变量限制在循环内部

作用域链

在当前作用域中查找所需变量,但是该作用域没有这个变量,那这个变量就是自由变量。如果在自己作用域找不到该变量就去父级作用域查找,依次向上级作用域查找,直到访问到window对象就被终止,这一层层的关系就是作用域链。

作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,可以访问到外层环境的变量和函数

作用域链的本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。作用域链的前端始终都是当前执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象。

当查找一个变量时,如果当前执行环境中没有找到,可以沿着作用域链向后查找。

3. 对执行上下文的理解

执行上下文类型

  1. 全局执行上下文 任何不在函数内部的都是全局执行上下文,它首先会创建一个全局的window对象,并且设置this的值等于这个全局对象,一个程序中只有一个全局执行上下文
  2. 函数执行上下文 当一个函数被调用时,就会为该函数创建一个新的执行上下文,函数的上下文可以有任意多个
  3. eval函数执行上下文 执行在eval函数中的代码会有属于他自己的执行上下文,不过eval函数不常使用,不做介绍

执行上下文栈

JavaScript引擎使用执行上下文栈来管理执行上下文

当JavaScript执行代码时,首先遇到全局代码,会创建一个全局执行上下文并且压入执行栈中,每当遇到一个函数调用,就会为该函数创建一个新的执行上下文并压入栈顶,引擎会执行位于执行上下文栈顶的函数,当函数执行完成之后,执行上下文从栈中弹出,继续执行下一个上下文。当所有的代码都执行完毕之后,从栈中弹出全局执行上下文

创建执行上下文

创建执行上下文有两个阶段:创建阶段执行阶段

创建阶段

  1. this绑定
    • 在全局执行上下文中,this指向全局对象(window对象)
    • 在函数执行上下文中,this指向取决于函数如何调用。如果它被一个引用对象调用,那么this会被设置成那个对象,否则this的值被设置为全局对象或者undefined
  2. 创建词法环境组件
  3. 创建变量环境组件

执行阶段

此阶段会完成对变量的分配,最后执行完代码

简单来说执行上下文就是指:
在执行一点JS代码之前,需要先解析代码。解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为undefined,函数先声明好可使用。这一步执行完了,才开始正式的执行程序。
在一个函数执行之前,也会创建一个函数执行上下文环境,跟全局执行上下文类似,不过函数执行上下文会多出thisarguments和函数的参数。

this/call/apply/bind

1. 对this对象的理解

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

  1. 第一种是函数调用模式,当一个函数不是一个对象的属性时,直接作为函数来调用时,this指向全局对象
  2. 第二种是方法调用模式,如果一个函数作为一个对象的方法来调用时,this指向这个对象
  3. 第三种是构造器调用模式,如果一个函数用new调用时,函数执行前会新创建一个对象,this指向这个新创建的对象
  4. 第四种是**applycallbind调用模式**,这三个方法都可以显示的指定调用函数的 this指向。
    其中apply方法接收两个参数:一个是this绑定的对象,一个是参数数组。
    call方法接收的参数,第一个是 this 绑定的对象,后面的其余参数是传入函数执行的参数。也就是说,在使用call方法时,传递给函数的参数必须逐个列举出来。
    bind方法通过传入一个对象,返回一个this绑定了传入对象的新函数。这个函数的 this指向除了使用new时会被改变,其他情况下都不会改变 这四种方式,使用构造器调用模式的优先级最高,然后是apply、call和bind调用模式,然后是方法调用模式,然后是函数调用模式

2. call()apply()的区别?

它们的作用一模一样,区别仅在于传入参数的形式的不同

  • apply接受两个参数,第一个参数指定了函数体内this对象的指向,第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组,apply方法把这个集合中的元素作为参数传递给被调用的函数
  • call传入的参数数量不固定,跟apply相同的是,第一个参数也是代表函数体内的this指向,从第二个参数开始往后,每个参数被依次传入函数

3. 实现call、apply及bind函数

见前端八股之手写代码篇

异步编程

1. 异步编程的实现方式

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

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

2. setTimeoutPromiseasync/await的区别

setTimeout

console.log('script start') //1. 打印 script start
setTimeout(function(){
    console.log('settimeout')   // 4. 打印 settimeout
})  // 2. 调用 setTimeout 函数,并定义其完成后执行的回调函数
console.log('script end')   //3. 打印 script start
// 输出顺序:script start->script end->settimeout

Promise

Promise本身是同步的立即执行函数,当在executor中执行resolve或者reject的时候, 此时是异步操作,会先执行then/catch等,当主栈完成后,才会去调用resolve/reject中存放的方法执行,打印p的时候,是打印的返回结果,一个Promise实例

console.log('script start')
let promise1 = new Promise(function (resolve) {
    console.log('promise1')
    resolve()
    console.log('promise1 end')
}).then(function () {
    console.log('promise2')
})
setTimeout(function(){
    console.log('settimeout')
})
console.log('script end')
// 输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout

async/await

async function async1(){
   console.log('async1 start');
    await async2();
    console.log('async1 end')
}
async function async2(){
    console.log('async2')
}
console.log('script start');
async1();
console.log('script end')
// 输出顺序:script start->async1 start->async2->script end->async1 end

async 函数返回一个Promise对象,当函数执行的时候,一旦遇到await就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async函数体

3. 对Promise的理解

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

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

  1. Promise的实例有三个状态:
    • Pending(进行中)
    • Fulfilled(已完成)
    • Rejected(已拒绝)
  2. Promise的实例有两个过程:
    • pending -> fulfilled : Resolved(已完成)
    • pending -> rejected:Rejected(已拒绝)
  3. Promise的特点:
    • 对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态,​pending​fulfilled​rejected。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态,这也是Promise这个名字的由来——"承诺"
    • 一旦状态改变就不会再变,任何时候都可以得到这个结果。如果改变已经发生了,你再对promise对象添加回调函数,也会立即得到这个结果。这与事件(event)完全不同,事件的特点是:如果你错过了它,再去监听是得不到结果的
  4. Promise的缺点:
    • 无法取消Promise,一旦新建它就会立即执行,无法中途取消
    • 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部
    • 当处于pending状态时,无法得知目前进展到哪一个阶段

总的来说:
Promise对象是异步编程的一种解决方案。Promise是一个构造函数,接收一个函数作为参数,返回一个Promise实例。一个Promise实例有三种状态,分别是pending、resolved和rejected,分别代表了进行中、已成功和已失败。实例的状态只能由pending转变resolved或者rejected 状态,并且状态一经改变,就凝固了,无法再被改变了。

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

4. Promise的基本用法

1. 创建Promise对象

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。

const promise = new Promise(function(resolve, reject) {
  // ...
  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
})

一般情况下都会使用new Promise()来创建Promise对象,但是也可以使用Promise.resolvePromise.reject这两个方法:

  • Promise.resolve() Promise.resolve(value)的返回值也是一个Promise对象,可以对返回值进行.then调用。例如Promise.resolve(11).then((value) => {console.log(value)}),这段代码会打印11
  • Promise.reject() Promise.reject也是new Promise的快捷形式,也创建一个Promise对象。例如Promise.reject(new Error("TypeError"))

2. Promise方法

Promise有一些常用的方法:thencatchallracefinallyanyallSettled

  • then
    promise.then((value) => {
    
     }, 
     (error) => {
    
     })
    
    then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。其中第二个参数可以省略。
    then方法返回的是一个新的Promise实例(不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法
  • catch 该方法相当于then方法的第二个参数,指向reject的回调函数。不过catch方法还有一个作用,就是在执行resolve回调函数时,如果出现错误,抛出异常,不会停止运行,而是进入catch方法中
  • all all方法可以完成并行任务, 它接收一个数组,数组的每一项都是一个Promise对象。当数组中所有Promise的状态都达到resolved的时候,all方法的状态就会变成resolved,如果有一个状态变成了rejected,那么all方法的状态就会变成 rejected。
  • race race方法和all一样,接受的参数是一个每项都是Promise的数组,但是与all不同的是,当最先执行完的事件执行完之后,就直接返回该Promise对象的值。如果第一个Promise对象状态变成resolved,那自身的状态变成了resolved;反之第一个Promise变成rejected,那自身状态就会变成rejected。
  • finally finally方法用于指定不管Promise对象最后状态如何,都会执行的操作。该方法是 ES2018引入标准的
  • any Promise.any()静态方法将一个Promise可迭代对象作为输入,并返回一个Promise。当输入的任何一个Promise兑现时,这个返回的Promise将会兑现,并返回第一个兑现的值。当所有输入Promise被拒绝(包括传递了空的可迭代对象)时,它会以一个包含拒绝原因数组的AggregateError拒绝。
  • allSettled Promise.allSettled()静态方法将一个Promise可迭代对象作为输入,并返回一个单独的Promise。当所有输入的Promise都已敲定时(包括resolved和reject),返回的Promise将被兑现,并带有描述每个Promise结果的对象数组。

5. 对async/await的理解

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

await在等待什么呢?一般来说,都认为await是在等待一个async函数完成。不过按语法说明,await 等待的是一个表达式,这个表达式的计算结果是Promise对象或者其它值(换句话说,就是没有特殊限定)。

因为async函数返回一个Promise对象,所以await可以用于等待一个async函数的返回值——这也可以说是await在等async函数,但要清楚,它等的实际是一个返回值。注意到await不仅仅用于等Promise对象,它可以等任意表达式的结果,所以,await后面实际是可以接普通函数调用或者直接量的。

await表达式的运算结果取决于它等的是什么。

  • 如果它等到的不是一个Promise对象,那await表达式的运算结果就是它等到的东西。
  • 如果它等到的是一个Promise对象,await就忙起来了,它会阻塞后面的代码,等着Promise对象resolve,然后得到resolve的值,作为await表达式的运算结果。

6. async/await对比Promise的优势

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

垃圾回收与内存泄漏

1. 浏览器的垃圾回收机制

垃圾回收的概念

垃圾回收:JavaScript代码运行时,需要分配内存空间来储存变量和值。当变量不在参与运行时,就需要系统收回被占用的内存空间,这就是垃圾回收。

回收机制:

  • Javascript 具有自动垃圾回收机制,会定期对那些不再使用的变量、对象所占用的内存进行释放,原理就是找到不再使用的变量,然后释放掉其占用的内存。
  • JavaScript中存在两种变量:局部变量和全局变量。全局变量的生命周期会持续要页面卸载;而局部变量声明在函数中,它的生命周期从函数执行开始,直到函数执行结束,在这个过程中,局部变量会在堆或栈中存储它们的值,当函数执行结束后,这些局部变量不再被使用,它们所占有的空间就会被释放。
  • 不过,当局部变量被外部函数使用时,其中一种情况就是闭包,在函数执行结束后,函数外部的变量依然指向函数内部的局部变量,此时局部变量依然在被使用,所以不会回收。

垃圾回收的方式

浏览器通常使用的垃圾回收方法有两种:标记清除,引用计数。

  1. 标记清除
    • 标记清除是浏览器常见的垃圾回收方式,当变量进入执行环境时,就标记这个变量“进入环境”,被标记为“进入环境”的变量是不能被回收的,因为他们正在被使用。当变量离开环境时,就会被标记为“离开环境”,被标记为“离开环境”的变量会被内存释放。
    • 垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记。然后,它会去掉环境中的变量以及被环境中的变量引用的标记。而在此之后再被加上标记的变量将被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后。垃圾收集器完成内存清除工作,销毁那些带标记的值,并回收他们所占用的内存空间。
  2. 引用计数
  • 另外一种垃圾回收机制就是引用计数,这个用的相对较少。引用计数就是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型赋值给该变量时,则这个值的引用次数就是1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数就减1。当这个引用次数变为0时,说明这个变量已经没有价值,因此,在在机回收期下次再运行时,这个变量所占有的内存空间就会被释放出来。
  • 这种方法会引起循环引用的问题:例如:obj1obj2通过属性进行相互引用,两个对象的引用次数都是2。当使用循环计数时,由于函数执行完后,两个对象都离开作用域,函数执行结束,obj1obj2还将会继续存在,因此它们的引用次数永远不会是0,就会引起循环引用。这种情况下,就要手动释放变量占用的内存。

减少垃圾回收

虽然浏览器可以进行垃圾自动回收,但是当代码比较复杂时,垃圾回收所带来的代价比较大,所以应该尽量减少垃圾回收。

  • 对数组进行优化:在清空一个数组时,最简单的方法就是给其赋值为[],但是与此同时会创建一个新的空对象,可以将数组的长度设置为0,以此来达到清空数组的目的。
  • object进行优化:对象尽量复用,对于不再使用的对象,就将其设置为null,尽快被回收。
  • 对函数进行优化:在循环中的函数表达式,如果可以复用,尽量放在函数的外面。

2. 哪些情况会导致内存泄漏

以下四种情况会造成内存的泄漏:

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