js 基础知识专栏

270 阅读51分钟
  1. JavaScript 有哪些数据类型,它们的区别?

    7 种: number \ string \ undefinded \ null \ symbol \ boolean \ bigint

    区别:这些数据类型是 JavaScript 中常见的数据类型,它们的区别如下:

    1. Number:表示数字,可以是整数或浮点数。例如:42、3.14。
    2. String:表示字符串,由一系列字符组成。例如:"hello"、"42"。
    3. Undefined:表示变量未定义或未赋值。例如:var x; console.log(x); // 输出 undefined。
    4. Null:表示空值或无值。例如:var y = null; console.log(y); // 输出 null。
    5. Symbol:表示独一无二的标识符,主要用于对象属性的键名。例如:var symbol1 = Symbol('6'); var obj = {[symbol1]: "value"}; console.log(obj[symbol1]); // 输出 "value"。
    6. boolean: 表示数据是真值还是假值
    7. BigInt:表示任意精度的整数,可以用于处理大整数。例如:var bigInt = 123456789012345678901234567890n; console.log(bigInt + 1n); // 输出 123456789012345678901234567891n。
  2. 数据类型检测的方式有哪些

    • typeof: 主要用来检测基本数据类型, 无法准确区分数组、对象和 null 类型。
    • instanceOf: 沿着作用域链查找, 判断对象是否在该作用域链上
    • constructor:每个对象都有一个 constructor 指向自己原型对象, 但是 constructor 容易被改写, 导致检测的结果并不可靠,也无法检测 null 和 undefined
    • Object.prototype.toString.call(obj): 会获取到对象内部的 class 属性,返回的数据格式是 [Object ..] 的字符串
  3. 判断数组的方式有哪些

    • Array.isArray()
    • Object.prototype.toString.call(obj)
    • instanceOf: 有缺陷,在多窗口的时候, 内置的 Array 并不是一个
    • Array.prototype.isPrototypeOf(): 用来判断是否在指定对象的原型链上
    • Object.getPrototypeOf(): 这个方法可以获取对象的隐式原型
  4. null 和 undefined 区别

    • null: 代表空值或无值,
    • undefined: 表示初始化变量后未赋值或未定义变量
  5. typeof null 的结果是什么,为什么?

    结果: object

    为什么: javascript 在设计初, 对象有一个标识符为 0,即 0 代表是对象, null 的二进制表示全为 0, 他的类型标签也为 0 和 Object 一样 ,所以 null 的类型就为 object, 这是一个历史遗留问题

  6. intanceOf 操作符的实现原理及实现

    原理: 沿着原型链查找, 如果对象原型链上有这个构造函数, 返回 true

    实现:

    	function myInstanceOf (obj, Constructor) {
    		let origin = obj.__proto__
    		while (origin) {
    			if (origin === Constructor.prototype) {
    					return true
    			}
    			origin = origin.__proto__
    		}
    		return fasle
    	}
    
  7. 为什么 0.1+0.2 !== 0.3,如何让其相等

    原因:计算采取二进制存储数据, 而 0.1 和 0.2 在二进制表示下是无限循环小数, 这导致在计算时存在精度缺失

    如何让他们相等: 可以使用 toFixed(2) 方法

  8. 如何获取安全的 undefined 值?

    • 初始化时不赋值: let a;
    • 使用 Object.defineProperty(global, 'sec_undefined', { get: function () { return undefined; }, enumerable: false, configurable: true })
  9. typeof NaN 的结果是什么?

    number

  10. isNaN 和 Number.isNaN 函数的区别?

    1. isNaN: 在 ES5 之前就存在, 用来检测一个数据是否是 NaN, 在内部会进行类型转换, 如果转换后是 NaN, 则返回 true, 否则返回 false

    2. Number.isNaN: 在 ES6 中新增的 Number 的静态函数, 用来检测一个数据是否是 NaN,不会进类型转换, 会严格检查参数是不是 NaN, 不会进行任何数据类型的转换

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

== 操作符只比较值是否相同, 会进行类型转换

1、 首先会判断数据类型是否相同, 如果相同会直接进行比较, 不会进行类型转换, 如果不同会进行类型转换

2、 会先判断是否在对比 null 和 undefined, 是的话就会返回 true

3、 判断两者类型是否为 string 和 number, 是的话将字符串转换为 number

4、 判断其中一方是否为 boolean, 是的话会将 boolean 转为 number 进行判断

5、 判断其中一方是否为 Object, 如果双方都是 object, 会判断两者是否为同一个对象, 是的话返回 true, 否则返回 false, 如果另一方是为 string、number 或者 symbol, 是的话会把 object 转为原始类型再进行判断,会先调用对象的 valueOf 方法,如果不行, 会调用 toString 方法, 再按照前面的规则进行比较

  1. 其他值到字符串的转换规则?

    • 1、 null 和 undefined 会转换为字符串 'null' 和 'undefined'
    • 2、 布尔值会转换为字符串 'true' 和 'false'
    • 3、 对象会转换为 "[object Object]" undefined 会转换为 "undefined", null 会转换为 "null"
    • 4、Symbol 类型的值直接转换,但是只允许显式强制类型转换,使用隐式强制类型转换会产生错误。
    • 5、对普通对象来说,除非自行定义 toString() 方法,否则会调用 toString()(Object.prototype.toString())来返回内部属性 [[Class]] 的值,如"[object Object]"。如果对象有自己的 toString() 方法,字符串化时就会调用该方法并使用其返回值。
    • 6、数组会将内部的元素用 ,拼接返回一个数组 注意: null 和 undefined 没有 toString() 方法, 但是可以使用 String 进行强转
  2. 其他值到数字值的转换规则?

    • Undefined 类型的值转换为 NaN。
    • Null 类型的值转换为 0。
    • Boolean 类型的值,true 转换为 1,false 转换为 0。
    • String 类型的值转换如同使用 Number() 函数进行转换,如果包含非数字值则转换为 NaN,空字符串为 0。
    • Symbol 类型的值不能转换为数字,会报错。
    • 对象(包括数组)会首先被转换为相应的基本类型值, 先调用 valueOf() 方法, 如果不行再调用 toString() 方法,如果返回的是非数字的基本类型值,则再遵循以上规则将其强制转换为数字。
  3. 其他值到布尔类型的值的转换规则?

    以下这些是假值: • undefined • null • false • +0、-0 和 NaN • ""

    假值的布尔强制类型转换结果为 false。从逻辑上说,假值列表以外的都应该是真值。

  4. || 和 && 操作符的返回值?

    • ||: 当至少一个操作数为真时,返回真;否则返回假。
    • &&: 当所有操作数都为真时,返回真;否则返回假。
  5. Object.is()与比较操作符"==="、“==”的区别?

    • Object.is: 确定两个值是否是相同的值, 不会进行类型转换, 返回一个布尔值, 他和 === 区别在于对 有符号 0 和 NaN 的判断, Object.is(+0, -0) // false , 但是 === 对他的判断是 true, 此外对于 NaN 的判断, NaN 和 NaN 是相等的,但是 === 不相等, Object.is(NaN, NaN) // true

    ==: 如果两边的值不相同, 会进行类型转换

    ===: 会判断值和类型是否相同,如果类型不同直接返回 false

  6. 什么是 JavaScript 中的包装类型?

    包装类型, 将基本数据类型通过构造函数的方式包装成对象, 称为包装类型, 具备一些特有的属性和方法。 在 js 中 Number 、 String、 Boolean 是基本包装类型

    注意:js 在内部其实对有些数据类型也会包装成对象,例如: string,我们可以获取他的长度属性,也可以添加属性, 但是只有在代码执行的那一行会生效

    const str = 'Hello'
    str.name = 'ok' // 会生效, 但是只有在这一行会生效
    console.log(str.name) // undefined
    
  7. JavaScript 中如何进行隐式类型转换

    如果是 + 号, 数字和字符串之间会拼接, 不会将字符串转换成数字,

    减,乘,除,大于,小于转换一样, 会将字符串转换成数字, 如果不能转换会是一个 NaN

原理:

当两个不同类型的数据进行抽象比较(==)时,JS 会将它们先转换成统一数据类型,低层级的数据类型向高层级的数据类型转换,直到“==”左右两边数据类型相同,然后比较数据的值是否相同。

可以简单理解为这个转换,小的会向上转换 number > string = boolean > object // 对象会先转换成原始类型

```
	数字遇上字符串:字符串→数字
	1 == '1'    //true

	数字遇上布尔:布尔→数字
	0 == false    //true

	数字遇上对象:对象→数字
	0 == [0]    //true

	字符串遇上布尔:字符串→数字,布尔→数字
	"" == false    //true
	"0" == false    //true    "0"0false0

	字符串遇上对象:对象→字符串
	[1, 2, 3] == '1,2,3'    //true

	布尔遇上对象:
	[0] == false    //true    [0] → "0" → 0, false → 0
	[] == false    //true     [] → "" → 0, false → 0
	![] == false    //true

	undefined == false    //false
	null == false        //false
	undefined == null    //true

```

注意:![] == false 为 true 是因为 ! 会将 [] 直接转换成 Boolean,在 JS 中,将其他数据类型转换成 Boolean 时, 只有空字符串("")、0、null、undefined、NaN 会转换成 false,其他数据都会转换成 true,所以空数组([])会被转换成 true,![]为 false,!![]为 true undefined 和 null 与任何有意义的值比较返回的都是 false,但是 null 与 undefined 之间互相比较返回的是 true

引用类型和基本类型相对复杂一些,先要把引用类型转成基本类型,再按上述的方法比较。

通过 ToPrimitive 将值转换为原始值

js 引擎内部的抽象操作 ToPrimitive 有着这样的签名:

ToPrimitive(input, PreferredType?)

input 是输入的值,即要转换的对象,必选;

preferedType 是期望转换的基本类型,他可以是字符串,也可以是数字。选填,默认为 number;

他只是一个转换标志,转化后的结果并不一定是这个参数值的类型,但是转换结果一定是一个原始值(或者报错)。

对于 Date 求原始值比较特殊,PreferredType 是 String,其他 Object 对象均为 Number。

既然要隐式转换,就应该有一套转换规则,才能追踪最终转换成了什么

隐式转换中主要涉及到三种转换:

    1、将值转为原始值,ToPrimitive()。

    2、将值转为数字,ToNumber()。

    3、将值转为字符串,ToString()。

PreferredType 转换策略
如果 PreferredType 被标记为 Number,则会进行下面的操作流程来转换输入的值。
如果输入值已经是一个原始值,那么直接返回该值即可
如果输入值为一个对象,则调用该对象的 valueOf() 方法,若 valueOf() 返回的是原始值,则返回原始值
否则调用该对象的 toString() 方法,若 toString() 返回的是原始值,则返回原始值
否则抛出 TypeError 异常

    如果PreferredType被标记为String,则会进行下面的操作流程来转换输入的值。
    如果输入值已经是一个原始值,那么直接返回该值即可
    否则调用该对象的 toString() 方法,若 toString() 返回的是原始值,则返回原始值
    如果输入值为一个对象,则调用该对象的 valueOf() 方法,若 valueOf() 返回的是原始值,则返回原始值
    否则抛出 TypeError 异常

注意点:

PreferredType 的值会按照这样的规则来自动设置:
该对象为 Date 类型,则 PreferredType 被设置为 String
否则,PreferredType 被设置为 Numbe

19. +操作符什么时候用于字符串的拼接?

当两边都是字符串时,会拼接;一边是字符串,另一边是数字,会拼接

```
	console.log('Hello, ' + 'world!'); // 'Hello, world!'
	console.log('The answer is ' + 42); // 'The answer is 42'
```

其中一个操作数是对象:当 + 操作符的其中一个操作数是对象时,JavaScript 会将该对象转换为字符串,然后进行字符串拼接, 有隐式转换规则, 两个对象会被转换成字符串,然后进行拼接。

```
	console.log('The object is ' + { foo: 'bar' }); // 'The object is [object Object]'
	const a = {}
	const b = {}
	console.log(a + b) // [object Object][object Object]
```

其中一个操作数是 null 或 undefined, 另一个操作数是字符串:当 + 操作符的其中一个操作数是 null 或 undefined 时,JavaScript 会将其转换为字符串 'null''undefined',然后进行字符串拼接。例如:

