JavaScript 的 `escape()` 函數雖已棄用(建議用 `encodeURIComponent`),但許多舊系統、API 簽名、第三方整合仍依賴此編碼格式。本文提供純 PHP 實作的 `escape()` 相容函數,支援 Unicode 補充平面、UTF-8 多位元組、大小端序處理。
| 字元範圍 | 編碼格式 | 範例 |
|---|---|---|
| ASCII 字母數字、@ * _ + - . / | 不編碼 | `a` `1` `-` `_` |
| 其他 ASCII (32-126) | `%XX` (2 位十六進位) | 空格 `%20`、`#` → `%23` |
| Unicode BMP (U+0000 ~ U+FFFF) | `%uXXXX` (4 位十六進位) | `中` → `%u4E2D` |
| Unicode 補充平面 (U+10000+) | `%uXXXX%uXXXX` (Surrogate Pair) | `😀` → `%uD83D%uDE00` |
/**
* PHP 實現 JavaScript escape() 編碼
* @param string $string UTF-8 編碼字串
* @return string escape 編碼後字串
*/
function js_escape($string) {
// 不需編碼的字元集合
$safeChars = '-_.+@/*0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$len = strlen($string);
$output = '';
$byteIndex = 0; // 位元組索引
$charIndex = 0; // 字元索引(處理多位元組)
while ($byteIndex < $len) {
$firstByte = ord($string[$byteIndex]);
$charBytes = 1;
$codePoint = 0;
// 判斷 UTF-8 字元位元組長度並解碼 code point
if ($firstByte < 0x80) { // 1 byte: 0xxxxxxx (ASCII)
$codePoint = $firstByte;
$charBytes = 1;
} elseif (($firstByte & 0xE0) === 0xC0) { // 2 bytes: 110xxxxx 10xxxxxx
$codePoint = (($firstByte & 0x1F) << 6) | (ord($string[$byteIndex+1]) & 0x3F);
$charBytes = 2;
} elseif (($firstByte & 0xF0) === 0xE0) { // 3 bytes: 1110xxxx 10xxxxxx 10xxxxxx
$codePoint = (($firstByte & 0x0F) << 12) |
((ord($string[$byteIndex+1]) & 0x3F) << 6) |
(ord($string[$byteIndex+2]) & 0x3F);
$charBytes = 3;
} elseif (($firstByte & 0xF8) === 0xF0) { // 4 bytes: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
$codePoint = (($firstByte & 0x07) << 18) |
((ord($string[$byteIndex+1]) & 0x3F) << 12) |
((ord($string[$byteIndex+2]) & 0x3F) << 6) |
(ord($string[$byteIndex+3]) & 0x3F);
$charBytes = 4;
} else {
// 無效 UTF-8 序列,跳過 1 byte
$byteIndex++;
continue;
}
// 取得原始字元(用於安全字元判斷)
$rawChar = substr($string, $byteIndex, $charBytes);
// 編碼邏輯
if ($codePoint < 0x100 && strpos($safeChars, $rawChar) !== false) {
// ASCII 安全字元:直接輸出
$output .= $rawChar;
} elseif ($codePoint < 0x100) {
// ASCII 非安全字元:%XX
$output .= '%' . strtoupper(dechex($codePoint));
} elseif ($codePoint <= 0xFFFF) {
// BMP 範圍:%uXXXX
$output .= '%u' . strtoupper(str_pad(dechex($codePoint), 4, '0', STR_PAD_LEFT));
} else {
// 補充平面:Surrogate Pair → %uXXXX%uXXXX
$codePoint -= 0x10000;
$highSurrogate = 0xD800 | (($codePoint >> 10) & 0x3FF);
$lowSurrogate = 0xDC00 | ($codePoint & 0x3FF);
$output .= '%u' . strtoupper(dechex($highSurrogate));
$output .= '%u' . strtoupper(dechex($lowSurrogate));
}
$byteIndex += $charBytes;
}
return $output;
}
/**
* 反向解碼:unescape()
*/
function js_unescape($string) {
// 先處理 %uXXXX
$string = preg_replace_callback('/%u([0-9A-Fa-f]{4})/', function($m) {
return mb_convert_encoding(hex2bin($m[1]), 'UTF-8', 'UCS-2BE');
}, $string);
// 再處理 %XX
$string = preg_replace_callback('/%([0-9A-Fa-f]{2})/', function($m) {
return chr(hexdec($m[1]));
}, $string);
return $string;
}
$testCases = [
'Hello World!' => 'Hello%20World%21',
'中文測試' => '%u4E2D%u6587%u6E2C%u8A66',
'😀🎉 Emoji' => '%uD83D%uDE00%uD83C%uDF89%20Emoji',
'email@example.com' => 'email@example.com', // 安全字元不編碼
'path/to/file.php' => 'path/to/file.php', // / 是安全字元
'special chars: #$%^&*' => 'special%20chars%3A%20%23%24%25%5E%26%2A',
];
foreach ($testCases as $input => $expected) {
$encoded = js_escape($input);
$decoded = js_unescape($encoded);
$status = ($encoded === $expected && $decoded === $input) ? '✅ PASS' : '❌ FAIL';
echo "$status\nInput: $input\nEncoded: $encoded\nExpected: $expected\nDecoded: $decoded\n\n";
}
function js_escape_fast($string) {
// 使用 mbstring 逐字元處理,效能優於手動位元運算
$safeChars = '-_.+@/*0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$output = '';
$length = mb_strlen($string, 'UTF-8');
for ($i = 0; $i < $length; $i++) {
$char = mb_substr($string, $i, 1, 'UTF-8');
$codePoint = mb_ord($char, 'UTF-8'); // PHP 7.2+
if ($codePoint < 0x100 && strpos($safeChars, $char) !== false) {
$output .= $char;
} elseif ($codePoint < 0x100) {
$output .= '%' . strtoupper(dechex($codePoint));
} elseif ($codePoint <= 0xFFFF) {
$output .= '%u' . strtoupper(str_pad(dechex($codePoint), 4, '0', STR_PAD_LEFT));
} else {
$codePoint -= 0x10000;
$output .= '%u' . strtoupper(dechex(0xD800 | (($codePoint >> 10) & 0x3FF)));
$output .= '%u' . strtoupper(dechex(0xDC00 | ($codePoint & 0x3FF)));
}
}
return $output;
}
JavaScript `escape()` 雖被標記為 Legacy,但在存量系統整合中仍頻繁出現。掌握其編碼規則(特別是 Surrogate Pair 處理),並能用 PHP 精確實現雙向轉換,是後端工程師處理跨語言資料交換的基本功。