JS常见问题

本文详细探讨了JavaScript中数组的判断方法、数据类型及其区别、如何避免对象属性修改、newString与基本类型的区别、Symbol的使用、解构赋值技巧、数组map的精度问题以及数组去重的处理,尤其关注对象的引用与循环引用处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 如何判断是数组

  1. Array.isArray
    console.log(Array.isArray(arr))
  2. instanceof(instanceof 运算符用于验证构造函数的 prototype 属性是否出现在对象的原型链中的任意位置)
    console.log(arr1 instanceof Array)
  3. constructor(实例的构造函数属性constructor指向构造函数)
    console.log(arr.constructor === Array)
  4. Object.prototype.toString.call()
    Object.prototype.toString.call(arr) === '[object Array]');
  5. 原型链
    arr.__proto__ === Array.prototype
  6. Array.prototype.isPrototypeOf(isPrototypeOf用于判断 一个对象是否是另一个对象的原型)
    Array.prototype.isPrototypeOf(arr)

2. JS数据类型和相关问题

  1. 数据类型
    string、boolean、number、null、undefined、bigInt、symbol、Object(array、function)

  2. null和undefined的区别
    undefined:未定义的值 。这个值的语义是,希望表示一个变量最原始的状态,而非人为操作的结果 。

    • 声明一个变量,但是没有赋值
    • 访问对象上不存在的属性或者未定义的变量
    • 函数定义了形参,但没有传递实参
    • 函数没有返回值时,默认返回undefined
    • 使用void对表达式求值

    null:空值。这个值的语义是,希望表示一个对象被人为的重置为空对象,而非一个变量最原始的状态 。 在内存里的表示就是,栈中的变量没有指向堆中的内存对象。

    • 定义的变量在将来用于保存对象,那么最好将该变量初始化为null
    • 当一个数据不再需要使用时,我们最好通过将其值设置为null来解除引用

    特殊的typeof null
    null值表示一个空对象指针,它代表的其实就是一个空对象。

  3. number和bigInt的区别

number:
JavaScript 所有数字都保存成 64 位浮点数,这给数值的表示带来了两大限制。一是数值的精度只能到 53 个二进制位(相当于 16 个十进制位),大于这个范围的整数,JavaScript 是无法精确表示,这使得 JavaScript 不适合进行科学和金融方面的精确计算。二是大于或等于2的1024次方的数值,JavaScript 无法表示,会返回Infinity。
Number在计算机中采取了IEEE754 双精度规范
使用8个字节(64位进行存储),结构如下

符号位 Sign(S) : 1bit (b63)
指数部分Exponent(E) : 11bit (b62-b52)
尾数部分Mantissa(M) : 52bit (b51-b0)
表达式 -1^S * 2^E * 1.M
在这里插入图片描述

最大安全整数Number.MAX_SAFE_INTEGER === 2^53-1
超出安全数,Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2 // true2^53也可以表示,但与2^53+1相同,所以无法知道表示的是谁,2^53不安全。
最大数:Number.MAX_VALUE = 2^1024
Number.EPSILON: 是 JS 能够表示的最小精度。误差如果小于这个值,就可以认为已经没有意义了,即不存在误差了。JS中所有数字都是浮点数,计算不精准,所以通过这个极小值,当误差小于这个值,则可以忽略不计。

bigInt:表示大整数,BigInt 类型的数据必须添加后缀n。没有位数的限制。
12n === 12是false,类型不同,12n == 12为true

  • 不能用 Math 对象中的方法。
  • 不能和 Number 类型数值进行混合算术运算,必须转换成同一类型。
  • 虽然 BigInt 不能和 Number 直接进行算术运算,但使用比较运算符(==、<、>等等)直接比较是允许的。
  • BigInt 转换为 Number 可能会丢失精度。
  1. symbol
    Symbol,表示独一无二的值
    当symbol作为属性名时,需要通过Object.getOwnPropertySymbols()遍历,对象的其他方法都无法遍历。
    Symbol.for()可以新建两个一样的symbol
let a1 = Symbol.for('a')
let a2 = Symbol.for('a')
console.log(a1 === a2) // true

Symbol.for()与Symbol()这两种写法,都会生成新的 Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。

3. 如何避免一个对象的属性被修改?

在 JavaScript 中,你可以使用不同的方式来设置对象的属性为不可修改。以下是其中三种常用的方法:

  1. 使用 Object.defineProperty() 或 Object.defineProperties() 方法:这两个方法可以用来定义或修改对象的属性,并且可以通过设置属性描述符的相关选项来控制属性的特性。通过将 writable 设置为 false,你可以将属性设置为不可修改。
const obj = {};
Object.defineProperty(obj, 'propertyName', {
  value: 'propertyValue',
  writable: false, // 将属性设置为不可修改
});

