基础八股剩余

发布时间 2023-12-31 21:37:59作者: 前端自信逐梦者

1. 0.1+0.2 为什么不等于 0.3?

首先需要知道的是js 内部计算都是以二进制进行的。
整数部分转二进制采用:除 2 取余,逆序排列
小数部分转二进制采用:乘 2 取整,顺序排列

js 中 Number 类型使用 IEEE754 标准 64 位存储,也就是标准的 Double 双精度浮点数。

其中小数位只能保留 52 位,而 0.1 和 0.2 的小数位都是无穷的,所以截取保留 52 位。(第一次的精度丢失)

将 0.1 和 0.2 相加得到 53 位,这里又要进行一次截取。(第二次的精度丢失)

总结
  1. js 使用二进制去处理数据的,采用的是双精度浮点数标准存储 Number 类型。
  2. 精度丢失不是 js 的问题,而是 IEEE754 的标准存储为有限,小数位超出 52 位只能进位截取。
  3. 过程中的精度丢失发生在存储和添加中。

2. Symbol 类型

2.1. 定义:

symbol 是 es6 中新增的基本数据类型,它表示了一个独一无二的值,可以避免属性名的冲突。

2.2. 概述

symbo 类型的值是通过 Symbol 构造函数创建的。
Symbol()函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述

let sym1 = Symbol(1);
let sym2 = Symbol(1);
console.log(sym1 === sym2); // false

如何让两个 symbol 值相等呢?

使用 Symbol.for(),它接受一个字符串参数,然后搜索有没有这个参数作为名称的 Symbol 值,如果有,就返回这个 Symbol 值,没有的话,就创建一个,然后注册到全局中。

let sym1 = Symbol.for(1);
let sym2 = Symbol.for(1);
let sym2 = Symbol.for(3);
console.log(sym1 === sym2); // true
console.log(sym1 === sym3); // false

那么问题来了,使用 Symbol()和使用 Symbol.for()创建 Symbol 有什么区别呢

区别就在于,使用 Symbol.for 创建的值会被登记在全局环境,供全局搜索,Symbol 不会。
使用 Symbol 创建虎,会首先在全局中查找有没有以当前参数作为名称的 Symbol 值,如果找到了就返回,没有就创建一个。而使用 Symbol 构造函数创建的每次都会返回一个新的 Symbol 值,因为它没有登记机制。

拓展 Symbol.keyFor()
Symbol.keyFor()会返回已经登记的 Symbol 值的 key。

let sym1 = Symbol(1);
let sym2 = Symbol.for(1);
console.log(Symbol.keyFor(sym1)); // undefined
console.log(Symbol.keyFor(sym2)); // 1

2.3. Symbol 作为对象的属性名

let sym1 = Symbol('name');
let sym2 = Symbol('age');

let obj = {
  sex: '男',
  [sym1]: '张三',
  [sym2]: 18,
};
console.log(obj); // { sex: '男', [Symbol(name)]: '张三', [Symbol(age)]: 18 }

打印对象的属性名

let sym1 = Symbol('name');
let sym2 = Symbol('age');

let obj = {
  sex: '男',
  [sym1]: '张三',
  [sym2]: 18,
};
// 1. for in
for (const key in obj) {
  console.log(key); // sex
}
// 2. Object.keys()
console.log(Object.keys(obj)); // ["sex"]
// 3. Object.getOwnPropertyNames()
console.log(Object.getOwnPropertyNames(obj)); // ["sex"]
// 4. Object.getOwnPropertySymbols()
console.log(Object.getOwnPropertySymbols(obj)); // [ Symbol(name), Symbol(age) ]
// 5.Reflect.ownKeys()
console.log(Reflect.ownKeys(obj)); // [ 'sex', Symbol(name), Symbol(age) ]

2.4. 使用 Symbol 的场景

  • 用于定义对象的属性名 ,避免属性名冲突
  • 用于实现常量或枚举值,结合 switch 语句使用

3. typeof null 为什么是 object

首先这是 js 的历史遗留问题,现在无法修复。
在判断数据类型时,是根据机器码的低位来判断的。
在早期的 js 版本中,使用的是 32 位来存储值,前三位的是数据类型的标记,
而对象的前三位机器码都是 000,null 在设计的最初,是想将其对应成 C 中的空指针,
它的机器码全为 0,那这就与对象的低位机器码一样,所以就导致了将 null 识别为 object 类型。

