一、URL 编码:encodeURI 和 encodeURIComponent

在开发中,URL 是我们经常要处理的内容。URL 中不能包含某些特殊字符(比如空格或中文),否则可能导致解析错误或传输失败。JavaScript 提供了两个函数来解决这个问题:encodeURIencodeURIComponent

它们是做什么的?

简单来说,这两个函数的作用是把特殊字符转成安全的编码格式,确保 URL 能被正确传输和解析。比如,空格会被编码成 %20,中文字符会被转成类似 %E5%BC%A0 的形式。

主要区别

两者的核心区别在于编码的范围:

  • encodeURI:适合编码整个 URL,会保留 URL 结构中必要的特殊字符,比如 :, /, ?, &, = 等。
  • encodeURIComponent:编码更彻底,连这些特殊字符也会被编码,适合用来处理 URL 的参数值。

实际例子

来看一个例子,假设我们要处理一个带中文和特殊字符的 URL:

// 编码整个 URL
const url = 'https://搜索.com/path with spaces?name=张三'
const encodedUrl = encodeURI(url)
console.log(encodedUrl)
// 输出:https://%E6%90%9C%E7%B4%A2.com/path%20with%20spaces?name=%E5%BC%A0%E4%B8%89

// 编码参数值
const param = '张三&李四=朋友'
const encodedParam = encodeURIComponent(param)
console.log(encodedParam)
// 输出:%E5%BC%A0%E4%B8%89%26%E6%9D%8E%E5%9B%9B%3D%E6%9C%8B%E5%8F%8B

正确使用场景

  • **用 **encodeURI:当你需要构建完整的 URL,比如拼接域名和路径时。
  • **用 **encodeURIComponent:当你需要编码 URL 的参数值,比如查询字符串中的值。

错误和正确的用法对比:

const baseUrl = 'https://api.example.com/search'
const searchTerm = '苹果 & 香蕉'

// 错误:直接用 encodeURI 编码参数值
const wrongUrl = `${baseUrl}?q=${encodeURI(searchTerm)}`
console.log(wrongUrl)
// 输出:https://api.example.com/search?q=苹果 & 香蕉
// 问题:& 没有被编码,可能导致参数解析错误

// 正确:用 encodeURIComponent 编码参数值
const correctUrl = `${baseUrl}?q=${encodeURIComponent(searchTerm)}`
console.log(correctUrl)
// 输出:https://api.example.com/search?q=%E8%8B%B9%E6%9E%9C%20%26%20%E9%A6%99%E8%95%89

小技巧

如果需要解码,可以用对应的 decodeURIdecodeURIComponent

const decoded = decodeURIComponent('%E8%8B%B9%E6%9E%9C')
console.log(decoded) // 输出:苹果

建议:在实际开发中,总是用 encodeURIComponent 处理参数值,用 encodeURI 处理完整 URL,这样能避免大多数编码问题。


二、赋值与比较:=、== 和 === 的区别

JavaScript 中的 ====== 是三种完全不同的运算符,搞清楚它们的区别能帮你写出更可靠的代码。

=:赋值运算符

= 是用来给变量赋值的,就像把东西放进一个盒子里:

let name = '小明' // 把 '小明' 放进 name 变量
let age = 25      // 把 25 放进 age 变量

简单明了,右边的值被存储到左边的变量中。

==:相等运算符

== 用来比较两个值是否相等,但它会在比较前进行类型转换。这种类型转换可能会带来意想不到的结果:

console.log(5 == '5')    // true,字符串 '5' 被转成数字 5
console.log(0 == '')     // true,空字符串被转成 0
console.log(0 == false)  // true,false 被转成 0

这种“宽松”的比较可能导致 bug,特别是在复杂逻辑中。

===:严格相等运算符

=== 不仅比较值是否相等,还会检查类型是否相同,不进行类型转换:

console.log(5 === '5')   // false,类型不同(number vs string)
console.log(0 === '')    // false,类型不同
console.log(0 === false) // false,类型不同

实际例子

来看一个综合的例子:

let a = 10
let b = '10'

if (a == b) {
  console.log('a == b 成立') // 会执行
}

if (a === b) {
  console.log('a === b 成立') // 不会执行
}

a = 20 // 重新赋值
console.log(a) // 输出:20

使用建议

  • **优先用 **===:它的行为更可预测,能减少 bug,代码意图也更清晰。
  • **避免用 **==:除非你明确需要类型转换的特性,否则不要用它。
  • 明确赋值场景= 只用于赋值,别和比较运算符混淆。