obj.propertyName = 'newValue'; // 尝试修改属性,但不生效
console.log(obj.propertyName); // 输出: propertyValue
  1. 使用 Object.freeze() 方法:Object.freeze() 方法可以冻结一个对象,使其属性变为不可修改(包括值和属性的可配置性)。一旦对象被冻结,任何对其属性进行修改的尝试都将被忽略。
const obj = {
  propertyName: 'propertyValue'
};

Object.freeze(obj); // 冻结对象

obj.propertyName = 'newValue'; // 尝试修改属性,但不生效
console.log(obj.propertyName); // 输出: propertyValue
  1. 使用 ECMAScript 6 的类和 get 访问器:在类中定义属性时,可以使用 get 访问器而不提供 set 访问器。这会使该属性成为只读属性,不可修改。
class MyClass {
  constructor() {
    this._propertyName = 'propertyValue';
  }

  get propertyName() {
    return this._propertyName;
  }
}

const obj = new MyClass();
obj.propertyName = 'newValue'; // 尝试修改属性,但不生效
console.log(obj.propertyName); // 输出: propertyValue

4. new String(‘a’)和’a’相同吗?

var str = 'hello world'
var str1 = String('hello world')
var str2 = new String('hello world')
console.log(str1 === str) // true
console.log(str2 === str) // false

str和str1都是基本类型,但str2,此时的String为一个构造函数,而 new 操作符创建了一个字符串对象,此时的 str2 为字符串对象,类型为引用类型。

5. 判断数据类型

function typeFunc(val) {
	if (typeof val !== 'object') return typeof val;
  else {
    return Object.prototype.toString.call(val).slice(8, -1).toLocaleLowerCase();
  }
}
console.log(typeFunc(new Date())) // date

6. new 一个构造函数,如果函数返回 return {} 、 return null , return 1 , return true 会发生什么情况?

  • 构造函数没有返回值、原始值1、null、true,都不受影响会返回构造函数的实例
  • 构造函数没有一个新的对象,那么new会生成一个新的对象
function Person() {
  
}
let p1 = new Person()
console.log(p1)  //Person {}

function PersonNumber() {
  return 1;
}
let p2 = new PersonNumber()
console.log(p2)  //PersonNumber {}

function PersonObj() {
  return {
    name: "1234"
  }
}

let p3 = new PersonObj()
console.log(p3)  //{name: '1234'}

7. Symbol

  1. symbol运算
    Symbol不能进行隐式类型转换,但可以显式转为字符串;不能转化为数字,但可以转化为布尔值
const s = Symbol('s')

// 隐式类型转换会抛类型错误
console.log(s + '/s'); // TypeError: Cannot convert a Symbol value to a string
console.log(`${s}/s`) // TypeError: Cannot convert a Symbol value to a string

// 只能先进行强制转换
console.log(String(s) + '/s'); // Symbol(s)/s
console.log(s.toString() + '/s'); // Symbol(s)/s
  1. 使用symbol
    Symbol值作为属性名时,需要注意两点:
  • 不能通过点运算符访问,需要通过方括号的形式访问。

  • 不能通过for…in、for…of遍历,也不会被 Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。但是它也不是私有属性,可以通过Object.getOwnPropertySymbols()和 Reflect.ownKeys()方法获取对象Symbol 属性名。

  1. 方法
  • Symbol.for()
    它跟symbol()的区别是Symbol()定义的值每次都是新建,即使描述相同值也不相等,而Symbol.for()定义的值会先检查给定的描述是否已经存在,如果不存在才会新建一个值,并把这个值登记在全局环境中供搜索,Symbol.for()定义相同描述的值时会被搜索到,描述相同则他们就是同一个值
// Symbol.for()
let a1 = Symbol.for('a');
let a2 = Symbol.for('a');
a1 === a2  // true

// Symbol.keyFor()
let a1 = Symbol.for("a");
Symbol.keyFor(a1);    // "a"
let a2 = Symbol("a");
Symbol.keyFor(a2);    // undefined,Symbol()写法没有登记机制,所以每次调用都会返回一个不同的值,返回undefined
  1. 使用场景
  • 作为对象属性 当一个复杂对象中含有多个属性的时候,很容易将某个属性名覆盖掉,利用 Symbol 值作为属性名可以很好的避免这一现象
const name = Symbol('name');
const obj = {
    [name]: 'ClickPaas',
}
  • ES6 中的类是没有 private 关键字来声明类的私有方法和私有变量的,但是我们可以利用 Symbol 的唯一性来模拟
const speak = Symbol();
class Person {
    [speak]() {
        console.log(123)
    }
}
let person = new Person()
console.log(person[speak]())

