JavaScript中10个让人困惑的特性

摘要:JavaScript是一门有趣的语言,但有些特性确实让人摸不着头脑。即使是有经验的开发者,也可能会被这些特性坑到。今天我们来聊聊JavaScript中10个比较特别的现象。

JavaScript是一门有趣的语言,但有些特性确实让人摸不着头脑。即使是有经验的开发者,也可能会被这些特性"坑"到。今天我们来聊聊JavaScript中10个比较特别的现象。

1. NaN:自己不等于自己

NaN表示"不是一个数字",但它有个很奇怪的特点:

console.log(Nan === NaN); // false
console.log(Nan == NaN);  // false

为什么会出现这种情况?因为NaN代表一个无法表示的数值结果,比如0除以0。既然两个"未定义的结果"不能保证相同,它们就不应该相等。

检测NaN的正确方法:

console.log(Number.isNaN(NaN)); // true

// 不要用全局的isNaN
console.log(isNaN("hello")); // true,因为它会先把"hello"转成数字
console.log(Number.isNaN("hello")); // false

2. 数组长度可以手动修改

数组的length属性不只是用来查看的,还可以修改:

let fruits = ['苹果', '香蕉', '橙子'];

// 增加长度会创建空位
fruits.length = 5;
console.log(fruits); // ['苹果', '香蕉', '橙子', empty × 2]

// 减少长度会删除元素
fruits.length = 1;
console.log(fruits); // ['苹果'] - 其他水果被删掉了

空位和undefined不一样:

let sparse = new Array(3); // [empty, empty, empty]
let explicit = [undefined, undefined, undefined];

console.log(sparse.map(() => '填充')); // 还是 [empty, empty, empty]
console.log(explicit.map(() => '填充')); // ['填充', '填充', '填充']

3. 看起来像数组但不是数组

有些对象看起来像数组,但不能用数组的方法:

function test() {
  console.log(Array.isArray(arguments)); // false
  // arguments.map(...) // 会报错
}

转换方法:

function betterTest() {
  const args = [...arguments]; // 方法一
  // const args = Array.from(arguments); // 方法二
  return args.map(x => x * 2);
}

4. 小数计算不精确

这是计算机浮点数的通病,不是JavaScript独有的:

console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false

解决方法:

// 对于大整数,用BigInt
const bigNum = BigInt(Number.MAX_SAFE_INTEGER);

// 对于小数,可以转成整数计算
function addDecimals(a, b) {
  const multiplier = 100; // 根据精度需要调整
  return (a * multiplier + b * multiplier) / multiplier;
}

// 或者用toFixed显示
console.log((0.1 + 0.2).toFixed(2)); // "0.30"

5. 对象的冻结是浅层的

Object.freeze()只能冻结第一层属性:

const frozenObj = Object.freeze({
  name: '不可变',
  details: { version: 1 }
});

// 不能修改第一层
// frozenObj.name = '新名字' // 报错

// 但可以修改内层对象
frozenObj.details.version = 2; // 这样可以

如果需要深度冻结,需要自己实现:

function deepFreeze(obj) {
  Object.freeze(obj);
  Object.getOwnPropertyNames(obj).forEach(prop => {
    if (obj[prop] !== null && 
        typeof obj[prop] === 'object' && 
        !Object.isFrozen(obj[prop])) {
      deepFreeze(obj[prop]);
    }
  });
  return obj;
}

6. 原型污染风险

如果不小心修改了Object.prototype,会影响所有对象:

// 危险操作示例
const badData = JSON.parse('{"__proto__": {"isHacked": true}}');
let target = {};

// 某些库可能会这样合并对象
if (badData.__proto__) {
  Object.assign(Object.prototype, badData.__proto__);
}

const cleanObj = {};
console.log(cleanObj.isHacked); // true - 所有对象都被影响了

防护措施:

// 创建没有原型的对象
const safeObj = Object.create(null);

// 对输入数据做检查
function sanitizeInput(data) {
  if (data && data.__proto__) {
    throw new Error('危险数据');
  }
  return data;
}

7. 类型转换的陷阱

+运算符遇到对象时会进行类型转换:

console.log([] + {}); // "[object Object]"
// 相当于 "" + "[object Object]"

console.log({} + []); // 0 或 "[object Object]",取决于环境

建议做法:

// 总是用===
console.log(0 == '0'); // true
console.log(0 === '0'); // false

// 显式转换
const num = Number('123');
const str = String(123);

8. 变量提升和暂时性死区

不同类型的变量提升方式不同:

// 函数声明 - 完全提升
sayHi(); // "你好!"
function sayHi() { console.log("你好!"); }

// var - 声明提升,值是undefined
console.log(myVar); // undefined
var myVar = "值";

// let/const - 有暂时性死区
console.log(myLet); // 报错
let myLet = "值";

9. this的指向问题

this的值取决于调用方式:

const obj = {
  name: "测试对象",
  oldMethod: function() {
    setTimeout(function() {
      console.log(this.name); // undefined
    }, 100);
  },
  newMethod: function() {
    setTimeout(() => {
      console.log(this.name); // "测试对象"
    }, 100);
  }
};

在类中使用箭头函数:

class Button {
  constructor(text) {
    this.text = text;
  }
  
  // 自动绑定this
  handleClick = () => {
    console.log(this.text);
  };
}

const btn = new Button('点击我');
document.getElementById('btn').addEventListener('click', btn.handleClick);

10. Symbol的用途

Symbol创建唯一的值,适合做"私有"属性:

const secretKey = Symbol('secret');

class SafeBox {
  constructor() {
    this[secretKey] = '机密数据';
  }
  
  getSecret() {
    return this[secretKey];
  }
}

const box = new SafeBox();
console.log(box.getSecret()); // "机密数据"

// 这些方式访问不到secretKey
for (let key in box) console.log(key); // 没有输出
console.log(JSON.stringify(box)); // "{}"

还可以自定义对象行为:

const counter = {
  value: 0,
  [Symbol.iterator]() {
    return {
      next: () => {
        return {
          value: this.value++,
          done: this.value > 5
        };
      }
    };
  }
};

for (let num of counter) {
  console.log(num); // 0, 1, 2, 3, 4
}

实用建议

  1. 总是使用=== 避免类型转换的意外

  2. 理解变量提升 把声明放在使用之前

  3. 小心小数运算 必要时转成整数计算

  4. 使用现代语法 箭头函数、let/const等

  5. 代码检查 使用ESLint发现潜在问题

// 好的实践
const calculatePrice = (price, quantity) => {
  // 转成整数避免小数问题
  const total = Math.round(price * 100) * quantity;
  return total / 100;
};

// 明确的类型检查
if (typeof value === 'number' && !Number.isNaN(value)) {
  // 安全操作
}

JavaScript的这些特性虽然有时候让人困惑,但理解之后就能更好地避免问题。希望这些例子能帮助你在开发中少踩一些坑!

本文内容仅供个人学习、研究或参考使用,不构成任何形式的决策建议、专业指导或法律依据。未经授权,禁止任何单位或个人以商业售卖、虚假宣传、侵权传播等非学习研究目的使用本文内容。如需分享或转载,请保留原文来源信息,不得篡改、删减内容或侵犯相关权益。感谢您的理解与支持!

链接: https://shenqiku.cn/article/FLY_13179