```
	console.log('The value is ' + null); // 'The value is null'
	console.log('The value is ' + undefined); // 'The value is undefined'
```

20. 为什么会有 BigInt 的提案?

  • 处理大整数:

在某些情况下,我们需要处理超出 JavaScript Number 类型范围的整数 2^53-1(约等于 9.007.199.254.740.991),例如在加密算法、大整数计算或科学计算中。这时就需要一种能够精确表示大整数的数据类型。

  • 避免精度丢失:(超过他的最大数值时)

JavaScript 中的 Number 类型使用双精度浮点数表示,因此会存在精度限制和精度丢失的问题。而 BigInt 类型可以精确表示任意大的整数,避免了这些问题。

总结:BigInt 提案的出现是为了解决 JavaScript 中处理大整数时的精度限制和精度丢失问题。BigInt 类型可以精确表示任意大的整数,使得 JavaScript 能够更好地应对涉及大整数运算的场景,从而提高了语言的实用性和适用性。

```
	// 使用普通的 Number 类型进行计算
	let maxSafeInteger = 9007199254740991
	console.log(maxSafeInteger + 10) // 9007199254741000,精度丢失

	// 使用 BigInt 类型进行计算
	let bigIntNum = 9007199254740991n
	console.log(bigIntNum + 10n) // 9007199254741001n,得到正确的结果
```

注意: 不能将普通的数值进行计算, 否则会导致错误, 要确保计算双方都为 bigint 类型数据

  1. object.assign 和扩展运算法是深拷贝还是浅栲贝以及两者区别

浅拷贝: 浅拷贝只拷贝对象的顶层属性,嵌套的对象或数组仍然是引用类型,指向原来的对象或数组。换句话说,浅拷贝只拷贝一层,嵌套的对象或数组不会被拷贝。

  • Object.assign() 静态方法将一个或者多个源对象中所有可枚举的自有属性复制到目标对象,并返回修改后的目标对象,是浅拷贝。
  • 扩展运算法: 浅拷贝

区别:

  • 语法不同

  • object.assign() 支持拷贝 Symbol 属性和不可枚举属性, 扩展运算符不支持。

  • object.assign() 返回的是原有目标对象, 扩展运算符返回的是一个新的对象。

  • 扩展运算符可以拷贝对象也可以拷贝数组。object.assign() 可以用于拷贝对象, 也可以拷贝数组, 但是他会将数组当成对象, 根据他的标识属性判断覆盖

注意: JSON.parse(JSON.stringify(obj)) 方法会丢失函数和 undefined 值。

  1. 如何判断一个对象是空对象
  • lodash 中存在 isEmpty() 方法,判断对象是否为空。
  • 使用 Object.keys(), 返回给定对象的自有属性可枚举的属性键组成的数组,如果数组 length 为 0,则该对象为空对象。
  • 使用 Object.getOwnPropertyNames(), 返回给定对象的所有自有属性键组成的数组(包括不可枚举的, 但不包括 symbol),如果数组 length 为 0,则该对象为空对象。
  • JSON.stringify(obj) === '{}' 则为空
  1. let、const、 var 的区别

    • let、 const 是 es2015 新添加的, 会产生额外的块级作用域, 在 javascript 编译时会记录它的位置, 并将其值记录为 unInitialize, 如果在声明 let 或 const 之前使用该值会报 ReferenceError。
    • const 表示常量, 不能修改, 但是对于数组和对象一类的值, 可以修改其内部属性, 但不能改变它指向的地址。
    • var 不会产生块级作用域,在编译时会把初始值设置为 undefined (变量提升的本质)
  2. const 对象的属性可以修改吗

    可以修改, 但是不能修改其指向的地址

  3. 如果 new 一个箭头函数的会怎么样

    会报错 TypeError: arrowFunction is not a constructor

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

    • 箭头函数比普通函数简洁
    • 箭头函数没有自己的 this
    • 箭头函数没有自己的参数 arguments
    • 箭头函数没有 prototype
    • 箭头函数继承的 this 方向永远不会改变
    • apply、call、bind 不能改变箭头函数的 this 指向
    • 箭头函数不能作为构造函数使用
  5. 箭头函数的 this 指向哪里?

指向箭头函数声明的那个空间,一句话: 调用者指向谁, 则指向谁。 mp.weixin.qq.com/s/1iw1MBfit…

从原理上来说:

函数声明的时候就确定了 this 的指向, 函数在声明的时候会绑定一个 [[scope]] 属性, 这个属性指向声明的那个上下文,当函数被调用时会产生一个函数执行上下文, 这个上下文中有一个 outter 变量用来记录外部环境, 这个 outter 与 [[scope]] 指向相同的上下文,对于箭头函数的 this 指向他声明的这个空间。

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

扩展运算符(Spread Operator)是 ES6 引入的一种新的语法糖,它的作用是将一个数组或对象展开成一个新的数组或对象。

使用场景:

  • 数组合并: 使用扩展运算符可以方便地合并多个数组。
  • 对象合并: 使用扩展运算符可以方便地合并多个对象。
  • 函数参数: 使用扩展运算符可以方便地将一个数组作为函数参数传递 ---》 这种叫做 剩余参数
  • 解构赋值: 使用扩展运算符可以方便地进行解构赋值。
  • 解构赋值
  const arr = [1, 2, 3, 4, 5];
  const [a, b, ...rest] = arr;
  console.log(a); // 1
  console.log(b); // 2
  console.log(rest); // [3, 4, 5]

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

ProxyES6 新增的,它可以拦截对对象的操作,包括读取、写入、删除、遍历等。它可以用来实现各种代理模式,如数据验证、数据缓存、数据代理等。

Proxy 是一种强大的元编程技术,可以实现各种功能,包括:

*   属性拦截: 可以拦截对象的属性读取和设置操作,例如,可以实现数据绑定、属性验证等功能。
*   方法拦截: 可以拦截对象的方法调用,例如,可以实现日志记录、权限控制等功能。
*   代理对象: 可以创建一个代理对象,代理另一个对象的行为,例如,可以实现懒加载、缓存等功能。
*   数据观察: 可以观察对象的数据变化,例如,可以实现数据绑定、状态管理等功能。

<!---->

    	// 创建代理对象
    	const target = {
    		foo: 'bar'
    	};

    	const handler = {
    		get(target, prop) {
    			console.log(`Getting ${prop}`);
    			return target[prop];
    		},
    		set(target, prop, value) {
    			console.log(`Setting ${prop} to ${value}`);
    			target[prop] = value;
    		}
    	};

    	const proxy = new Proxy(target, handler);
    	console.log(proxy.foo); // Getting foo, bar
    	proxy.foo = 'baz'; // Setting foo to baz

30. 对象与数组的解构的理解

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

在内部,JavaScript 会执行以下步骤:

  1. 创建一个新的对象 temp,并将 obj 的属性复制到 temp 中:temp = { a: 1, b: 2, c: 3 }
  2. 将 temp 的属性赋给变量 a、b 和 c:a = temp.a, b = temp.b, c = temp.c

解构的实现

解构的实现是基于 JavaScript 的内部实现机制。具体来说,解构是通过以下几个步骤实现的:

  • 语法分析: JavaScript 解析器分析解构语句,并确定需要解构的数组或对象。
  • 创建临时对象: JavaScript 创建一个临时对象,用于存储解构后的值。
  • 复制属性: JavaScript 将原始数组或对象的属性复制到临时对象中, 查找机制 ---> 通过原型链查找机制获取对象的所有属性名。
  • 赋值: JavaScript 将临时对象的属性赋给变量。