8. 如何让 var [a, b] = {a: 1, b: 2} 解构赋值成功?

解构赋值:左侧是一个数组,而右侧则应该是一个具有迭代器接口的对象(如数组、Map、Set等)

var [a, b] = {a: 1, b: 2}
直接赋值会报错,因为{a: 1, b: 2}不是一个可以迭代的对象,要想赋值成功,就需要将{a: 1, b: 2}变为一个有迭代器的对象
在这里插入图片描述
先了解一下array为什么可以被迭代,打印数组后可以看到原型有一个方法,该方法返回一个迭代器对象
在这里插入图片描述
所以要想使一个数据具有迭代器,则满足

interable
{
    [Symbol.iterator]: function () {
        return 迭代器 (可通过next()就能读取到值)
    }
}

则最终改代码如下,给object原型加一个可以迭代的对象方法即可。

Object.prototype[Symbol.iterator] = function(){
   // 使用 Object.values(this) 方法获取对象的所有值,并返回数组的迭代器对象
   return Object.values(this)[Symbol.iterator]()
}
var [a, b] = {a: 1, b: 2} // a,b可以正常被赋值

9. [‘1’,‘5’,‘11’].map(parseInt) 为[1,NaN,3][‘1’,‘2’,‘3’].map(parseInt) 为[1, NaN, NaN]

  • Array.map()回调函数的参数:
    element:数组中当前正在处理的元素
    index:正在处理的元素在数组中的索引
    array:调用了 map() 的数组本身

  • parseInt()参数:
    string:要被解析的值
    radix:从 2 到 36 的整数,表示进制的基数。如果不再这个范围,则返回 NaN。如果是 0 ,则表示未指定,基数将会根据字符串的值进行推算(在当前场景下,会被作为 10 进制)

  • ['1','5','11'].map(parseInt)为[1,NaN,3]解析:
    第一次:radix 为 0 ,表示 10 进制,所以 parseInt(‘1’, 0)最后一个省略 返回 1
    第二次:radix 为 1 ,不在 2-10 的范围内,所以 parseInt(‘5’, 1) 返回 NaN
    第三次:radix 为 2 ,表示 2 进制,所以 parseInt(‘11’, 2) 中的 11 被作为 2 进制 ,返回的是 10 进制的结果。即:2进制的 11 转化为 10 进制为 3

  • ['1','2','3'].map(parseInt) 为[1,NaN,NaN]`

10. 数组去重

不考虑数组元素类型、对象可能存在循环引用
如果对象存在循环引用,那就返回一个标识符

function uniqueArray(arr) {
  const seen = new Map();
  const result = [];

  function isObject(item) {
    return typeof item === 'object' && item !== null;
  }

  function stringifyWithCircular(value) {
    const cache = [];

    return JSON.stringify(value, (key, val) => {
      if (isObject(val)) {
        if (cache.includes(val)) {
          // 循环引用,返回一个标识符
          return '[Circular]';
        }
        cache.push(val);
      }
      return val;
    });
  }

  for (const item of arr) {
    const stringifiedItem = stringifyWithCircular(item);
    console.log(stringifiedItem, 'stringifiedItem=')
    if (!result.some(existingItem => stringifiedItem === stringifyWithCircular(existingItem))) {
      result.push(item);
    }
  }

  return result;
}


let obj1 = {a: 1};
let obj2 = {a: 2, self: obj1};
obj1.self = obj2;
let arr = [1,2,{a: 1}, {a: 1}, obj1, obj1, obj2]
console.log(uniqueArray(arr))

11. script标签中defer和async的区别?

css和js加载阻塞问题
脚本文件的下载和执行是与文档解析同步进行,也就是说,它会阻塞文档的解析

  • async:HTML5新增属性,用于异步下载脚本文件,下载完毕立即解释执行代码。
  • defer:用于开启新的线程下载脚本文件,并使脚本在文档解析完成后执行。
  1. script:同步执行 。如果遇到script标签就暂停html的解析,去下载并执行script标签文件。会阻塞页面解析渲染。

  2. script+async:异步下载 同步执行。在html解析的过程中有async的script标签,就异步加载。开启新的线程,用于下载script文件。但是script文件的执行是同步的,执行的过程仍然会阻塞html的解析渲染。

    • 只适用于外联脚本,这一点和defer一致
    • 如果有多个声明了async的脚本,其下载和执行也是异步的,不能确保彼此的先后顺序
    • async会在load事件之前执行,但并不能确保与DOMContentLoaded的执行先后顺序
  3. script+defer:异步加载 延后执行。和async一样,script引用的外部文件是异步加载的,但是defer会将外部文件的执行推迟到html解析结束后再执行。
    关于defer我们需要注意下面几点:

    • defer只适用于外联脚本,如果script标签没有指定src属性,只是内联脚本,不要使用defer
    • 如果有多个声明了defer的脚本,则会按顺序下载和执行
    • defer脚本会在DOMContentLoaded和load事件之前执行

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

  • 所以0.2+0.1因为转为2进制时丢失精度,所以不等于0.3
  • 为什么 0.1 + 0.1 == 0.2 是 true
    0.1转为2进制16位为0.1000000000000000,所以是0.1+0.1=0.2
    因为 64 位浮点数,小数部分最多展示 16 位,因为在 IEEE 754 标准中的 64 位浮点数的小数部分,最多有 53 位, 2 的 53 次方就是 16 位数字,所以小数部分最多展示 16 位。

在 IEEE 754 标准中,浮点数的表示是有限的,而 0.1 和 0.2 在二进制下是无限循环小数。为什么是无限循环呢,这就要了解 10 进制小数如何转换为 2 进制小数了,方法如下:
“乘 2 取整,顺序排列”

  • 用 2 乘十进制小数,可以得到积,将积的整数部分取出
  • 再用 2 乘余下的小数部分,又得到一个积,再将积的整数部分取出
  • 如此进行,直到积中的小数部分为零,或者达到所要求的精度为止
    在这里插入图片描述

如何解决?

  1. 可以将其转换为整数后再进行运算,运算后再转换为对应的小数
var a = 0.1, b = 0.2
var result = (a * 100 + b * 100) / 100
console.log(result) // 0.3
console.log(result === 0.3) // true
  1. 利用 ES6 中的极小数 Number.EPSILON 来进行判断
var a = 0.1, b = 0.2, c = 0.3;
var result = (Math.abs(a + b - c) < Number.EPSILON);
console.log(result) // true
  1. 将数字转化为字符串,然后模拟加法运算
function addStrings(num1, num2) {
    // 分割整数部分和小数部分
    let [int1, dec1] = num1.split('.');
    let [int2, dec2] = num2.split('.');
    // 确保小数部分不为 undefined
    dec1 = dec1 || '';
    dec2 = dec2 || '';
    // 让两个小数部分的长度相同
    let maxDecLen = Math.max(dec1.length, dec2.length);
    dec1 = dec1.padEnd(maxDecLen, '0');
    dec2 = dec2.padEnd(maxDecLen, '0');
    // 反转小数部分进行加法运算
    let carry = 0;
    let decimalResult = [];
    for (let i = maxDecLen - 1; i >= 0; i--) {
        let sum = parseInt(dec1[i], 10) + parseInt(dec2[i], 10) + carry;
        carry = Math.floor(sum / 10);
        decimalResult.push(sum % 10);
    }
    decimalResult = decimalResult.reverse().join('').replace(/0+$/, ''); // 去掉尾部多余的零
    // 反转整数部分进行加法运算
    let intResult = [];
    int1 = int1.split('').reverse().join('');
    int2 = int2.split('').reverse().join('');
    let maxIntLen = Math.max(int1.length, int2.length);
    for (let i = 0; i < maxIntLen; i++) {
        let digit1 = parseInt(int1[i] || '0', 10);
        let digit2 = parseInt(int2[i] || '0', 10);

        let sum = digit1 + digit2 + carry;
        carry = Math.floor(sum / 10);
        intResult.push(sum % 10);
    }
    if (carry) {
        intResult.push(carry);
    }
    intResult = intResult.reverse().join('');
    // 组合整数部分和小数部分
    let result = intResult;
    if (decimalResult) {
        result += '.' + decimalResult;
    }

    return result;
}
console.log(addStrings("0.1", "0.2")); // 输出: "0.3"

13. 准确的倒计时

function example5(leftTime) {
    const now = performance.now();
    function start() {
        setTimeout(() => {
            const diff = leftTime - (performance.now() - now);
            console.log(diff);
            if(diff > 0) {
                requestAnimationFrame(start);
            } else {
                console.log('Time is up!');
            }
        }, 1000);
    }
    start();
}
example5(5000)

没有解决setTimeout会延迟的问题,当线程被占用之后,很容易出现误差
更精准的倒计时&计时器组件

14. let const var区别

  • var 声明是全局作用域或函数作用域,而 let 和 const 是块作用域。
  • var 变量可以在其作用域内更新和重新声明;let 变量可以更新但不能重新声明;const 变量既不能更新也不能重新声明。
  • var 和 let 可以在不初始化的情况下声明,而 const 必须在声明时初始化
  • let const 存在暂时死区,在定义之前不可以使用
  • var在全局作用域声明的变量有一种行为会挂载在window对象上,let和const不会
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值