4. substring()和 substr()的区别

区别就在于传递的参数不同。

4.1 substring(startIndex,endIndex)

substring() 方法返回该字符串从起始索引到结束索引(不包括)的部分,如果未提供结束索引,则返回到字符串末尾的部分。

let str = 'hello world';
console.log(str.substring(0)); // hello world
console.log(str.substring(2)); // llo world
console.log(str.substring(0, 1)); // h
console.log(str.substring(1, 4)); // ell

4.1 substr(startIndex,length)

substr() 方法返回该字符串的一部分,从指定的索引开始,然后扩展到给定数量的字符(MDN 显示已弃用)

let str = 'hello world';
console.log(str.substr(0)); // hello world
console.log(str.substr(2)); // llo world
console.log(str.substr(0, 1)); // h
console.log(str.substr(1, 4)); // ello

5. 如何实现 js 脚本异步加载?

5.1 动态设置 script 标签,设置标签的 src 属性为需要加载 js 脚本的 URL,通过 onload 或 onreadystatechange 事件来检测脚本是否加载完成。

const script = document.createElement('script');
script.src = 'path/script.js';
script.onload = function () {
  // 脚本加载完成后的回调
};
document.body.appendChild(script);

5.2 使用 XMLHttpRequest 或者 Fetch API 发送异步请求,请求成功后得到响应文本并解析成 js 代码,然后通过 eval()或者 Function()构造函数执行该脚本。

const xhr = new XMLHttpRequest();
xhr.open('GET', 'path/script.js');
xhr.onload = function () {
  const script = document.createElement('script');
  script.textContent = xhr.responseText;
  document.body.appendChild(script);
};
xhr.send();

5.2 相对于同步加载,异步加载 js 有什么区别呢?

  • 提高响应速度,避免因为 js 阻塞造成页面无法渲染或卡顿的情况。
  • 避免因为加载 js 脚本耗时较长,导致页面长时间空白的情况,使其他资源可以更快加载和呈现。
  • 可以灵活的控制脚本的加载顺序或执行时机。比如动态的加载或卸载 js 脚本,提升了可扩展性。

5.3 拓展:script 标签的 async 和 defer 属性

async 异步
使用这个属性的 script 标签,在浏览器遇到时,就会进行脚本的下载,下载完成后就会执行,但是如果一些脚本中有些需要操作 DOM 的时候,就会导致页面 DOM 还没有加载完毕,脚本就对其操作,产生报错。
defer 推迟
在浏览器遇到这个属性的 script 标签时,就会进行脚本的下载,不会立即执行,如果 HTML 解析还没结束,那么它会等 HTML 解析结束后再去执行。

6. for...in 和 for...of 的区别?

6.1 for...in

for...in 用来遍历对象的可枚举属性(包括自由属性和继承属性)。
不支持遍历对象的 Symbol 属性,要想遍历可以用 Reflect.ownKeys(obj),返回的是一个数组。

let obj = {
  name: '张三',
  age: 18,
};
for (const key in obj) {
  console.log(key);
}

6.2 for...of

for...of 用于遍历可迭代对象,将每个元素作为迭代变量来遍历。
即 for...of 可以遍历实现了 iterator 接口的对象。

拓展:可以让 for...of 遍历对象吗?

可以,需要手动实现对象的 iterator 接口。

7. 数组的 slice()和 splice()会改变原数组吗?怎么删除数组最后一个元素?

7.1 slice()

slice()会截取数组,返回从起始索引到结束索引(不包含)的元素,并构成一个新的数组。不会改变原来的数组
参数
参数 1:可选,起始的索引 start

  • start < 0,从数组末尾开始计算,即此时真正的 newStart = start + arr.length
  • start < -arr.lenth,或者省略,则使用 0
  • start >= arr.lenth,不会提取任何元素

参数 2:可选,结束的所用 end

  • end < 0,从数组末尾开始计算,即此时真正的 newEnd = end + arr.length
  • end < -arr.lenth,或者省略,则使用 arr.length
  • end >= arr.lenth,则提取所有元素