三、undefined 和 null:它们有何不同?

在 JavaScript 中,undefinednull 都表示“没有值”,但它们的含义和使用场景有明显区别。

基本概念

  • undefined:表示变量或属性“未定义”,是 JavaScript 自动赋予的原始状态。通常出现在:
    • 声明但未赋值的变量
    • 对象中不存在的属性
    • 函数没有返回值时的默认返回
  • null:表示“有意为空”,通常由开发者主动设置,表示这里本应有对象,但现在没有。

常见场景

undefined 的例子

// 1. 变量声明但未赋值
let name
console.log(name) // undefined

// 2. 函数没有返回值
function sayHi() {
  console.log('嗨')
}
let result = sayHi() // result 是 undefined

// 3. 访问不存在的属性
const user = { name: '小明' }
console.log(user.age) // undefined

// 4. 函数参数未传值
function greet(name) {
  console.log('你好,' + name)
}
greet() // 输出:你好,undefined

null 的例子

// 1. 主动表示“没有对象”
let person = null // 表示目前没有对象

// 2. 清除引用
let user = { name: '小明' }
user = null // 表示不再需要这个对象

// 3. 函数返回值表示特定含义
function findUser(id) {
  return null // 表示没找到用户
}

类型和比较

有趣的是,typeof null 返回 'object'(这是 JavaScript 的一个历史遗留问题)。而 undefined 的类型是 'undefined'

console.log(typeof undefined) // 'undefined'
console.log(typeof null)      // 'object'

console.log(null == undefined)  // true,宽松比较会转换
console.log(null === undefined) // false,严格比较类型不同

实用技巧

  • 默认值处理:可以用空值合并运算符 ?? 来设置默认值,只有 nullundefined 会触发:
function greet(name) {
  name = name ?? '访客'
  console.log('你好,' + name)
}
greet() // 输出:你好,访客
  • 检查空值:想判断变量是否为 nullundefined,可以用:
if (value == null) {
  // value 是 null 或 undefined
}

建议

  • undefined 表示系统层面的“未定义”状态,比如变量未赋值。
  • null 表示程序逻辑中的“有意为空”,比如对象被清空或查找失败。
  • 在现代 JavaScript 中,尽量明确区分两者的用途,代码会更清晰。

四、浮点数精度问题:为什么 0.1 + 0.2 不等于 0.3?

在 JavaScript 中,运行 0.1 + 0.2 会得到 0.30000000000000004,而不是预期的 0.3。这是为什么?

原因

计算机用二进制存储数字,而十进制的小数(如 0.10.2)在二进制中可能是无限循环小数。比如:

  • 0.1 在二进制中是 0.0001100110011...(无限循环)
  • 0.2 在二进制中是 0.001100110011...(无限循环)

JavaScript 使用 IEEE 754 标准的 64 位浮点数存储数字,精度有限,只能截断这些无限小数。这就导致计算时会产生微小的误差。

实际演示

console.log(0.1 + 0.2) // 输出:0.30000000000000004
console.log(0.1 + 0.2 === 0.3) // false

解决方案

  1. toFixed 处理显示
let sum = 0.1 + 0.2
console.log(sum.toFixed(1)) // 输出:"0.3"(字符串)
  1. 用误差范围比较
let sum = 0.1 + 0.2
let epsilon = 0.00000001
console.log(Math.abs(sum - 0.3) < epsilon) // true
  1. 转成整数计算
let result = (0.1 * 10 + 0.2 * 10) / 10
console.log(result) // 输出:0.3
  1. 使用精确计算库:对于金融等高精度场景,推荐用 decimal.jsbig.js
import Big from 'big.js'

const a = new Big(0.1)
const b = new Big(0.2)
const c = a.plus(b)
console.log(c.toString()) // 输出:"0.3"

建议:在涉及精确计算(尤其是金融场景)时,优先使用整数计算或专门的库,避免直接操作浮点数。


五、var、let 和 const:作用域与声明的差异

JavaScript 中的变量声明方式有 varletconst,它们在作用域、变量提升和赋值行为上各有不同。

作用域区别

  • var:函数作用域,声明的变量在整个函数内可见。
  • let** 和 **const:块级作用域,只在声明的代码块(如 {})内有效。
function example() {
  if (true) {
    var x = 10
    let y = 20
    const z = 30
  }
  console.log(x) // 输出:10
  console.log(y) // 报错:y 未定义
  console.log(z) // 报错:z 未定义
}

