一、URL 编码:encodeURI 和 encodeURIComponent
在开发中,URL 是我们经常要处理的内容。URL 中不能包含某些特殊字符(比如空格或中文),否则可能导致解析错误或传输失败。JavaScript 提供了两个函数来解决这个问题:encodeURI
和 encodeURIComponent
。
它们是做什么的?
简单来说,这两个函数的作用是把特殊字符转成安全的编码格式,确保 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
小技巧
如果需要解码,可以用对应的 decodeURI
和 decodeURIComponent
:
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 中,undefined
和 null
都表示“没有值”,但它们的含义和使用场景有明显区别。
基本概念
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,严格比较类型不同
实用技巧
- 默认值处理:可以用空值合并运算符
??
来设置默认值,只有 null
或 undefined
会触发:
function greet(name) {
name = name ?? '访客'
console.log('你好,' + name)
}
greet() // 输出:你好,访客
- 检查空值:想判断变量是否为
null
或 undefined
,可以用:
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.1
和 0.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
解决方案
- 用
toFixed
处理显示:
let sum = 0.1 + 0.2
console.log(sum.toFixed(1)) // 输出:"0.3"(字符串)
- 用误差范围比较:
let sum = 0.1 + 0.2
let epsilon = 0.00000001
console.log(Math.abs(sum - 0.3) < epsilon) // true
- 转成整数计算:
let result = (0.1 * 10 + 0.2 * 10) / 10
console.log(result) // 输出:0.3
- 使用精确计算库:对于金融等高精度场景,推荐用
decimal.js
或 big.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 中的变量声明方式有 var
、let
和 const
,它们在作用域、变量提升和赋值行为上各有不同。
作用域区别
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?
- 创建独立作用域:
在 ES6 之前,JavaScript 只有函数作用域和全局作用域,IIFE 可以避免变量污染全局:
(function() {
const user = '张三' // 不会污染全局
console.log(user)
})()
console.log(user) // 报错:user 未定义
- 模块化开发:
IIFE 是早期模块化的基础,通过返回对象暴露公共接口:
const myModule = (function() {
const privateVar = '这是私有的'
function privateMethod() {
console.log('私有方法')
}
return {
publicMethod: function() {
console.log(privateVar)
privateMethod()
}
}
})()
myModule.publicMethod() // 输出:这是私有的 私有方法
- 保存闭包状态:
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 的普及,let
、const
和模块系统(如 import
/export
)已经取代了许多 IIFE 的场景。但在老代码或需要隔离作用域的场景中,IIFE 仍然非常实用。
总结与最佳实践
通过本文,我们深入探讨了 JavaScript 中几个关键概念的原理和应用场景。以下是核心建议:
- URL 编码:用
encodeURI
处理完整 URL,用 encodeURIComponent
处理参数值,确保 URL 安全可靠。
- 比较运算符:优先用
===
进行严格比较,避免 ==
带来的意外 bug。
- undefined 和 null:明确区分两者的用途,用
null
表示程序逻辑的“有意为空”,用 undefined
表示系统未定义状态。
- 浮点数精度:处理小数运算时,优先用整数计算或精确计算库,避免直接操作浮点数。
- 变量声明:用
const
和 let
替代 var
,遵循“能用 const
就用 const
”的原则。
- 立即执行函数:在需要隔离作用域或模块化时使用 IIFE,现代开发中可结合 ES6 模块系统。