let arr = [1, 2, 3, 4, 5];
console.log(arr.slice(1)); //[2, 3, 4, 5]
console.log(arr.slice(1, 3)); //[2, 3]
console.log(arr.slice()); //[1, 2, 3, 4]

7.2 splice()

slice()可以在数组中添加、删除、替换元素,并返回被删除元素的数组。会改变原来的数组
参数
参数 1:起始的索引 start

  • start < 0,从数组末尾开始计算,即此时真正的 newStart = start + arr.length
  • start < -arr.lenth,使用 0
  • start >= arr.lenth,不会删除任何元素
  • 不传参数,不会删除任何元素,这和传入 undefined 不同,传入 undefined 会被转换成 0

参数 2:可选,表示数组中要从 起始索引 开始删除的元素数量
参数 3:可选,从 起始索引 开始要加入到数组中的元素。
参数 4:同上
...
参数 n

let arr = [1, 2, 3, 4, 5];
// 第二个参数不填,会从起始索引开始,删除后面的所有元素
console.log(arr.splice(1)); //[2, 3, 4, 5]
console.log(arr); // 1

let arr1 = [1, 2, 3, 4, 5];
console.log(arr1.splice(1, 2, 3)); // [2, 3]
console.log(arr1); // [1, 3, 4, 5]

7.3 怎么删除数组最后一个元素?

  • pop()
let arr = [1, 2, 3, 4, 5];
arr.pop();
console.log(arr); // [1, 2, 3, 4]
  • splice()
let arr = [1, 2, 3, 4, 5];
arr.splice(arr.length - 1, 1);
console.log(arr); // [1, 2, 3, 4]
// 或者
let arr3 = [1, 2, 3, 4, 5];
// 起始索引传入-1,起始索引会被转换成 -1 + arr3.length = -1 + 5  = 4,同上
arr3.splice(-1, 1);
console.log(arr3); // [1, 2, 3, 4]
  • slice()
let arr = [1, 2, 3, 4, 5];
// 结束索引传入-1,结束索引会被转换成 -1 + arr3.length = -1 + 5  = 4
let newArr = [...arr.slice(0, -1)];
console.log(newArr); // [ 1, 2, 3, 4 ]

8. == 与 === 有什么区别?

8.1 ==双等号

==运算符比较两个值是否相等,如果两个值不同类型,会先进行类型转换后再比较。
可以概括为一下几点:

  1. 如果两个值类型相同,则直接比较它们的值。
  2. 如果一个值为 null,另一个值为 undefined,则它们相等。
  3. 如果一个值为数字,另一个值为字符串,则先将字符串转位数字,再比较。
  4. 如果一个值是布尔值,另一个值是非布尔值,则先将布尔值转换成数字在进行比较。
  5. 如果一个值是对象,另一个值是数组、字符串、布尔值,则讲对象转换为原始值再比较。
// 字符串被转为数字再比较
console.log(1 == '1'); // true
// 布尔值被转为数字再比较
console.log(true == 1); // true
console.log(null == undefined); // true
// [5]转为原始值,[5].toString() = '5'
console.log('5' == [5]); // true
// 使用==比较引用类型时,实际上比较的两个对象在内存中位置
console.log({} == {}); // false

8.2 ===三等号

=== 运算符,不会进行类型转换,只有当运算符左右的类型和值都相等是才返回 true。
同理,当===运算符左右两边都是引用类型的时候,则比较的是对象在内存中的位置。

9. let 和 const 在全局声明变量,window 能获取吗?

let 和 const 声明变量的时候,会创建一个块级作用域,不会被挂载到全局对象上,
因此通过 window 无法获取,这与 var 不同,var 声明的变量会被挂载到全局对象上,可以
通过 window 获取。
如何在全局获取 let 或者 const 声明的变量呢?
可以通过将变量的值赋值给一个全局对象的属性。

10.forEach 可以结束循环吗?

数组的 forEach 不支持提前结束循环,即不支持 break 和 return 的方法跳出循环,但是可以通过向外抛出一个错误达到提前结束循环的效果。

拓展:退出双层 for 循环

outer: for (let i = 0; i < 10; i++) {
  for (let j = 0; j < 10; j++) {
    if (j == 0) {
      console.log(j);
    } else {
      break outer; //break;
    }
  }
}