变量提升

  • var:存在变量提升,声明会被“提升”到函数顶部,但值是 undefined
  • let** 和 **const:没有变量提升,访问前会报错。
console.log(name) // 输出:undefined
var name = '小明'

console.log(age) // 报错:无法在声明前访问
let age = 25

重复声明

  • var:允许重复声明,会覆盖之前的值。
  • let** 和 **const:不允许重复声明。
var hobby = '篮球'
var hobby = '足球' // 没问题,值为 '足球'

let color = 'red'
let color = 'blue' // 报错:不能重复声明

值的修改

  • var** 和 **let:允许修改值。
  • const:不能重新赋值,但如果存储的是对象,对象的属性可以修改。
var count = 1
count = 2 // 正常

let score = 90
score = 95 // 正常

const API_URL = 'https://api.example.com'
API_URL = 'https://api.test.com' // 报错

const user = { name: '张三' }
user.name = '李四' // 正常
user = {} // 报错

使用建议

  • **优先用 **const:适合不变的值,代码更安全。
  • **用 **let:需要重新赋值的变量。
  • **尽量避免 **var:它的变量提升和函数作用域可能导致难以调试的 bug。

最佳实践:遵循“能用 const 就用 const,不行再用 let”的原则,基本不用 var


六、立即执行函数(IIFE):隔离作用域的利器

立即执行函数(Immediately Invoked Function Expression,IIFE)是一种定义后立即运行的函数。它的形式简单却用途广泛,尤其在老代码和特定场景中非常常见。

什么是 IIFE?

IIFE 是一个函数表达式,定义后立刻被调用,形如:

(function() {
  const message = '我会立即执行'
  console.log(message)
})()

外层的圆括号让 JavaScript 解析器将函数视为表达式,而不是声明,从而可以直接调用。

其他写法

IIFE 有几种等效的写法:

// 形式一
(function() {
  // 代码
})()

// 形式二
(function() {
  // 代码
}())

// 箭头函数形式
(() => {
  // 代码
})()

为什么用 IIFE?

  1. 创建独立作用域
    在 ES6 之前,JavaScript 只有函数作用域和全局作用域,IIFE 可以避免变量污染全局:
(function() {
  const user = '张三' // 不会污染全局
  console.log(user)
})()
console.log(user) // 报错:user 未定义
  1. 模块化开发
    IIFE 是早期模块化的基础,通过返回对象暴露公共接口:
const myModule = (function() {
  const privateVar = '这是私有的'
  
  function privateMethod() {
    console.log('私有方法')
  }
  
  return {
    publicMethod: function() {
      console.log(privateVar)
      privateMethod()
    }
  }
})()

myModule.publicMethod() // 输出:这是私有的 私有方法
  1. 保存闭包状态
    IIFE 可以用来保存循环中的变量状态,解决经典的闭包问题:
const buttons = document.querySelectorAll('button')
for (let i = 0; i < buttons.length; i++) {
  buttons[i].onclick = (function(index) {
    return function() {
      console.log('点击了第 ' + index + ' 个按钮')
    }
  })(i)
}

实际应用场景

  • 避免全局变量冲突:在引入多个第三方库时,IIFE 可以隔离命名空间,像 jQuery 就用类似方式实现。
  • 一次性初始化:比如创建计数器模块:
const counter = (function() {
  let count = 0
  return {
    increment: function() {
      return ++count
    },
    getValue: function() {
      return count
    }
  }
})()

console.log(counter.getValue()) // 0
counter.increment()
console.log(counter.getValue()) // 1

现代替代方案

随着 ES6 的普及,letconst 和模块系统(如 import/export)已经取代了许多 IIFE 的场景。但在老代码或需要隔离作用域的场景中,IIFE 仍然非常实用。


总结与最佳实践

通过本文,我们深入探讨了 JavaScript 中几个关键概念的原理和应用场景。以下是核心建议:

  1. URL 编码:用 encodeURI 处理完整 URL,用 encodeURIComponent 处理参数值,确保 URL 安全可靠。
  2. 比较运算符:优先用 === 进行严格比较,避免 == 带来的意外 bug。
  3. undefined 和 null:明确区分两者的用途,用 null 表示程序逻辑的“有意为空”,用 undefined 表示系统未定义状态。
  4. 浮点数精度:处理小数运算时,优先用整数计算或精确计算库,避免直接操作浮点数。
  5. 变量声明:用 constlet 替代 var,遵循“能用 const 就用 const”的原则。
  6. 立即执行函数:在需要隔离作用域或模块化时使用 IIFE,现代开发中可结合 ES6 模块系统。