JavaScript 的反射指的是程式在執行期檢視、操作自身結構的能力——例如列舉物件的所有屬性與方法、動態呼叫函式。JS 原生提供 for...in 迴圈作為最基礎的反射工具。本文詳細說明 for...in 行為、原型鏈影響、屬性枚舉屬性、現代替代 API (Object.keys、Reflect.ownKeys) 以及實務應用:動態樣式套用。
for (var prop in obj) {
// prop: 字串,屬性名稱 (包含繼承自原型鏈的可枚舉屬性)
// obj[prop]: 對應的值
}
| 特性 | 說明 |
|---|---|
| 遍歷順序 | ES2015+ 規範:整數索引升序 → 字串鍵建立順序 → Symbol 鍵建立順序。舊引擎可能不同。 |
| 包含繼承屬性 | 是,會遍歷原型鏈上所有 enumerable: true 的屬性。 |
| 排除不可枚舉 | 內建方法 (如 toString、valueOf) 預設 enumerable: false,不會出現。 |
| Symbol 屬性 | 不會 被 for...in 遍歷。 |
| 刪除/新增屬性 | 迴圈中刪除屬性不會遍歷到;新增屬性可能或不會遍歷到 (視引擎)。 |
var obj = {
name: "UEditor",
version: "1.4.3",
render: function() { console.log("rendering..."); },
config: { theme: "default" }
};
// 手動加入原型屬性 (示範繼承)
Object.prototype.lib = "baidu";
for (var p in obj) {
var val = obj[p];
if (typeof val === "function") {
console.log(`方法: ${p}`);
// val(); // 動態呼叫
} else {
console.log(`屬性: ${p} = ${JSON.stringify(val)}`);
}
}
/* 輸出:
屬性: name = "UEditor"
屬性: version = "1.4.3"
方法: render
屬性: config = {"theme":"default"}
屬性: lib = "baidu" ← 來自 Object.prototype!
*/
for (var p in obj) {
if (obj.hasOwnProperty(p)) { // 僅處理物件自身屬性
console.log(p, obj[p]);
}
}
// 或現代寫法
Object.keys(obj).forEach(p => console.log(p, obj[p]));
| API | 回傳 | 包含繼承 | 包含 Symbol | 包含不可枚舉 |
|---|---|---|---|---|
for...in |
迴圈 | 是 | 否 | 否 |
Object.keys(obj) |
string[] | 否 | 否 | 否 |
Object.getOwnPropertyNames(obj) |
string[] | 否 | 否 | 是 |
Object.getOwnPropertySymbols(obj) |
Symbol[] | 否 | 是 | 否 |
Reflect.ownKeys(obj) |
(string|Symbol)[] | 否 | 是 | 是 |
Object.entries(obj) |
[key, value][] | 否 | 否 | 否 |
函式接收樣式物件,需套用至 DOM 元素。若直接 element.style = styleObj,會覆蓋元素原有的所有內建樣式,導致未指定的樣式 (如 height) 遺失。
function setStyleBad(element, styleObj) {
element.style = styleObj; // 錯誤:style 唯讀,且會整個替換
}
function setStyle(element, styleObj) {
if (!element || !styleObj) return;
for (var prop in styleObj) {
if (styleObj.hasOwnProperty(prop)) {
// 瀏覽器自動處理 camelCase ↔ kebab-case 轉換
// 如 backgroundColor → background-color
element.style[prop] = styleObj[prop];
}
}
}
// 使用範例
var myStyle = {
color: "#fff",
backgroundColor: "#f00", // JS 用駝峰
borderWidth: "2px",
fontSize: "14px"
};
var el = document.getElementById("myDiv");
setStyle(el, myStyle);
// 僅更新上述 4 個屬性,原有 height、margin 等保持不變
function setStyleFlexible(element, styleObj) {
for (var key in styleObj) {
if (!styleObj.hasOwnProperty(key)) continue;
var value = styleObj[key];
// 將 kebab-case 轉駝峰
var jsKey = key.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
element.style[jsKey] = value;
}
}
// 現在可直接用 CSS 鍵名
setStyleFlexible(el, {
"color": "#fff",
"background-color": "#f00",
"border-width": "2px"
});
function mixin(target, source) {
for (var key in source) {
if (source.hasOwnProperty(key)) {
target[key] = source[key];
}
}
return target;
}
var base = { init() { console.log("base init"); } };
var feature = {
doSomething() { console.log("feature"); },
prop: 123
};
var combined = mixin(Object.create(base), feature);
combined.init(); // base init
combined.doSomething(); // feature
for...in:遍歷原型鏈較慢,優先用 Object.keys / Object.entries。hasOwnProperty:除非刻意要包含繼承屬性。for...in:會遍歷索引字串、原型方法、順序不保證。請用 for...of、forEach、for (let i=0; i<len; i++)。obj[prop] 是反射的核心,配合 typeof 判斷函式/值。with 陳述式被禁用,反射更依賴 for...in / Object.* API。for...in,能遍歷物件可枚舉屬性(含繼承)。hasOwnProperty 過濾自有屬性,或改用 Object.keys 等現代 API。element.style[prop],避免整體覆蓋。enumerable、Symbol、原型鏈對遍歷的影響,是撰寫健壯動態程式碼的關鍵。