解构是一种语法糖,允许你从数组或对象中提取值并将它们赋给变量。它的原理是基于 JavaScript 的内部实现机制,通过创建临时对象、复制属性和赋值来实现。

  1. 如何提取高度嵌套的对象里的指定属性?
  • 使用 lodash 中的 get 方法

        const _ = require('lodash');
        const nestedObject = {
            a: {
                b: {
                    c: {
                        d: 'value'
                    }
                }
            }
        };
    
        const value = _.get(nestedObject, 'a.b.c.d');
        console.log(value); // Output: "value"
        ```
    
    
  • ?. 使用可选链操作符, 遇到 undefined 或 null 时会返回 undefined。

  • 使用嵌套解构赋值 const nestedObject = { a: { b: { c: { d: 'value' } } } };

    const { a: { b: { c } } } = nestedObject;
    
  1. 对 rest 参数的理解

剩余参数(Rest Parameter)是 ES6 引入的一种语法糖,允许我们将一个不定数量的参数表示为一个数组

  1. ES6 中模板语法与字符串处理

模板字符串(Template String)是 ES6 引入的一种语法糖,它允许在字符串中嵌入表达式,使得字符串的拼接更加方便和可读。

  • 模板字符串可以包含表达式,通过来嵌入表达式。这个{}来嵌入表达式。 这个 {} 中可以放任何表达式,包括函数调用、对象属性访问等。
  • 可以多行展示

其实模板字符串底层是一个函数,只是返回的结果是一个字符串, 这些处理均在后台处理

  1. new 操作符的实现原理

new 操作符的实现步骤是:

  1. 创建一个新对象

  2. 会给对象添加一个 [[prototype]] 的隐式属性 ---> 指向函数对象的 prototype

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

  4. 执行构造函数中的代码

  5. 并将 this 的引用返回, 如果返回的是基本数据类型, 那就返回 this

function myNew(fn, ...args) {
    let obj = Object.create(null); // 创建一个空对象
    obj.__proto__ = fn.prototype; // 绑定一隐式原型执向构造函数的显示原型 prototype
    fn.apply(obj, args); // 执行函数
    return obj; // 返回对象
 }

  1. map 和 Object 的区别
  • 键的类型

    • Map:键可以是任意类型,包括对象、函数等。
    • Object:键只能是字符串或 Symbol 类型。如果使用数字作为键,它会被转换为字符串。
  • 构造方式

    • Map:通过调用内置的 Map 构造函数创建。
    • Object:通过字面量、调用 Object 构造函数或使用 Object.create()方法创建。
  • 原型链

    • Map:不会从原型中继承多余的键值对。
    • Object:会从原型(如 Object.prototype)中继承多余的键值对。
  • 键值对顺序

    • Map:保留键值对的插入顺序。
    • Object:不保证键值对的顺序。非负整数键会按从小到大排序,然后是其他类型的键。
  • 访问接口

    • Map:提供 get(key)、set(key, value)、has(key)等方法来访问和操作键值对。
    • Object:通常使用点运算符或方括号运算符访问属性。
  • 迭代器

    • Map:是可迭代对象,可以通过 for...of 循环或 forEach 方法遍历键值对。
    • Object:默认不可迭代,需要使用 Object.keys()、Object.values()、Object.entries()等方法获取迭代器。
  1. map 和 weakMap 的区别
  • map 是引用类型,weakMap 是弱引用类型,weakMap 的键只能是引用类型, 其键只能是 Object 或继承至 Object,而 map 的键可以是任何类型的值。
  • weakMap 不能被迭代, 因为他的键随时可能被回收, 因此, 他不具有 forEach 这些方法
  1. JavaScript 有哪些内置对象
  • Global
  • Math
  • Date
  • String
  • RegExp
  • Array
  • Object
  • Function
  • Number -Boolean
  • Error
  • Promise‌
  • Map‌
  • Set‌
  • WeakMap‌
  • WeakSet‌
  • ArrayBuffer‌
  • TypedArray‌
  • DataView‌
  • WebAssembly
  1. 对 JSON 的理解
  • JSON(JavaScript Object Notation)是一种轻量级的数据交换格式, 它以文本形式存储和表示数据, 采用键值对的结构来存储数据,并使用逗号进行分隔
  • JSON 的键必须是字符串,并且需要包裹在双引号中。值可以是字符串、数字、布尔值、数组、对象或 null。数组由方括号包围,对象由花括号包围,键值对之间用冒号分隔
  • 简洁性和跨语言的特性,JSON 被广泛应用于 Web 开发中的客户端和服务器之间的数据传输,以及配置文件、日志记录和 API 通信等领域。几乎所有编程语言都有解析 JSON 的库,而在 JavaScript 中,可以直接使用 JSON,因为 JavaScript 内置了 JSON 的解析功能。
  1. JavaScript 脚本延迟加载的方式有哪些?

defer 和 async 都只能外部加载的 js 文件上

  • defer: 推迟脚本执行 会在渲染主线程解析完后, 执行 js 代码, 加上这个属性后, js 的下载不会阻塞主线程, 浏览器会异步下载, 渲染主线程结束后,再去执行 js 代码
  • async: 异步执行

js 的下载不会阻塞主线程, 浏览器会异步下载, 但是当他下载完后, 会立即执行 js 代码这个有个问题, 他的下载完成时机无法把控, 如果多个 js 文件, 如果多个 js 文件之间有依赖关系, 会出现问题

  • 动态创建 DOM 节点:

    可以动态的创建一个 script 标签, 可以控制 js 的加载时机

  • 使用 setTimeOut 延时加载

  • 将脚本放在文档底部:

    放在底部, 浏览器会先解析完 html, 再去加载 js, 渲染页面使用 jQuery 的 getScript 方法:如果项目中使用了 jQuery,可以利用 jQuery 提供的$.getScript()方法来异步加载脚本文件。这个方法接受一个 URL 作为参数,当脚本加载并执行成功后,会调用一个回调函数。

  • 使用模块加载器:

    对于使用模块打包工具(如 Webpack)的项目,可以将 JavaScript 代码分割成多个模块,并在需要时动态加载这些模块。这种方式不仅可以实现延迟加载,还能优化应用性能和用户体验

  1. JavaScript 类数组对象的定义?

    类数组对象(Array-like object)是指那些具有 length 属性和按索引存储数据的对象,但它们并不继承自 Array.prototype。

常见的类数组对象包括函数的 arguments 对象、DOM 方法返回的 NodeList 等。

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

MDN :developer.mozilla.org/zh-CN/docs/…

csdn 总结: blog.csdn.net/qq_43223007…

  1. Unicode、UTF-8、UTF-16、UTF-32 的区别?

(1)Unicode

Unicode由统一码联盟开发,于1991年首次发布,旨在为全球所有字符提供统一的编码标准 不直接指定字节长度,而是通过不同的UTF编码形式(如UTF-8、UTF-16、UTF-32)来表示字符

  • 不涉及字节序问题,因为它只是字符集规范
  • 设计目标是为了兼容现有的字符编码方案,因此具有广泛的兼容性
  • 作为字符集标准,不涉及具体编码效率

(2)UTF-8

UTF-8是ASCII的扩展,最初由Ken Thompson和Rob Pike于1992年设计,用于兼容ASCII并支持多语言文本

  • 变长编码,使用1到4个字节表示一个字符,根据字符不同而变化
  • 与字节序无关,因为它是字节顺序无关的编码
  • 后兼容ASCII,因此广泛用于互联网和文件存储
  • 对于ASCII字符高效,但对于非ASCII字符可能不如UTF-16高效

(3)UTF-16

UTF-16基于UCS-2,最早由Unicode标准定义,主要用于表示基本多文种平面的字符

  • 变长编码,使用2或4个字节表示一个字符,基本平面字符使用2个字节,增补平面字符使用4个字节
  • 有字节序问题,分为大端序(Big Endian)和小端序(Little Endian),通常使用BOM(Byte Order Mark)标识
  • 不兼容ASCII,但广泛用于Windows系统和Java内部编码

(4)UTF-32

UTF-32是UCS-4的子集,使用32位定长码元表示Unicode字符,是最简单的一种编码方式

  • 定长编码,每个字符固定使用4个字节
  • 也有字节序问题,分为大端序(Big Endian)和小端序(Little Endian),同样使用BOM标识
  • 虽然简单,但由于占用空间大,实际应用较少,逐渐被淘汰
  • 由于定长编码,处理速度最快,但空间效率最低

(5)总结

Unicode作为一种字符集标准,为全球文字提供了统一的编码方案。而UTF-8、UTF-16和UTF-32则是Unicode的具体实现方式,各有优缺点。UTF-8以其向后兼容ASCII和广泛的应用场景成为互联网上最常用的编码方式;UTF-16在处理双字节字符时效率较高,广泛应用于Windows系统和Java内部编码;UTF-32虽然处理速度快,但由于空间效率低下,逐渐被淘汰。

  1. 常见的位运算符有哪些?其计算规则是什么? blog.csdn.net/boyaaboy/ar…
  • 按位与运算符(&)

    按位与运算符(&)是一种用于对二进制位进行操作的运算符,它逐位比较两个数的二进制表示,只有当两个相应的二进制位都为1时,结果位才为1,否则为0。应用场景:按位与运算符在编程中有多种应用场景,包括但不限于:

    • 清零:如果想将一个单元清零,只要与一个各位都为零的数值相与,结果为零。
    • 屏蔽特定位:通过与适当的掩码进行按位与运算,可以屏蔽掉某些位。例如,0000 1111 & 0000 0011的结果为0000 0011,前两位被屏蔽成为0。
    • 判断奇偶:通过判断最未位是0还是1来决定奇偶,可以用 if ((a & 1) == 0) 代替 if (a % 2 == 0) 来判断 a 是否为偶数。
  • 按位或运算符(|)

    按位或运算符(|)是一种用于执行按位或操作的运算符,它对两个数的二进制表示进行逐位比较,只要有一个为1,结果位就为1。

    • 按位或运算符只能用于整型和字符型数据,不能用于其他数据类型(如浮点型)。
    • 在进行按位或运算时,如果操作数长度不同,系统会自动对齐右端,然后进行运算
  • 异或运算符(^)

    异或运算符(^)是一种用于执行按位异或操作的运算符,它对两个数的二进制表示进行逐位比较,只有两个位不同时为1,结果位才为1。

  • 取反运算符() 取反运算符()是一种用于执行按位取反操作的运算符,它对一个数的二进制表示进行逐位取反,即将1变为0,0变为1。

  • 左移运算符(<<)

    左移运算符(<<)是一种用于将一个数的二进制表示向左移动指定的位数的运算符,它会将左边的位数向左移动,右边的位数用0填充。在数学运算中, 对于处理数的幂次方非常有用, 相当于乘以 2

    • 在使用左移运算符时,需要注意不要超出数据类型的范围。如果左移的位数过多,可能会导致溢出或未定义的行为。
    • 左移运算符不能直接用于浮点数,只能用于整数类型。如果需要对浮点数进行位移操作,可以先将其转换为整数类型。
  • 右移运算符(>>)

    右移运算符(>>)是一种用于将一个数的二进制表示向右移动指定的位数的运算符,它会将右边的位数向右移动,左边的位数用符号位(即0或1)填充。在数学运算中, 对于处理整数的除法运算非常有用, 相当于除以 2

    • 对于右移运算符, 向右移动时,如果是无符号位的数,符号位会被填充为0。如果是有符号位的数,符号位会被填充为符号位本身。
    • 在使用右移运算符时,需要注意符号位的处理。不同编译器和平台对符号位的处理方式可能不同,因此在移植代码时需要特别注意。
    • 右移运算符不能直接用于浮点数,只能用于整数类型。如果需要对浮点数进行位移操作,可以先将其转换为整数类型。
  • 原码、补码、反码 在计算机系统中,原码、补码和反码是三种不同的二进制编码方式,主要用于表示有符号整数。这些编码方式帮助计算机处理正数和负数的运算。以下是对这三种编码方式的具体介绍:

    1. 原码:原码是最简单的编码方式,它直接将一个数值转换为二进制形式。对于正数,原码与普通的二进制表示相同;对于负数,最高位(符号位)设为1,其余位表示该数的绝对值。例如,+5的原码是0101,-5的原码是1101。原码的优点在于其直观性,但其缺点是在进行加减运算时需要判断符号位,增加了运算的复杂性。

    2. 反码:反码用于简化二进制的减法运算,特别是负数的处理。正数的反码与其原码相同,而负数的反码则是将其原码除符号位外的所有位取反(0变1,1变0)。例如,+5的反码是0101,-5的反码是1010。反码的优点是在进行减法运算时可以直接相加,但缺点是存在+0和-0两个零值。

    3. 补码:补码是目前最常用的二进制编码方式,它解决了原码和反码存在的问题。正数的补码与其原码相同,而负数的补码是其反码加1。例如,+5的补码是0101,-5的补码是1011。补码的优点是可以统一处理正数和负数的加减运算,且只有一种零表示。

    总的来说,原码、补码和反码各有优缺点,它们在不同的应用场景中发挥着重要作用。了解这些编码方式有助于更好地理解计算机内部的数据处理机制。

  1. 为什么函数的 arguments 参数是类数组而不是数组? 如何遍历类数组? ‌ 函数的 arguments 参数是类数组而不是数组的主要原因是为了保持与早期 JavaScript 版本的兼容性。在JavaScript的早期版本中,arguments对象被设计为只能使用数值索引访问参数,而不具备数组的方法和属性。这使得它类似于数组,但又不是真正的数组‌‌

    类数组可以转换成真正的数组后进行遍历类数组内部实现了可迭代协议,因此可以使用 for...of 循环来遍历类数组对象。

遍历类数组:

  • 使用 Array.prototype.slice.call(arguments) 方法可以将 arguments 对象转化为数组。
  • slice 方法返回一个新的数组, 不会改变原来的数组
  • Array.from() 方法可以将类数组对象转化为数组。 接收一个具有 length 属性的对象 或者 可迭代对象,返回一个数组。
  • 使用展开运算符 ... 运算符可以将类数组对象转化为数组。 内部实现了可迭代协议, 因此可以遍历类数组对象。
  • 使用 for...of 循环遍历类数组对象。
  • 使用 for...in 循环遍历类数组对象,但需要注意的是,for...in 循环会遍历对象的所有可枚举属性,包括继承的属性。因此,对于类数组对象,最好使用 for...of 或者其他更具体的循环方式来遍历。
  1. 什么是 DOM 和 BOM? .
  • DOM: 文档对象模型(Document Object Model) 是用于HTML和XML文档的编程接口。它允许 JavaScript 和其它语言动态地访问和更新文档的内容、结构和样式。DOM 将文档表示为一个由节点、属性和对象组成的结构化文档。
  • BOM: 浏览器对象模型(Browser Object Model) 是由浏览器提供的一组对象,用于描述和操作浏览器窗口。例如,window 对象提供了对浏览器窗口的所有操作,包括访问和修改文档、控制浏览器导航等。BOM 提供了与浏览器交互的功能,如获取当前 URL、设置标题、控制窗口大小等。
  1. 对类数组对象的理解,如何转化为数组

    类数组是指具有 length 和索引的属性的对象,但不一定是数组。例如 arguments 对象、HTMLCollection 和 NodeList 等。要将类数组对象转化为数组,可以使用 Array.from() 方法、Array.prototype.slice.call() 方法或者扩展运算符 (...) 等。

  2. escape、encodeURI、encodeURIComponent

以下是对 escape、encodeURI 和 encodeURIComponent 的详细介绍:

  • escape

    • 定义:escape 是一个旧的函数,用于对字符串进行编码,使得它能被安全地用作统一资源标识符(URI)。它会将字符串中的每个字符转换成相应的百分号编码(%xx 形式),除了一些保留字符会被保留原样。
    • 特点:escape 不会对 ASCII 字母数字字符进行编码,对特殊字符进行编码,对非 ASCII 字符进行编码。然而,它已经被废弃,不推荐使用。
  • encodeURI

    • 定义:encodeURI 函数用于对整个 URI 进行编码。它会把所有非保留字符转换成它们的百分号编码形式,保留字符不会被编码。
    • 特点:encodeURI 适用于编码完整的 URI,它可以确保整个 URI 被安全地传输。它会保留 URI 中的保留字符,如 :/?#[]@!$&'()*+,;=。
  • encodeURIComponent

    • 定义:encodeURIComponent 与 encodeURI 类似,但它的用途更具体——用于对 URI 组件进行编码。它会编码所有非保留字符和保留字符,只留下一小部分字符不编码。
    • 特点:encodeURIComponent 适用于编码 URI 中的某个组成部分,比如查询字符串或者路径的一部分。它会对除了 -._~ 之外的所有保留字符进行编码。

    综上所述,escape 函数由于其不一致和不安全的行为,不应再使用。encodeURI 和 encodeURIComponent 提供了更安全、更可靠的编码方法,应根据具体场景选择使用。

    同时他还有一对 decodeURI 和 decodeURIComponent 函数,用于解码 encodeURI 和 encodeURIComponent 编码的字符串。

  1. URI 和 URL 的区别

URI和URL是互联网资源标识和定位的两种重要概念,它们之间既有联系也有区别。以下是具体分析:

  • URI: 统一资源标识符

  • URL: 统一资源定位器

    1. 定义

      • URI(Uniform Resource Identifier):URI是一个广泛的概念,它用于唯一地标识一个或多个资源。URI可以是绝对的(如URL),也可以是相对的(如相对路径)。
      • URL(Uniform Resource Locator):URL是URI的一个子集,专门用于指定资源的地址。它不仅标识资源,还提供了访问该资源的方法。
    2. 结构

      • URI:一般由三部分组成:资源的标志符、主机名和相对URI。
      • URL:通常由协议、主机名、端口号、路径、参数和片段组成。
    3. 用途

      • URI:主要用于编程中作为资源的唯一标识,不直接用于访问资源。
      • URL:主要用于用户通过浏览器直接访问资源。

    综上所述,URI和URL在互联网资源标识和定位中扮演着不同的角色。URI作为一个更广泛的概念,提供了资源的标识,而URL则是URI的一种特定形式,专门用于指定资源的地址和访问方法。

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

image.png Ajax 不是一种新技术,也不是一种新语言,而是一个编程概念; HTML 和 CSS 可以组合使用来标记和设置信息样式, javascript 可以修改网页动态显示(用于与用户进行交互), nodejs 内置的 XMLHttpRequest 则是在网页上执行 ajax, 允许网页将内容加载到屏幕上无需刷新页面, 这使得 web 页面可以只更新页面的局部, 而不影响用户的操作。

- 理解:ajax 是通过内置的 XMLHttpRequest 对象发送请求, 理解这个函数即可

- 区别一般 http 请求与 ajax 请求
    - ajax 请求是一种特别的 http 请求
    - 对服务器端来说, 没有任何区别, 区别在浏览器端
    - 浏览器端发请求: 只有 XHR 或 fetch 发出的才是 ajax 请求, 其它所有的都是非 ajax 请求

- 浏览器端接收到响应
    - 一般请求: 浏览器一般会直接显示响应体数据, 也就是我们常说的刷新/跳转页面
    - ajax 请求: 浏览器不会对界面进行任何更新操作, 只是调用监视的回调函数并传入响应相关数据
   function req() {
       const xhr = new XMLHttpRequest()
       xhr.open('get', 'http://localhost:8080')  // 参数1:请求方式, 参数2:请求地址, 参数3:是否异步(默认为 true)表示是否异步执行操作, 参数4: user 默认为 null, 用于身份验证, 参数5: password 默认为 null, 用于身份验证
       xhr.send()
       // 当 readyState 状态发生改变时调用的处理函数 onreadystatechange
       /**
           readyState 值
               0 - 服务器连接已建立,未调用 open()。
               1 - 服务器连接已建立,调用 open() 但不调用 send()。
               2 - 请求已接收, 调用 send(),并且标题和状态可用。
               3 - 请求处理中。responseText 保存数据。
               4 - 请求已完成,且响应已就绪。
        */
       xhr.onreadystatechange = function() {
         if (xhr.readyState === 4) {
           if (xhr.status >= 200 && xhr.status < 300) {
             console.log(xhr.response);
             res.innerHTML = xhr.response
           }
         }
       }
     }

51. JavaScript 为什么要进行变量提升,它导致了什么

变量提升是JavaScript中的一种特性,它允许在代码执行前将变量和函数的声明提前到它们所在作用域的顶部。这一机制的存在有其特定的理由,但也带来了一些问题。

### 为什么要进行变量提升

1.  **提高性能**    *   在JavaScript代码执行之前,会进行语法检查和预编译。这个操作只会执行一次,通过变量提升,可以在代码执行前预先为变量分配栈空间,从而避免了每次执行代码时都重新解析一遍变量和函数,提高了代码的执行效率。

2.  **容错性更好**    *   变量提升可以在一定程度上提高JavaScript代码的容错性。例如,即使开发者先使用了一个变量再声明它,由于变量提升的存在,这段代码仍然可以正常执行。这在一定程度上减少了因疏忽导致的代码错误。

### 变量提升导致的问题

1.  **逻辑混乱**    *   变量提升可能导致代码的逻辑变得难以理解和维护。例如,当一个变量在某个作用域内被声明并初始化,但由于变量提升,它的声明可能被提前到作用域的顶部,而初始化则留在原地,这可能导致代码的执行顺序与直观理解不符。

2.  **覆盖问题**    *   变量提升可能导致内部变量覆盖外部变量。例如,在一个函数内部声明了一个与外部作用域同名的变量,由于变量提升,这个内部变量的声明将被提前到函数作用域的顶部,从而可能覆盖外部作用域中的同名变量。

3.  **函数声明与表达式混淆**    *   需要注意的是,只有函数声明会被提升,而函数表达式不会被提升。这可能导致开发者在使用时产生混淆,特别是在同时使用函数声明和函数表达式的情况下。

综上所述,变量提升是JavaScript中的一个重要特性,它在提高性能和容错性方面发挥了积极作用。然而,开发者也需要注意变量提升带来的潜在问题,如逻辑混乱、覆盖问题等,并在编写代码时遵循良好的编码规范,以避免这些问题的发生。

52. 什么是尾调用,使用尾调用有什么好处?

尾调用(Tail Call)是一种特殊的函数调用方式,当一个函数的最后一个操作是调用另一个函数时,这个调用就被称为尾调用。在尾调用中,当前函数执行完毕后,直接将控制权交给被调用的函数,而不需要保留当前函数的执行上下文。

### 尾调用的定义

尾调用的形式如下:

```javascript
function outerFunction() {
    // 一些操作
    return innerFunction(); // 这是一个尾调用
}
```

### 尾调用的好处

1.  **优化栈空间***   尾调用优化(Tail Call Optimization, TCO)是一种编译器或解释器技术,用于优化尾调用。通过这种优化,可以避免在每次递归调用时创建新的栈帧,从而节省内存和提高性能。

*   例如,在递归计算阶乘时,如果不使用尾调用优化,每次递归调用都会增加一个新的栈帧,导致栈溢出。而使用尾调用优化后,可以重用同一个栈帧,避免栈溢出问题。

2.  **提高性能***   由于减少了栈帧的创建和销毁,尾调用优化可以提高程序的运行效率。特别是在处理深度递归时,性能提升尤为明显。

3.  **简化代码逻辑***   尾调用使得递归函数更加简洁和易读,因为不需要显式地管理栈帧。

### 示例

#### 非尾调用的递归函数

```javascript
function factorial(n) {
    if (n === 0) {
        return 1;
    } else {
        return n * factorial(n - 1); // 这是一个非尾调用
    }
}
```

#### 尾调用优化的递归函数

为了实现尾调用优化,可以使用辅助参数来保存中间结果:

```javascript
function factorial(n, acc = 1) {
    if (n === 0) {
        return acc;
    } else {
        return factorial(n - 1, n * acc); // 这是一个尾调用
    }
}
```

在这个例子中,`factorial` 函数的最后一个操作是调用自身,并且没有额外的操作需要完成,因此这是一个尾调用。现代 JavaScript 引擎(如 V8 引擎)已经实现了尾调用优化,但并不是所有环境都支持。

### 注意事项

1.  **兼容性***   并非所有的 JavaScript 引擎都支持尾调用优化。在使用之前,请确保目标环境支持该特性。

*   在某些情况下,即使支持尾调用优化,也可能因为其他原因(如调试模式)而被禁用。

2.  **递归深度***   尽管尾调用优化可以减少栈帧的使用,但对于非常深的递归,仍然可能导致栈溢出。在这种情况下,可以考虑使用迭代方法或其他优化策略。

3.  **可读性***   过度依赖尾调用可能会使代码变得难以理解和维护。在设计递归算法时,应权衡性能和可读性。

总结来说,尾调用优化是一种有效的技术,可以在特定情况下显著提高递归函数的性能。然而,它也有其局限性和需要注意的地方。在实际开发中,应根据具体需求和环境选择合适的优化策略。

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

在了解他们的异同之前, 我们先了解他的历史:

  • 1、 CommonJS规范: 诞生于服务器端 js 的早期, 尤其是在 Node.js 出现之前, 在那个时候 js 主要用于浏览器, 没有模块化的概念, 这导致所有的 js 代码和库都通过 <script /> 标签引入, 并且都在一个全局作用域中, 这种形式代码组织混乱, 容易造成命名冲突等。 随着 js 被用于复杂的服务器端开发, 需要一种方式来组织和封装代码, 以便于管理和重用, 所以 CommonJS 规范被提出, 目的是为 js 创建一个模块生态系统

  • 2、 ESM 规范: 随着前端开发的日益复杂化和模块化, 以及 JS 标准不断的发展, 社区感到需要内置在语言层面的模块系统, 这套系统适用于服务器端和浏览器端

区别:

  • 1、导出方式: CommonJs 通过 exports 和 module.exports 来暴露模块, 而 ESM 模块则通过 import 和 export 和 default export 来暴露模块。

  • 2、加载方式:

    CommonJs 是运行时加载模块的,且是同步加载,不支持异步加载, ESM 则是静态加载模块的,支持异步加载, 在编译时输出接口。这导致:CommonJs 允许一些动态编程技巧, 比如给予条件的导入模块, 或在任意位置导入模块, 甚至在函数内部。 ESM 是在编译时静态分析并处理模块依赖的, 意思是他会在 js 引擎处理 ESM 之前, 会先解析 import 和 export 语句, 构建出模块之间的依赖关系, 由于 ESM 的静态性质, 所有的 import 和 export 语句必须位于模块的顶层作用域, 不能被条件语句包围, 也不能动态生成, 这可以使工具能在打包阶段进行摇树优化, 移除未使用的代码, 因为他们可以准确地知道哪些导出被导入并使用了,如果强行要使用, 可以使用动态导入模式, 返回的是一个 promise

import('./myModule.js')
      .then((module) => {
           module.default();  // 调用默认导出的函数
       });
  • 3、异步加载:CommonJs 通常不支持异步加载和动态导入, 而 ESM 是原生支持的
  • 4、执行环境:CommonJs 主要用于 Node.js 环境, ESM 可以跨环境使用在浏览器环境和 Node.js 环境都可以使用
  • 5、树摇优化: CJS 不能进行, ESM 支持
  • 6、this 指向: CJS 中顶层的 this 指向这个模块本身, 而 ESM 中顶层 this 指向 Undefined
  • 7、值修改: CJS 可以进行值修改, 而 ESM 不可以(可读的)
  1. 常见的 DOM 操作有哪些

    1. DOM 节点的获取

      • document.getElementById()
      • document.querySelector() // 获取匹配的第一个元素
      • document.querySelectorAll() // 获取查询到所有元素,返回的是一个伪数组
      • document.getElementsByClassName() // 获取所有的类名, 返回一个伪数组
      • document.getElementsByTagName() // 获取所有的标签名,返回一个伪数组
    2. DOM 节点的创建

      • document.createElement() // 创建一个元素节点
      • document.createTextNode() // 创建文本节点
    3. DOM 节点的删除

      • parentDom.removeChild(childNode) // 注意一定要是一个父节点进行删除
    4. 添加 DOM 元素 (3 种方式)

      • parentNode.appendChild(node) // 在某个节点后面进行添加结点
      • parentNode.insertBefore(node, childNode) // 将某个元素放在指定元素前面
      • parentNode.replaceChild(oldNode, newNode) // 用新的节点替换旧的节点
    5. 设置 DOM 元素的属性

      1. 通过 classList 修改类名
        • parentNode.classList.add('className') // 添加类名
        • parentNode.classList.remove('className') // 删除类名
        • parentNode.classList.toggle('className') // 如果存在就删除, 如果不存在就添加
      2. 通过 setAttribute 修改属性
        • parentNode.setAttribute('attrName', 'attrValue') // 设置属性
        • parentNode.removeAttribute('attrName') // 移除设置的属性
        • parentNode.getAttribute('attrName') // 获取我们设置的属性
    6. 克隆节点 const cloneNode = node.cloneNode(true) // 深拷贝, 拷贝所有属性, 拷贝所有子节点 const cloneNode = node.cloneNode(false) // 浅拷贝, 只拷贝节点本身, 不拷贝属性, 不拷贝子节点, 只拷贝标签

  2. use strict 是什么意思?使用它区别是什么?

"use strict"是JavaScript中的一种指令,用于启用严格模式(strict mode),它被放置在JavaScript代码的顶部(通常是在函数体或脚本的第一行)。使用严格模式后,JavaScript代码会在执行时遵循更严格的语法规则,从而帮助开发者避免一些常见的错误,并提高代码的可靠性和性能。

一、主要区别

  1. 变量声明

    • 非严格模式下,可以隐式地创建全局变量,即如果一个变量没有使用var、let或const关键字声明,它会被自动添加到全局对象中。
    • 严格模式下,所有变量必须先声明后使用,否则会抛出ReferenceError。
  2. 删除操作

    • 非严格模式下,可以使用 delete 操作符删除变量、函数和函数参数。
    • 严格模式下,不允许使用 delete 操作符删除这些元素,否则会抛出错误。
  3. 保留字

    • 非严格模式下,某些JavaScript保留字(如with)可以用作变量名。

    • 严格模式下,不能使用保留字作为变量名。

    1. this指向
    • 非严格模式下,函数中的this值可能会被自动转换为全局对象或undefined。

    • 严格模式下,函数中的this值保持其原始值,不会自动转换。

    1. eval函数
    • 非严格模式下,eval函数的作用域与周围的作用域一样。

    • 严格模式下,eval函数拥有独立的作用域,不会污染外部环境。

    1. 八进制字面量
    • 非严格模式下,八进制字面量(如0777)是允许的。

    • 严格模式下,必须使用0o或0O前缀来表示八进制数,否则会抛出错误。

    1. 重复属性定义
    • 非严格模式下,对象的属性可以被重复定义,后面的属性会覆盖前面的属性。
    • 严格模式下,无法对只读属性赋值,否则会抛出错误。

二、优点

  1. 减少错误:通过强制要求变量声明和禁止使用未声明的变量,减少了因变量提升(hoisting)导致的错误。

  2. 提高安全性:限制了对eval和arguments对象的使用,防止了潜在的安全问题。

  3. 提升性能:严格模式有助于JavaScript引擎进行更多的优化,从而提高代码的运行速度。

  4. 增强可维护性:通过消除一些不合理的语法和行为,使代码更加规范和易于维护。

三、缺点

  • 改变语义:严格模式改变了一些JavaScript的默认行为,可能会导致依赖这些行为的旧代码在严格模式下出现问题。

  • 兼容性问题:虽然现代浏览器都支持严格模式,但在非常旧的浏览器中可能会出现兼容性问题。

  1. 强类型语言和弱类型语言的区别

    强类型语言和弱类型语言是编程中常见的分类方式,在类型检查、隐式转换、以及安全性方面存在一些区别

    1. 类型检查
      • 强类型语言,在编译时进行严格类型检查, 确认所有变量和表达式的类型正确
      • 弱类型语言,在运行时进行类型检查, 允许更灵活的类型转换
    2. 隐式转换
      • 强类型语言,不允许隐式转换, 必须通过显示转换来改变数据类型
      • 弱类型语言,允许隐式转换,不同类型的数据可以直接参与运算(有隐式转换规则)
    3. 安全性
      • 强类型语言,在编译时进行类型检查,可以提早发现问题,避免一些错误, 同时强制执行类型检查的机制会让代码更安全,减少运行时的问题
      • 弱类型语言,灵活性高,在运行时进行类型检查,不能提早发现问题, 需要开发者注意
    4. 开发效率
      • 强类型语言, 开发效率低,需要编写类型声明, 开发时间稍长, 有助于提早发现问题
      • 弱类型语言, 开发灵活,不需要显示类型定义, 效率高, 但是容易出错
    5. 性能
      • 强类型语言, 编译时运行优化, 性能更高
      • 弱类型语言, 解释执行, 运行速度慢, 特别是在类型转换频繁的时候
  2. 解释性语言和编译型语言的区别

解释性语言和编译型语言的区别在执行方式、运行效率、跨平台方面存在一些区别

    1. 执行方式 解释性语言: 逐行执行, 每行执行完再执行下一行 编译型语言: 先将代码编译成机器码,生成可执行文件,然后再运行
    1. 运行效率 解释性语言: 运行效率低,每次运行都需要重新逐行进行解释 编译型语言: 运行效率高,程序只需要一次编译成机器码, 后序将不再进行转换
    1. 跨平台 解释性语言: 只要有解释器,就可以运行,跨平台能力较好 编译型语言: 编译后的机器码, 需要对应的环境和操作系统, 跨平台能力差, 通常需要重新编译
    1. 安全性 解释性语言: 源代码可以随意查看, 安全性差 编译型语言: 源代码会经过编译, 安全性好
    1. 开发效率 解释性语言: 开发效率高, 无需编译, 修改完代码后可以立即执行 编译型语言: 开发效率低, 需要编译, 修改完代码需要重新编译
  1. for..in 和 for...of 的区别

    • for in 方法:

      for...in 语句迭代一个对象的所有可枚举字符串属性(除 Symbol 以外),包括继承的可枚举属性。以及原型身上的所有可枚举属性。

      他的 key 值是对象中的属性名, 而不是属性值。

    • for of 方法:

      for...of 语句执行一个循环,该循环处理来自可迭代对象的值序列。可迭代对象包括内置对象的实例,例如 Array、String、TypedArray、Map、Set、NodeList(以及其他 DOM 集合),还包括 arguments 对象、由生成器函数生成的生成器,以及用户定义的可迭代对象。

      他的第一个值是属性值,而不是属性名。

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

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

obj[Symbol.iterator] = function* (){
    const keys = Object.getOwnPropertyNames(this)
    for (let i = 0; i < keys.length; i++) {
        yield this[keys[i]]
    }
    
}
 for (let value of obj) { 
   console.log(value);
}

  1. ajax、 axios、 fetch 的区别.
  • ajax 是一个用于在浏览器和服务器之间传输数据的技术,它使用 XMLHttpRequest 对象来发送请求,并接收响应。语法:详见上一条
  • fetch 是现代浏览器中发送网络请求的一个 API, 他基于 Promise 提供了一种强大和灵活的方式来处理 Http 请求, 他的返回值是一个 Promise 对象, 他可以替代 XMLHttpRequest, 并结合 async/await 使用, 更加简单易用。语法:
fetch(resource)
fetch(resource, options)
// option 的参数

ptions 参数可以是一个对象,用于设置请求的详细信息,包括但不限于:

method: 请求方法,如 GET , POST, PUT, DELETE 等。
headers: 请求头部,用于设置 Content-Type 或其他自定义头部。
body: 请求体,通常用于 POST 或 PUT 请求。
mode: 请求模式,如 cors, no-cors, same-origin。
credentials: 是否发送 cookies,如 include, omit, same-origin。
cache: 缓存模式,如 default, no-store, reload 等。
  • axios Axios 是一个基于 promise 网络请求库,作用于node.js 和浏览器中。 它是 isomorphic 的(即同一套代码可以运行在浏览器和node.js中)。在服务端它使用原生 node.js http 模块, 而在客户端 (浏览端) 则使用 XMLHttpRequests, 支持 Promise 的 API,并且可以 拦截请求和响应等, 他是专门用于做 http 请求的。 语法:
// 查看官网
https://www.axios-http.cn/docs/intro
  1. 基本定义

    • Ajax:Ajax即“Asynchronous JavaScript And XML”,是一种创建交互式网页应用的网页开发技术。它的核心是通过JavaScript在客户端与服务器进行异步通信,从而实现页面局部刷新而无需重新加载整个页面。
    • axios:axios是一个基于Promise的HTTP库,可以在浏览器和node.js中使用。它对浏览器端的XMLHttpRequests和node.js的http模块进行了封装,支持拦截请求和响应、转换数据、取消请求等功能。
    • fetch:Fetch是一个现代的原生JavaScript API,用于替代传统的XMLHttpRequest对象。它返回一个Promise对象,支持链式调用和async/await语法,使得异步代码更加简洁和易读。
  2. 兼容性

    • Ajax:广泛兼容,包括较旧版本的浏览器,如IE9+。
    • axios:需要引入第三方库,因此不适用于所有环境,特别是不支持ES6的旧浏览器环境。
    • fetch:原生支持现代浏览器,但不支持IE(需polyfill),兼容性相对较差。
  3. 默认设置

    • Ajax:默认携带Cookie,支持进度监控、错误处理和同步/异步配置。
    • axios:默认携带Cookie,支持拦截器、取消请求和超时设置。
    • fetch:默认不会携带Cookie,需要手动配置,且不支持进度监控和请求取消。
  4. 使用场景

    • Ajax:适用于需要广泛兼容性的传统项目或小型项目。
    • axios:适合现代框架(如Vue、React)的项目,尤其是需要频繁处理拦截器和高级配置的业务场景。
    • fetch:适合现代项目中轻量化和直接使用浏览器原生能力的场景。

总的来说,Ajax因其成熟性和广泛的浏览器兼容性,适用于传统项目;axios以其丰富的功能和简洁的语法,成为现代框架开发中的首选工具;而fetch则以其原生支持和简洁的API,适合现代项目中简单的网络请求。开发者应根据项目需求和技术栈选择合适的工具,以实现高效、可靠的HTTP请求处理。

  1. 数组的遍历方法有哪些

包括但不限于这些, 每种方法都有自己的使用场景

  • forEach
  • map
  • for of
  • for in
  • for 循环
  • while
  • reduce
  • filter
  • some
  • every
  1. forEach 和 map 方法有什么区别
  • forEach 没有返回值, 但是允许你修改原数组, 始终返回的是 undefined , 遍历速度慢于 map, 无法使用 break 和 continue 语句来跳出循环, 不能在后面接链式操作
  • map 有返回值, 返回一个新的数组, 也能修改原数组(但是不建议这么使用) 可以在后面接链式操作, 遍历速度比 forEach 快, 同样不能使用 break 和 continue 语句来跳出循环,
  1. addEventListener()方法的参数和使用

    元素添加事件的句柄, 有三个参数, 第一个参数是事件名,第二个参数是事件处理函数,第三个参数是个 boolean 值, 代表事件在那个阶段被执行, 默认是 false (在冒泡阶段执行), 为 true 则是在捕获阶段执行。

  2. 对原型、原型链的理解,原型修改、重写

  • 原型:

    原型就是每个对象都拥有一个属性,包含了原型链上的属性和方法, 原型指向的对象被称为 原型对象或 对象原型, 原型可以分为 隐私原型和显示原型

  • 什么是隐式原型:

    每个对象身上都有一个 [[proto]] 属性 开发者可以使用 proto 去获取, 但这并不是官方的, 官方提供了 Object.getPrototypeOf() 方法用于获取对象的原型

  • 什么是显示原型:

    每个构造函数身上都有一个 prototype 属性, 这个属性指向的就是对象的原型对象

  • 什么是对象原型:

    通过 Object.create(obj) 创建出来的对象, obj 就是创建出来对象的对象原型

  • 什么是原型对象: prototype 所指向的对象

    例子:

        const obj = {}  const a = Object.create(obj)  ,这个 obj 对象就是 a 的 原型对象
    

    原型修改、重写:

    修改: (修改原型对象的属性)

    在对象的原型对象添加方法, 原型对象也是一个对象, 所以在原型对象上添加方法, 就是在修改原型对象, 也就修改了原型链上的方法

// 定义构造函数
            function Person(name) {
                this.name = name;
            }

            // 向原型添加新方法
            Person.prototype.sayHello = function() {
                console.log('Hello, my name is ' + this.name);
            };

            // 创建实例并调用新方法
            const alice = new Person('Alice');
            alice.sayHello(); // 输出: Hello, my name is Alice

        重写:(继承时, 重写父类方法)
            在继承中, 父类拥有一个方法, 子类继承后并不能满足自己的需求, 需要修改这个方法, 这种叫做重写

            // 定义基类构造函数
            function Animal(name) {
                this.name = name;
            }

            // 定义基类方法
            Animal.prototype.speak = function() {
                console.log(this.name + ' makes a noise.');
            };

            // 定义子类构造函数
            function Dog(name) {
                Animal.call(this, name); // 调用父类构造函数
            }

            // 创建新的原型对象,并设置其原型为 Animal 的实例
            Dog.prototype = Object.create(Animal.prototype);
            Dog.prototype.constructor = Dog; // 修正构造函数引用

            // 重写子类方法
            Dog.prototype.speak = function() {
                console.log(this.name + ' barks.');
            };

            // 创建实例并调用方法
            const dog = new Dog('Rex');
            dog.speak(); // 输出: Rex barks.
  1. 原型链指向

    了解原型链, 我们先考虑原型链的作用:

    js 并不是一门面向对象的语言, 但是为了实现面向对象编程,同时满足继承等功能, 引入了原型链的概念,实现给类添加属性, 能够实现 js 的继承, 通过原型链, 对象可以继承原型对象身上所有共享的方法和属性, 实现了继承

    原型链本质就是: 对象的隐式原型和构造函数的显示原型的链式指向, 构造函数的显示原型指向原型链的终点, 终点是 null, 对象的隐式原型指向他的构造函数的显示原型, 以此类推

  2. 原型链的终点是什么?如何打印出原型链的终点? 如何获得对象非原型链上的属性?

    终点是 null

        console.log(Object.prototype.__proto__) // null
    

    如何获取对象非原型链上的属性:

    Object 原型身上有一个方法, Object.getOwnPropertyNames, 他可以获取到对象身上的所有自由属性包括不可枚举的数据, 但是不能获取到原型链上的属性

  3. 对闭包的理解

    闭包是指那些有权访问和操作外部自由变量的函数, 闭包是指有权访问另一个函数作用域中的变量的函数。

    -------看上面的就行-------

  • 闭包是指有权访问另一个函数作用域中的变量的函数。
  • 在本质上,闭包是将函数内部和函数外部连接起来的桥梁。
  • 闭包包含自由(未绑定到特定对象)变量,这些变量不是在这个代码块内或者任何全局上下文中定义的,而是在定义代码块的环境中定义的(局部变量)
  1. 对作用域、作用域链的理解 3.对执行上下文的理解

在 js 中作用域分为四大作用域:全局作用域、函数作用域、块级作用域

    1. 全局作用域

    本质就是一个对象, 在红宝书中有对他进行描述, 他是 js 引擎内置的,指向计算机中的一块内存, 开发人员不能获取到他, 但是他确实存在

    在浏览器中他指向的是 window, 在 node 中他指向的是 global 对象, 在非严格模式下, window 和 global 是同一个对象,node 的 global 对象复用了 window 对象

    1. 函数作用域 当函数被调用的时候, 会临时创建一个执行上下文, 这个上下文就是函数的作用域, 里面保存了函数的变量等信息, 当函数执行完毕, 会销毁这个上下文
    1. 块级作用域

    块级作用域是 es2015 提出来的, 当时使用的 {} 代码块时会被识别成一个块级作用域, 还记得 var 吗, 他会变量提升, 在 es5 中如果在 {} 中,他也会被提升

        const a = 1
         if (false) {
             var a = 2
         }
        console.log(a) // 在 es5 中他会被提升到上面, 在 es2015 后他便不再提升, 他会在自己的作用域中
         同时 constlet 都是块级作用域, 不会被提升, 在 js 编译时会记录他的 关键字以及 位置, 如果在 constlet 声明变量前使用了变量会报错, reference Error, 因为 constlet 在执行前的瞬间会形成暂时性死区
    

对执行上下文的理解:评估和执行 javascript 代码的环境的抽象概念

  1. 对 this 对象的理解

    在JavaScript中,this对象是一个特殊的关键字,它在不同的上下文环境中指向不同的对象。以下是对this对象的详细理解:

    1. 全局上下文中的 this

    • 当在浏览器的全局作用域中执行代码时,this 指向全局对象,即 window 对象。

    • 在严格模式(use strict)下,全局函数中的 thisundefined

    1. 函数作为对象方法时的 this

    • 当函数被作为某个对象的方法调用时,this 指向该对象。

    • 如果函数不是直接作为对象的方法调用,而是通过其他方式(如赋值给变量后调用),则 this 可能不会指向预期的对象。

    1. 构造函数中的 this

    • 使用 new 关键字调用构造函数时,this 指向新创建的对象实例。

    1. 箭头函数中的 this

    • 箭头函数不绑定自己的 this,它的 this 是在其定义的位置继承自外部作用域的 this

    1. 事件处理函数中的 this

    • 在DOM事件处理函数中,this 通常指向触发事件的DOM元素。

    1. 改变 this 的指向

    • JavaScript 提供了 callapplybind 方法来显式地设置 this 的指向。
      • callapply 可以改变函数执行时 this 的指向,但它们立即执行函数。

      • bind 返回一个新的函数,并且这个新函数的 this 永远绑定为指定的对象,除非再次使用 bind

    1. 特殊情况下的 this

    • 在类的方法中,this 指向类的实例。

    • 在模块系统中,this 可能指向模块对象或其他特定对象,具体取决于模块系统的实现。

    1. 注意事项

    • 由于 this 的指向是由函数的调用方式决定的,因此在编写代码时需要特别注意函数是如何被调用的,以避免意外的 this 指向问题。
    • 在使用匿名函数或回调函数时,特别容易遇到 this 指向不正确的问题,这时可以考虑使用箭头函数或将 this 保存到一个变量中以供后续使用。
  2. call() 和 apply() 的区别?

参数不同

  • call 接收多个参数, 第一参数是 this, 后面是参数列表, 多个参数以 ,分隔
  • apply 接收两个参数, 第一个参数是 this, 后面是参数数组, 多个参数以数组的形式传入
  1. 实现 call、apply 及 bind 函数
function myBind (context, ...args) {
    const self = this // 记录调用者自身
    return function (...data) {
        // 合并两者的参数
        const mergeParams = [...args, ...data]
        return self.apply(context, mergeParams)
    }
  }

  function myCall (context, ...args) {
    const self = this
    // 将传进来的 this 包装成一个对象
    context = context === undefined || context === null ? globalThis : Object(context)
    const key = Symbol()
    context[key] = self
    const res = context[key](...args)
    delete context[key]
    return res
  }

  myApply 的实现和 myCall 一致

72. 异步编程的实现方式?

blog.csdn.net/yiyueqinghu…

  • 1、回调函数的方式 : 回调函数有一个致命的弱点,就是容易写出回调地狱

  • 2、Promise对象: 核心思想:每一个异步任务返回一个Promise对象,该对象有一个then方法,允许指定回调函数, 为了解决回调地狱而产生的,将回调函数的嵌套,改成链式调用 Promise.resolve(xxx).then( time2 =>step2(time2)) .then( time3 => step3(time3)) .then( result => { console.log(result is ${result}) }); Promise的写法只是回调函数的改进,用then()方法免去了嵌套,更为直观。但这样写也存在了很明显的问题,代码变得冗杂了,语义化并不强。

  • 3、生成器函数 Generator/ yield

    Generator 函数是 ES6 提供的一种异步编程解决方案:

    • yield表达式可以暂停函数执行,next方法用于恢复函数执行,这使得Generator函数非常适合将异步任务同步化。
    • yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。
    • 每个yield返回的是{value:yield返回的值,done:true/false(执行状态)} ...
  • 4、 async / await

    是一种异步处理的语法糖, 内部对 Promise 和 generator 进行了封装

  1. setTimeout. Promise. Async/Await 的区别

    在JavaScript中,setTimeoutPromiseasync/await是三种常见的异步编程方法,它们各有特点和适用场景。以下是对这三种方法的详细对比:

    1. 执行方式

    • setTimeout:通过将一个函数或代码段延迟指定时间后执行。它接受多个参数,第一个是要执行的函数或代码段,第二个是延迟的时间(以毫秒为单位), 后面的参数是可选的,可以传递给函数或代码段。

    • Promise:代表一个异步操作的最终完成(或失败)及其结果值。它有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。Promise对象用于处理异步操作,并提供链式调用的方式。

    • async/awaitasync关键字用于定义一个异步函数,而await关键字用于等待一个Promise对象。这种方式使得异步代码看起来更像同步代码,提高了代码的可读性和可维护性。

    1. 错误处理机制

    • setTimeout:没有内置的错误处理机制。如果回调函数抛出异常,该异常不会被捕获,除非使用try...catch结构包裹回调函数。

    • Promise:可以通过.catch()方法来捕获并处理Promise被拒绝时的错误。

    • async/await:可以在await表达式周围使用try...catch结构来捕获并处理异步操作中的错误。

    1. 性能与效率

    • setTimeout:由于是宏任务,其回调函数的执行可能会受到其他同步任务或微任务的影响,导致实际延迟时间可能比指定的时间长。

    • Promise:Promise本身不直接影响性能,但其处理方式(如链式调用)可能会影响代码的清晰度和可维护性。

    • async/await:虽然async/await使得异步代码看起来更像同步代码,但它本质上仍然是基于Promise实现的,因此其性能与Promise类似。然而,由于其语法糖的特性,它在某些情况下可能更易于理解和调试。

    1. 应用场景

    • setTimeout:适用于需要简单延时执行的场景,如动画效果、定时任务等。
    • Promise:适用于复杂的异步操作链式处理场景,如网络请求、文件读取等。
    • async/await:适用于需要等待多个异步操作依次完成的场景,如串行执行多个异步任务。
  2. 对 Promise 的理解

    Promise 是 es6 提出的, 用来解决回调地狱的问题,将回调函数的嵌套,改成链式调用。

    定义: Promise 是一个构造函数, 用来生成一个 Promise 对象, 他表示一个异步任务最终成功或失败及其结果值,他有三种状态: pending(进行中)、fulfilled(已成功)和rejected(已失败)。并且一旦从pending 状态变为fulfilled或rejected,就不能再变回其他状态。

    优点:

    • 解决了回调地狱的问题,使异步代码更加清晰和可维护。
    • 提供了统一的 API,各种异步操作都可以用同样的方法进行处理。
    • 支持链式调用,使得代码更加简洁和优雅。

    缺点:

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

(1)创建 Promise 对象

 new Promise((resolve, reject) => {})

(2)Promise 方法

  • 在 Promise 原型身上有:
catch()、 then()、 finally()
  • 在 Promise 对象身上有:
    • all(): 会等待所有的 Promise 都成功时才成功,只要有一个失败就失败, 并且返回的是失败的值
    • race(): 会等待第一个 Promise 成功或失败,只返回最快的 Promise 响应, 无论结果是失败还是成功的
    • allSettled(): 会等待所有的 Promise 都完成,无论成功或失败, 并且返回的是一个数组, 数组中包含每个 Promise 对象的响应结果
    • reject(): 返回一个已经拒绝的 Promise 对象, 拒绝原因为给定的参数
    • resolve(): 将给定的值转换成一个 Promise 对象, 如果本身就是 Promise 将直接返回该 Promise, 如果是 thenable 对象, 则调用他的 then 方法及其两个回调函数, 返回的结果将以该值被兑换
    • any(): 将一个 Promise 可迭代对象作为输入, 任何一个 Promise 被兑现的时候, 这个返回的 Promise 将被兑现,并返回第一个被兑现的值, 当输入所有的 Promise 都被拒绝(或则传入一个空的可迭代对象),它会以一个包含拒绝原因数组的 AggregateError 拒绝。
  1. Promise 解决了什么问题.

    1. 解决异步回调地狱的问题, 嵌套的回调函数嵌套太多,代码可读性差, 容易出现回调地狱的问题。
    2. 统一 API, 各种异步操作, 都使用同样的方式处理。
    3. 支持链式调用, 使代码更加的简洁和优雅。
    4. 提供了统一的 catch 方法进行错误捕获处理。
  2. Promise.all 和 Promise.race 的区别的使用场景,对 async/await 的理解

    1. Promise.all 方法, 接收一个 Promise 可迭代对象作为输入, 并返回一个 Promise, 当所有 Promise 都被兑换的时候, 返回的 Promise 也将被兑换,并返回一个包含所有兑现值的数组, 如果输入的任何Promise被拒绝, 则返回第一个被拒绝的原因
    2. Promise.race 方法, 静态方法接受一个 promise 可迭代对象作为输入,并返回一个 Promise。这个返回的 promise 会随着第一个 promise 的敲定而敲定。返回最快被敲定的 promise 的值。

    对 async/await 的理解: 是处理异步任务的一个语法糖。实现原理是将 Promise 和 generator 的结合,使得异步代码看起来更像同步代码。

    async 声明一个异步函数的关键字,会返回一个 Promise 对象,当执行到这样的函数时, 他会在后台执行函数体中的任务,不会阻塞主线程,被 async 标记的函数会返回一个 Promise 对象, 如果函数体中有 return 值, 那么返回值会成为 Promise 对象的 resolve 值, 如果函数体中有异常, 会被 reject

    await 会暂停当前 async 函数的执行, 等待 Promise 对象状态变为 resolved 然后继续执行, 如果 Promise 对象被拒绝, 那么 await 会给出错误, 同时 await 语句会可以被 try / catch 语句捕获错误

    使用场景:

    1. 网络请求: 网络请求是一个异步操作,可以使用 async/await 来简化代码, 使得代码更易于阅读和理解。
    2. 文件读取: 文件读取也是一个异步操作,可以使用 async/await 来简化代码, 使得代码更易于阅读和理解。
  3. await 到底在等啥?

    在等待一个 promise 兑现, 并获取他兑现后的值

    await 后面跟着的是一个表达式,这个表达可以是 Promise 实例,Thenable 对象(具有 then 方法的对象),或任意类型的值。

  4. async/await 的优势

    1. 简洁易读, 语法简单, 不需要手动调用 then 方法, 可以直接返回值, 并且可以处理异常。
    2. 执行代码可以像同步代码一样, 逻辑清晰, 容易理解。
  5. async/await 对比 Promise 的优势

    1. 语言简洁性:async/await 语法简单, 书写代码短, 执行代码可以像同步代码一样,逻辑清晰, 容易理解。Promise 采用链式调用, 可能会导致 ‘回调地狱’ 使代码维护难
    2. 错误处理: Promise 使用 .catch() 方法捕获错误, 每一个 then 都可能有异常, 增加代码复杂度;async/await 可以使用 try /catch 语句处理异常, 使错误更加集中和清晰
    3. 并发处理能力: Promise 可以并行处理多个异步任务, 并通过 Promise.all() 等操作管理这些方法, async/await 本质是串行执行异步操作, 可以通过并发编程模式来提高效率, 但在处理高并发时需要更多的控制
    4. 性能: Promise 处理大量并发请求时,性能更优, 他可以启动多个异步操作, async/await 由于其串行执行的特性, 可能在处理大量并发请求时性能稍逊一筹
  6. async/await 如何捕获异常

使用 try catch 语句进行捕获,在 catch 的 参数中会获取到错误原因

        async function test () {
            try {
                const res =  await 0/a
            } catch (err) {
                console.log('🚀 ~ This is a result of console.log ~ ✨: ', err); // ReferenceError: a is not defined
            }
        }

        test()

82. 并发与并行的区别?

1.  **定义**

*   **并发**:指在一个时间段内多个程序或线程同时运行,但并不是真正意义上的同时执行。

*   **并行**:指在同一时刻多个任务真正同时执行,通常是通过多处理器或多核系统实现的。

2.  **资源抢占**

*   **并发**:由于多个任务交替进行,CPU资源会被不同任务轮流占用,因此存在资源竞争和切换开销。

*   **并行**:每个任务都有独立的处理单元,不存在CPU资源的抢占问题,因此可以更高效地利用系统资源。

3.  **执行方式**

*   **并发**:任务之间可能会相互影响,需要协调和管理,如使用锁、信号量等同步机制来避免冲突。

*   **并行**:任务独立执行,通常不需要复杂的同步机制,但在某些情况下仍需要协调以确保数据一致性。

4.  **适用场景**

*   **并发**:适用于I/O密集型任务,如网络请求处理、用户交互等场景,可以提高系统的响应速度和吞吐量。

*   **并行**:适用于计算密集型任务,如大规模数据处理、科学计算等场景,可以显著提高计算效率。

5.  **硬件依赖**

*   **并发**:可以在单处理器系统上实现,通过时间分片来实现多任务的交替执行。
*   **并行**:依赖于多处理器或多核系统,每个处理器或核心可以独立执行一个任务。

并发强调的是在单个处理器上通过任务切换实现多任务的“同时”执行,而并行则是通过多个处理器或核心真正实现多任务的同时执行。并发适合处理I/O密集型任务,而并行则更适合计算密集型任务。在实际应用中,通常会结合两者的优点,以最大限度地提高系统性能。

83. 什么是回调函数? 回调函数有什么缺点? 如何解决回调地狱问题?

什么是回调函数?

定义:回调函数是指一个函数作为参数传递给另一个函数,并在特定事件发生或条件满足时被调用执行的函数。这种机制使得程序可以在某个操作完成后自动执行特定的代码段,而无需在主流程中等待该操作完成。

回调函数有什么缺点?

  • 可读性差:当多个回调函数嵌套在一起时,代码结构会变得复杂且难以理解,形成所谓的“回调地狱”。
  • 异常捕获困难:由于回调函数的执行时机不确定,异常捕获变得更加困难。
  • 耦合性严重:回调函数的使用可能导致代码之间的紧密耦合,使得代码难以维护和扩展

如何解决回调地狱问题?

    1. Promise:通过链式调用的方式处理异步操作,避免回调地狱,并提供更好的错误处理机制。
    1. async/await:基于 Promise 进一步简化异步编程的语法糖,使代码更加接近同步编程的风格,易于理解和维护
  1. setTimeout, setInterval,requestAnimationFrame 各有什么 特点?

    1. setTimeout:

      • 在设置的时间后,传入的函数会被执行,且只会执行一次, 执行会在全局对象中的匿名函数中执行, 所以回调 this 指向 window, 但是你可以使用箭头函数来解决 this 指向问题。
      • 接收多个参数:语法: setTimeout(functionRef, delay, param1, param2, /* …, */ paramN)
      • 返回值:timeOutId, 一个非零值, 用来标识这个定时器, 可以使用 clearTimeout(timeOutId,) 来取消这个定时器。
    2. setInterval:

      • Window 接口的 setInterval()方法,在设定的时间间隔,重复调用一个函数或执行一个代码片段。
      • 接收多个参数:setInterval(func, delay, arg1, arg2, /* …, */ argN)
      • 返回值:intervalId, 一个非零值, 用来标识这个定时器, 可以使用 clearInterval(intervalId) 来取消这个定时器。
    3. requestAnimationFrame:

      • requestAnimationFrame() 方法,方法会告诉浏览器你希望执行一个动画。它要求浏览器在下一次重绘之前,调用用户提供的回调函数。
      • 参数: 一个回调函数
      • 注意: requestAnimationFrame 是一次性执行的, 他会在浏览器下一次渲染的时候被通知执行

      备注:若你想在浏览器下次重绘之前继续更新下一帧动画,那么回调函数自身必须再次调用 requestAnimationFrame()。requestAnimationFrame() 是一次性的。

       ```
         // 示例:
           let animationId;
           function animate() {
               // 在这里编写你的动画逻辑
               console.log('Animating...');
      
               if (???) { // 如果满足某种条件,请求下一帧 
                   // 请求下一帧
                   animationId = requestAnimationFrame(animate);   
               }
               
           }
      
           // 开始动画
           animationId = requestAnimationFrame(animate);
      
           // 停止动画
           cancelAnimationFrame(animationId);
      
       ```
      

      如果你想停止动画,你可以使用 cancelAnimationFrame() 方法取消

      优点:

      • 高效:requestAnimationFrame 会根据屏幕刷新率自动调整回调函数的调用频率,从而节省资源。
      • 流畅:由于其高频率调用,动画更加流畅。
      • 节能:相比 setTimeout 和 setInterval,requestAnimationFrame 更节能,因为它只在需要重绘时才调用回调函数。

    注意这种情况: 他隐式的改变了函数的 this 指向, 但是如果你是自己写的函数,那么在浏览器环境下他始终指向 window

    ```
        function MyObject() {
            this.value = 42;
            
            this.showValue = function() {
                console.log(this.value);
            };
        }
    
        const obj = new MyObject();
    
        // 使用 setInterval 调用 obj.showValue
        setInterval(obj.showValue, 1000); // 输出: 42
    
    ```
    
  2. 对象创建的方式有哪些?

  • (1)工厂模式
  • (2)构造函数模式
  • (3)原型模式
  • (4)寄生构造函数模式
  • (5)稳妥构造函数模式
  • (6)ES6 的 class
  • (7)ES6 的 Object.create()
  • (8)ES6 的 Object.assign()
  • (11)字面量创建对象
  1. 对象继承的方式有哪些?

常见的有 8 种

 // 原型链继承
        /*
            核心: 子类原型等于父类实例
            缺点: 多个实例对引用类型的操作会被覆盖
            原理: 改变了原型链的指向
        */
        // 父类
        function Person() {
            this.colors = ['black', 'white']
        }
        // 子类
        function Child() {

        }
        Child.prototype = new Person()
        // 我们知道原型对象身上有个 constructor 属性, 用于指向构造函数, 这样我们是修改了构造函数中的 constructor 的指向
        // 如果我们不改回去, Child.prototype.constructor 就已经不指向 Child 了
        Child.prototype.constructor = Child
        const child = new Child()
        console.log('🚀 ~ This is a result of console.log ~ ✨: ', child.colors);
        // 但原型链继承有缺陷, 多个实例对引用类型的操作会被覆盖, 原因: new Person() 在 Child 的原型链上, 是同一份数据, 修改了就等于修改了所有实例
        child.colors.push('red')
        console.log('🚀 ~ This is a result of console.log ~ ✨:push.red ', child.colors);
        const instance = new Child()
        console.log('🚀 ~ This is a result of console.log ~ ✨:instance  ', instance.colors);


        // 构造函数继承
        /*
            核心: 使用 call 等方法, 显示改变父类的 this 指向, 使父类的 this 指向子类
            缺点: 只能继承父类的实例属性和方法, 不能继承原型属性和方法,影响性能, 每个子类都有父类实例函数的副本, 影响性能
            原理: 在子类构造函数中, 调用父类构造函数并显示的改变父类构造函数的 this 指向子类
        */

        function ConstructorPerson() {
            this.colors = ['black', 'white']
        }

        function ConstructorChild() {
            ConstructorPerson.call(this) // 这句话是核心  
        }

        const constructorChild = new ConstructorChild()
        constructorChild.colors.push('red')
        console.log('🚀 ~ This is a result of console.log ~ ✨:push.red ', constructorChild);
        const instanceConstructor = new ConstructorChild()
        console.log('instanceConstructor  ', instanceConstructor);


        // 组合继承
        /*
            核心: 在原型链上继承了一份, 在实例身上也继承了一份。 构造函数继承和原型链继承的组合, 将构造函数和原型链继承结合, 解决了原型链继承的不足, 既可以继承实例属性, 也可以继承原型属性
            缺点: 需要维护两份相同的数据, 在实例身上有一份, 原型身上还有一份
            原理: 增强子类实例,在实例身上有一份, 在原型链上再写一份, 可以弥补构造函数继承不能访问原型链上的属性, 也可以解决原型链继承时多个实例对象对引用类型数据操作时的覆盖, 因为他会先找实例身上, 再找原型身上
        */

        function CompositePerson() {
            this.colors = ['black', 'white']
            this.name = 'parent'
        }

        CompositePerson.prototype.getName = function () {
            return this.name
        }

        function CompositeChild() {
            CompositePerson.call(this) // 构造函数继承, 给实例身上添加了两个属性
            this.name = 'child' // 写在这里
        }

        // 继承原型链
        CompositeChild.prototype = new CompositePerson() // 原型链继承, 在原型身上添加了两个属性
        CompositeChild.prototype.constructor = CompositeChild // 在查找时依然以 CompositeChild 为构造函数
        const compositeChild = new CompositeChild()

        console.log('🚀 ~ This is a result of console.log ~ ✨: ', compositeChild.colors, compositeChild.name);
        compositeChild.colors.push('red')   
        console.log('��� ~ This is a result of console.log ~ ��:push.red ', compositeChild.colors);
        const compositeChildInstance = new CompositeChild()
        console.log('compositeChildInstance  ', compositeChildInstance);

        // 原型式继承
        /*
            核心: 利用一个空对象作为中介, 将某个对象直接赋值给空对象构造函数的原型
            缺点: 原型链继承多个实例的引用类型属性指向相同, 如果对引用类型数据操作, 会覆盖
            原理: 利用对象原型进行继承, 以一个对象作为原型, 实现继承
        */
        const proto = {
            name: 'proto',
            colors: ['black', 'white']
        }
        // 以这对象作为原型, 实现继承
        const instanceProto = Object.create(proto) // 实现了原型的继承
        instanceProto.name = 'instanceProto'
        instanceProto.colors.push('red')
        console.log(instanceProto);
        // colors 的值会被上一步覆盖
        const instanceProto2 = Object.create(proto)
        console.log(instanceProto2, 'instanceProto2');

        // 寄生式继承
        /*
            核心: 在原型式继承的基础上, 增强对象, 返回增强对象
            缺点: 和原型式继承相同,继承了原型链的多个实例的引用类型属性指向相同, 如果对引用类型数据操作, 会覆盖
            原理: 使用 Object.create() 创建对象后, 在这对象身上添加方法或属性, 最后返回这个对象
        */
        function createAdditionalObj (obj) {
            // 用传进来的对象作为原型
            const temp = Object.create(obj)
            temp.sayHi = function () { // 实现寄生
                console.log('hi');
            }
            // 返回这个对象
            return temp
        }

        const additionalObj = createAdditionalObj(proto) // 如果多个对象对引用类型数据操作, 会覆盖, 后面创建的对象, 也会对应的被覆盖
        additionalObj.sayHi()
        console.log('🚀 ~ This is a result of console.log ~ ✨: ', additionalObj);

        // 寄生组合式继承
        /*
            核心: 使用组合继承方式(原型链继承 + 构造函数继承) + 寄生式继承的方式实现
            缺点: 实现有复杂性, 对于新手来说, 可能不容易理解, 并且原型链有污染的风险, 如果修改过父类的原型, 那么整个原型链都会被污染, 有性能开销, 虽然只调用一次父类构造函数, 但是依然涉及创建父类的副本以及修改子类的原型
            会造成性能消耗
            原理: 原型链继承 + 构造函数继承 + 寄生式继承
            注意: 这是一种比较成熟的实现方式, 很多库都在使用这种方法
        */

        function inheritPrototype (child, parent) {
            // 我们用父类的原型对象构造一个对象的副本
            const prototype = Object.create(parent.prototype) // 可以拿到父类的原型对象
            prototype.constructor = child // 修改 constructor 指向子类
            // 原型链继承, 将父类的实例对象赋值给子类的原型对象
            child.prototype = prototype
        }

        // 父类
        function combineProtoPerson () {
            this.colors = ['black', 'white']
            this.name = 'parent'
        }

        // 子类
        function combineProtoChild () {
            combineProtoPerson.call(this) // 构造函数继承
            this.name = 'child'
        }

        // 实现寄生式继承, 用一个函数实现
        inheritPrototype(combineProtoChild, combineProtoPerson)

        const instance1 = new combineProtoChild()
        const instance2 = new combineProtoChild()

        instance1.colors = ['black', 'white']
        instance1.colors.push('red')
        console.log('🚀 ~ This is a result of console.log ~ ✨: ', instance1.colors);
        console.log(instance2);

        // Es6 的 class 语法实现继承
        class ParentClass {
            constructor (colors, name) {
                this.colors = colors
                this.name = name
            }

            get() {
                console.log('hello');
            }
        }

        class ChildClass extends ParentClass {
            constructor (colors, name) {
                super(colors, name)
                this.name = 'child1'
            }
        }

        const childClass = new ChildClass(['black', 'white'], 'child')
        console.log('🚀 ~ This is a result of console.log ~ ✨: ', childClass);

        // 混入方式继承多个对象
        /*
            核心: 使用 Object.assign() 方法, 将多个对象原型身上的方法拷贝到目标对象的原型上
            缺点: 存在原型污染, 如果出现同名的属性或方法, Object.assign 方法后面对象的会覆盖前面的, 会造成原型污染, 代码可读性性差
            原理: 利用 Object.assign() 方法, 将多个对象原型身上的方法拷贝到目标对象的原型上
        */

         function MyParent() {
             this.colors = ['black', 'white']
             this.name = 'parent'
         }

        MyParent.prototype.get = function () {
            console.log('hello');
        }

         function MyChild() {
             this.name = 'child'
         }

         function TargetClass() {
            // 给实例添加属性
            MyParent.call(this)
            MyChild.call(this)
         }

         TargetClass.prototype = Object.create(MyParent.prototype) // 以父类原型对象创建实例
         Object.assign(TargetClass.prototype, MyChild.prototype) // 合并其他原型上的属性
         TargetClass.prototype.constructor = TargetClass

         const targetInstance = new TargetClass()
         console.log('🚀 ~ This is a result of console.log ~ ✨: ', targetInstance);

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

    (1)垃圾回收的概念 (2)垃圾回收的方式 (3)减少垃圾回收

浏览器垃圾回收机制是一种自动管理内存的机制,用于释放不再被使用的内存空间。以下是对浏览器垃圾回收机制相关信息的具体介绍:

  1. 垃圾回收的概念:垃圾回收是指自动内存管理的一种形式,它负责查找并释放那些不再被程序使用的内存资源。在JavaScript中,垃圾回收机制尤为重要,因为JavaScript是一种高级、解释型语言,运行时需要频繁分配和回收内存。

  2. 垃圾回收的方式:常见的垃圾回收方式包括标记清除(Mark-and-Sweep)、引用计数(Reference Counting)以及V8引擎特有的分代式垃圾回收。其中,标记清除是当前主流浏览器普遍采用的方法,而V8引擎则采用了更为高效的分代式垃圾回收策略,将内存分为新生代和老生代,分别采用不同的算法进行优化处理.

  3. 减少垃圾回收:为了减少垃圾回收的频率和提高性能,开发者可以采取一系列优化措施。例如,尽量减少变量的创建和销毁,避免使用全局变量,谨慎使用闭包,避免循环引用等。这些措施有助于降低垃圾回收器的负担,从而提高应用程序的性能和响应速度。

推荐观看这篇文章: juejin.cn/post/742740…

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

什么叫内存泄漏: 程序中分配的内存由于某种原因程序未释放或无法释放叫做内存泄漏

    1. 闭包: 函数与函数之间共享变量,导致函数无法释放,导致内存泄漏。
    1. 循环引用: 当两个对象相互引用时,导致无法释放,导致内存泄漏。
    1. 缓存: 缓存中存储的数据无法释放,导致内存泄漏。
    1. 引用计数算法: 引用计数算法是一种内存管理算法,它通过计数器来跟踪对象被引用的次数,当引用计数为 0 时,对象被释放。但是,引用计数算法存在一些问题,例如:
      1. 循环引用: 当两个对象相互引用时,导致无法释放,导致内存泄漏。
      2. 误判: 引用计数算法可能会误判对象为未被引用,导致内存泄漏。

...

  1. 闭包

    • 闭包是指函数与其词法环境的组合。在JavaScript中,当一个内部函数引用了外部函数的变量时,这些变量不会被垃圾回收机制回收,因为它们仍然被内部函数引用着。这会导致内存泄漏,特别是在长时间运行的应用中。
  2. 循环引用

    • 循环引用发生在两个或多个对象相互引用时,形成一个闭环。例如,对象A引用对象B,而对象B又引用对象A。这种情况下,即使没有其他引用指向这些对象,它们也不会被垃圾回收,因为引用计数永远不会归零。
  3. 缓存

    • 缓存是一种常见的优化技术,用于存储频繁访问的数据以加快访问速度。然而,如果缓存中的数据没有被正确清理或管理,可能会导致内存泄漏。例如,缓存中存储了大量不再需要的数据,而这些数据占用了大量内存空间。
  4. 引用计数算法

    • 引用计数是一种内存管理算法,通过跟踪对象的引用次数来决定何时释放对象。当引用计数为零时,对象会被释放。然而,引用计数算法存在一些问题:
      • 循环引用:如前所述,当两个对象相互引用时,它们的引用计数永远不会归零,导致内存泄漏。
      • 误判:引用计数算法可能会误判某些对象为未被引用,从而无法及时释放内存。例如,在某些复杂的数据结构中,临时引用可能导致引用计数不准确。

    除了上述原因外,还有一些其他可能导致内存泄漏的情况:

  5. 全局变量

    • 全局变量在整个程序生命周期内都存在,因此如果不小心使用全局变量来存储大量数据,可能会导致内存泄漏。
  6. 事件监听器

    • 在JavaScript中,如果添加了事件监听器但没有在适当的时候移除,会导致内存泄漏。例如,DOM元素被删除后,如果没有移除其事件监听器,该元素及其相关资源将无法被垃圾回收。
  7. 定时器和回调

    • 定时器(如setInterval)和回调函数如果没有在不需要时清除,也会导致内存泄漏。例如,定时器持续运行但不再需要时,如果没有调用clearInterval来停止它,它将一直占用内存。
  8. DOM泄漏

    • 在Web开发中,如果动态创建的DOM元素没有被正确移除,或者事件监听器没有被移除,就会导致DOM泄漏。这种情况通常发生在单页应用(SPA)中,页面切换时没有正确清理旧的DOM元素和事件监听器。
  9. 第三方库和插件

    • 一些第三方库和插件可能没有正确地管理内存,导致内存泄漏。在使用这些库时,需要注意它们的内存管理方式,并在不需要时进行适当的清理。
  10. 引用计数:

    引用计数是一种常见的内存管理技术,用于跟踪对象生命周期。每当有一个新的变量引用一个对象时,该对象的引用计数加1;每当有一个变量停止引用该对象时,该对象的引用计数减1。当引用计数为0时,意味着该对象不再被任何变量所引用,因此可以安全地被销毁。

  11. 如何判断一个对象是否属于某个类?

可以使用 instanceof 操作符来判断

    class MyClass {
        constructor (name) {
            this.name = name
        }
    }

    const instance = new MyClass('test')
    console.log(instance instanceof MyClass) // true

91. async/await 实现

在讲到这个的时, 你必须清楚什么是协程, 一个线程可以有多个协程, 但是一个线程只能同时执行一个协程, 比如说一个 A 协程开启了 B 协程,那么A 协程会被挂起, 等 B 协程执行完毕后, A 协程才被恢复, 这样就保证了一个线程只能同时执行一个协程, 同时 A 协程又叫做 B 协程的父协程。在生成器函数中, 通过 yield 将协程挂起, 使用 next 函数恢复协程

协程带来的好处就是可以提升性能,协程的切换并不会像线程切换那样过多地消耗资源。了解了协程的存在,我们就可以知道为什么会有生成器了。

async 的作用是将 generator 和自执行器进行了封装, await 则类似于 yield 语句
    function test (num) {
        return Promise.resolve(2 * num)
    }

    function * generator () {
        const yield1 = yield test(1)  // 他会执行 test 中代码
        const yield2 = yield test(yield1)
        return yield2
    }

    // 实现自执行函数
    function asyncFunc (generators) {
        return () => {
            return new Promise((resolve, reject) => {
                const gen = generators()
                
                const doYield = (val) => {
                    let res 
                
                    try {
                        res = gen.next() // 这是一个满足迭代器协议的值
                    } catch (e) {
                        reject(e) // 失败了, 抛出错误
                    }
                    if (res.done) {
                        return resolve(res.value) // 返回数据, 他是以个 Promise 值
                    } else {
                        res.value.then(result => doYield(result)) // res.value 就是函数 test 执行后的结果
                    }
                }
                doYield() // 执行这个函数, 
            })
        }
    }

    // 在外部使用
    asyncFunc(generator)()
    

92. 看题会输出什么


 function bar() {
        var myname = 'Tom'
        let test1 = 100
        if (1) {
          let myname = 'Jerry'
          console.log(test, myname)
        }
      }

      function foo() {
        var myname = '彭于晏'
        let test = 2
        {
          let test = 3
          bar()
        }
      }

      var myname = '刘德华'
      let test = 1
      foo()
      
      // 1 Jerry

image.png