JavaScript 中的 SetDataProperty 抽象操作:探讨属性赋值、查找与描述符的底层逻辑

在JavaScript的底层机制中,对象的属性操作是核心且频繁的行为。无论是简单的 obj.prop = value 赋值,还是通过 Object.defineProperty 精细控制属性,其背后都有一系列抽象操作在默默执行。其中,SetDataProperty 抽象操作扮演着至关重要的角色,它是JavaScript引擎内部处理数据属性赋值请求的关键一环。理解 SetDataProperty 不仅能加深我们对JavaScript对象模型、属性描述符和原型链交互的理解,还能帮助我们编写更健壮、可预测的代码。

本讲座将深入探讨 SetDataProperty 的底层逻辑,从ECMAScript规范的视角出发,解析其工作原理、与相关抽象操作的协作、以及在不同场景下对属性赋值行为的影响。


ECMAScript 规范中的抽象操作与属性描述符

在深入 SetDataProperty 之前,我们必须先建立一些基础概念。

抽象操作 (Abstract Operations)
ECMAScript规范定义了一系列抽象操作,它们是JavaScript引擎内部使用的低级函数,用于描述语言的行为。这些操作不是直接暴露给开发者的API,但它们构成了JavaScript语言语义的基石。SetDataProperty 就是其中之一。

属性描述符 (Property Descriptors)
JavaScript中对象的每个属性都有一个与之关联的属性描述符。它是一个记录了属性各种特征的内部数据结构。一个属性描述符可以包含以下键值对:

类型 描述 默认值 (对于新创建属性)
value any 属性的值。对于数据属性,它存储实际的数据。 undefined
writable boolean 如果为 true,则属性的值可以被修改。如果为 false,则尝试修改属性值将被忽略(非严格模式)或抛出 TypeError(严格模式)。 true
get function 访问器属性的 getter 函数。当读取属性时,会调用此函数。 undefined
set function 访问器属性的 setter 函数。当写入属性时,会调用此函数。 undefined
enumerable boolean 如果为 true,则属性会在 for...in 循环和 Object.keys() 等操作中被枚举。 true
configurable boolean 如果为 true,则属性的类型(数据属性或访问器属性)可以更改,并且属性可以从对象中删除,其描述符的属性(writableenumerable 等)也可以被修改。如果为 false,则这些操作通常会被限制。 true

需要注意的是,一个属性描述符要么是数据描述符(包含 valuewritable),要么是访问器描述符(包含 getset),但不能同时是两者。enumerableconfigurable 可以应用于两者。


[[Set]] 抽象操作:赋值的入口

在大多数情况下,我们通过 obj.prop = valueobj['prop'] = value 这样的语法来给对象属性赋值。这些操作在底层会触发对象的 [[Set]] 内部方法(这是一个更高级别的抽象操作,每个JavaScript对象都必须实现它)。

[[Set]] 内部方法负责处理属性赋值的整个流程,包括:

  1. 查找属性是否存在于对象自身或原型链上。
  2. 判断属性是数据属性还是访问器属性。
  3. 处理原型链上的属性赋值行为。
  4. 最终,在适当的条件下,调用 SetDataProperty 来实际更新或创建数据属性。

其签名为 O.[[Set]](P, V, Receiver),其中:

  • O 是要修改属性的对象。
  • P 是属性键(通常是一个字符串或Symbol)。
  • V 是要赋给属性的值。
  • Receiver 是最初发起赋值操作的对象。这在原型链和访问器属性的 this 绑定中非常重要。

我们来看一个简化的 [[Set]] 逻辑流程,以便理解 SetDataProperty 在其中的位置:

// 简化版的 [[Set]] 抽象操作流程
function OrdinarySet(O, P, V, Receiver) {
    // 1. 获取对象O自身是否拥有属性P的描述符
    let ownDesc = O.[[GetOwnProperty]](P);

    // 2. 如果O自身有P的描述符
    if (ownDesc !== undefined) {
        // 2a. 如果是数据属性
        if (ownDesc.isDataDescriptor === true) {
            // 在这里,我们将调用 SetDataProperty 来处理这个数据属性
            return SetDataProperty(O, P, V); // 注意:这里简化了Receiver的传递,SetDataProperty内部不直接用Receiver
        }
        // 2b. 如果是访问器属性
        if (ownDesc.isAccessorDescriptor === true) {
            let setter = ownDesc.set;
            if (setter === undefined) {
                // 如果没有setter,则无法赋值
                if (IsStrict()) {
                    throw new TypeError("Cannot set property " + P + " of " + O + " which has only a getter");
                }
                return true; // 非严格模式下静默失败
            }
            // 调用setter函数,this绑定到Receiver
            setter.call(Receiver, V);
            return true;
        }
    }

    // 3. 如果O自身没有属性P,则遍历原型链
    let parent = O.[[GetPrototypeOf]]();
    if (parent !== null) {
        // 递归调用原型链上的[[Set]]
        return parent.[[Set]](P, V, Receiver);
    }

    // 4. 如果原型链上也没有,且O自身没有,那么需要创建一个新的数据属性
    // 默认创建的属性是可写、可枚举、可配置的
    let newDesc = {
        [[Value]]: V,
        [[Writable]]: true,
        [[Enumerable]]: true,
        [[Configurable]]: true
    };
    // 尝试在O上定义这个新属性
    return O.[[DefineOwnProperty]](P, newDesc);
}

从上述简化流程可以看出,SetDataProperty 主要处理两种情况:

  1. O 自身已经有一个数据属性 P 时,[[Set]] 会委托给 SetDataProperty 来更新其值。
  2. O 自身和原型链上都没有 P,或者原型链上有一个只读的数据属性 P 时,[[Set]] 的最终目标是尝试在 O 上创建或修改一个数据属性。在这个场景下,虽然 SetDataProperty 不直接被 [[Set]] 递归调用,但 [[Set]] 的最终行为(通过 [[DefineOwnProperty]])与 SetDataProperty 的内部逻辑有重叠,尤其是在创建新属性时。

SetDataProperty 抽象操作:核心逻辑解析

SetDataProperty 抽象操作的完整签名为 SetDataProperty(O, P, V)。它负责在对象 O 上设置属性 P 的值为 V。它的执行过程如下:

SetDataProperty(O, P, V) 规范步骤:

  1. O 必须是一个对象。P 必须是一个属性键(字符串或Symbol)。
  2. 获取属性描述符:
    调用 O.[[GetOwnProperty]](P) 获取对象 O 自身属性 P 的描述符 oldDesc
  3. 如果 oldDesc 存在(属性已存在于 O 自身):
    a. 检查 writable 属性:
    如果 oldDesc 是一个数据描述符,并且其 [[Writable]] 属性为 false,则表示该属性不可写。

    • 如果当前是严格模式,抛出 TypeError
    • 如果是非严格模式,静默地返回 true(表示操作成功,但实际上没有改变值)。
      b. 更新属性值:
      创建一个新的属性描述符 newDesc,只包含 [[Value]]: V
      调用 O.[[DefineOwnProperty]](P, newDesc) 来更新属性 P 的值。这将修改 oldDesc 中的 [[Value]]
      返回 true
  4. 如果 oldDesc 不存在(属性不存在于 O 自身):
    a. 创建新的数据描述符:
    创建一个新的、完整的属性描述符 newDesc,其属性如下:

    • [[Value]]: V
    • [[Writable]]: true (默认可写)
    • [[Enumerable]]: true (默认可枚举)
    • [[Configurable]]: true (默认可配置)
      b. 定义新属性:
      调用 O.[[DefineOwnProperty]](P, newDesc) 来在对象 O 上定义这个新属性。
      返回 true

通过这个流程,我们可以看到 SetDataProperty 的核心在于 O.[[DefineOwnProperty]](P, desc) 这个内部方法。[[DefineOwnProperty]] 负责实际在对象上创建、修改或删除属性,并严格遵守属性描述符的规则。


SetDataProperty[[DefineOwnProperty]] 的协作

[[DefineOwnProperty]] 是一个更底层的抽象操作,它负责在对象上定义一个属性,无论是创建新属性还是修改现有属性。它的签名是 O.[[DefineOwnProperty]](P, Desc)。它会执行一系列复杂的检查,以确保属性的定义或修改符合当前属性描述符的规则。

SetDataProperty 依赖于 [[DefineOwnProperty]] 来执行最终的属性操作。当 SetDataProperty 被调用时:

  • 如果属性 P 已经存在于 O 自身且是数据属性,并且 writabletrueSetDataProperty 会创建一个 { [[Value]]: V } 的描述符,并传递给 O.[[DefineOwnProperty]](P, { value: V }) 来更新值。
  • 如果属性 P 不存在于 O 自身,SetDataProperty 会创建一个 { value: V, writable: true, enumerable: true, configurable: true } 的描述符,并传递给 O.[[DefineOwnProperty]](P, newDesc) 来创建新属性。

这意味着 SetDataProperty 实际上是 [[DefineOwnProperty]] 在特定场景(简单赋值数据属性)下的一个简化包装器。[[DefineOwnProperty]] 内部会执行更全面的检查,例如:

  • 尝试修改一个不可配置的属性。
  • 尝试将一个数据属性转换为访问器属性,反之亦然。
  • 尝试修改一个不可写的属性(即使 SetDataProperty 已经检查过 writable[[DefineOwnProperty]] 也会再次确保)。

例如,如果我们尝试用 Object.defineProperty 修改一个不可写的属性,会直接触发 [[DefineOwnProperty]] 的错误检测:

const obj = {};
Object.defineProperty(obj, 'prop', {
    value: 10,
    writable: false,
    enumerable: true,
    configurable: true // 注意这里仍是可配置的
});

console.log(obj.prop); // 10

try {
    obj.prop = 20; // 尝试通过赋值修改
} catch (e) {
    console.log(e instanceof TypeError); // true (如果当前环境是严格模式)
    // 在非严格模式下,这里不会抛出错误,而是静默失败,obj.prop 仍为 10
}

console.log(obj.prop); // 10

try {
    // 尝试通过 Object.defineProperty 修改 value
    Object.defineProperty(obj, 'prop', { value: 30 });
} catch (e) {
    console.log(e instanceof TypeError); // true
    // [[DefineOwnProperty]] 会检测到试图修改不可写属性的值,抛出 TypeError
}

console.log(obj.prop); // 10

在这个例子中:

  • obj.prop = 20 触发 [[Set]],然后 [[Set]] 委托给 SetDataProperty
    • SetDataProperty 发现 prop 存在且 writable: false
    • 在严格模式下,SetDataProperty 立即抛出 TypeError
    • 在非严格模式下,SetDataProperty 静默返回 true,但值未改变。
  • Object.defineProperty(obj, 'prop', { value: 30 }) 直接触发 obj.[[DefineOwnProperty]]('prop', { value: 30 })
    • [[DefineOwnProperty]] 发现现有属性 propwritablefalse,但传入的描述符 value 尝试改变其值。
    • 根据规范,这会抛出 TypeError

这说明 SetDataProperty 专注于处理赋值语法的 writable 限制,而 [[DefineOwnProperty]] 则处理所有定义/修改属性时的更广泛的描述符一致性检查。


SetDataProperty 与原型链的复杂交互

理解 SetDataProperty 如何与原型链协同工作是理解JavaScript属性赋值的关键。当执行 obj.prop = value 时,[[Set]] 抽象操作会首先介入,它的行为取决于原型链上是否存在 prop 属性以及其类型。

我们通过几个典型的场景来剖析:

场景一:属性 P 存在于 O 自身,且为数据属性

这是 SetDataProperty 最直接的应用。

const obj = {
    myProp: 10
};

obj.myProp = 20;
console.log(obj.myProp); // 20

底层逻辑:

  1. obj.[[Set]]('myProp', 20, obj) 被调用。
  2. obj.[[GetOwnProperty]]('myProp') 找到 { value: 10, writable: true, enumerable: true, configurable: true }
  3. 发现 myProp 是数据属性,且 writabletrue
  4. [[Set]] 调用 SetDataProperty(obj, 'myProp', 20)
  5. SetDataProperty 获取 obj 自身的 myProp 描述符,发现 writabletrue
  6. SetDataProperty 调用 obj.[[DefineOwnProperty]]('myProp', { value: 20 })
  7. obj 上的 myProp 值更新为 20

场景二:属性 P 存在于 O 自身,且为不可写数据属性

const obj = {};
Object.defineProperty(obj, 'myProp', {
    value: 10,
    writable: false,
    enumerable: true,
    configurable: true
});

console.log(obj.myProp); // 10

// 严格模式下:
try {
    'use strict';
    obj.myProp = 20; // TypeError
} catch (e) {
    console.error("严格模式错误:", e.message); // 严格模式错误: Cannot assign to read only property 'myProp' of object '#<Object>'
}
console.log(obj.myProp); // 10

// 非严格模式下:
obj.myProp = 30; // 静默失败
console.log(obj.myProp); // 10

底层逻辑:

  1. obj.[[Set]]('myProp', 20/30, obj) 被调用。
  2. obj.[[GetOwnProperty]]('myProp') 找到 { value: 10, writable: false, ... }
  3. 发现 myProp 是数据属性,但 writablefalse
  4. [[Set]] 调用 SetDataProperty(obj, 'myProp', 20/30)
  5. SetDataProperty 获取 obj 自身的 myProp 描述符,发现 writablefalse
    • 如果是严格模式,SetDataProperty 立即抛出 TypeError
    • 如果是非严格模式,SetDataProperty 返回 true,但没有实际修改属性值。

场景三:属性 P 存在于 O 自身,且为访问器属性

const obj = {
    _value: 10,
    set myProp(val) {
        console.log(`Setting myProp to ${val}`);
        this._value = val;
    },
    get myProp() {
        console.log(`Getting myProp`);
        return this._value;
    }
};

obj.myProp = 20;
console.log(obj.myProp); // 20

底层逻辑:

  1. obj.[[Set]]('myProp', 20, obj) 被调用。
  2. obj.[[GetOwnProperty]]('myProp') 找到 { get: ..., set: ..., ... }
  3. 发现 myProp 是访问器属性。
  4. [[Set]] 提取 setter 函数 obj.myProp
  5. setter.call(obj, 20) 被调用(this 绑定到 obj)。
  6. SetDataProperty 在此场景下不被调用,因为这是一个访问器属性,其赋值行为由 setter 函数直接控制。

场景四:属性 P 存在于原型链上,为数据属性,且 O 自身没有 P

这是最常引起误解的场景,也是 SetDataProperty 间接发挥作用的关键。

const proto = {
    myProp: 10
};
const obj = Object.create(proto);

console.log(obj.myProp); // 10 (从原型继承)

obj.myProp = 20; // 发生什么?
console.log(obj.myProp); // 20 (obj 自身现在有了 myProp)
console.log(proto.myProp); // 10 (原型上的属性未受影响)

底层逻辑:

  1. obj.[[Set]]('myProp', 20, obj) 被调用。
  2. obj.[[GetOwnProperty]]('myProp') 返回 undefinedobj 自身没有 myProp)。
  3. [[Set]] 查找原型链:parent = obj.[[GetPrototypeOf]]() 得到 proto
  4. 递归调用 proto.[[Set]]('myProp', 20, obj)
  5. proto.[[GetOwnProperty]]('myProp') 找到 proto 上的 { value: 10, writable: true, ... }
  6. 发现 myProp 是数据属性,且 writabletrue
  7. 此时,proto.[[Set]] 的逻辑会检查 Receiver (即 obj) 是否与 proto 相同。由于 obj !== proto,并且 proto 上的 myProp 是一个可写的数据属性,规范规定此时应该尝试在 Receiver (即 obj) 上创建一个新的数据属性。
  8. proto.[[Set]] 最终会调用 Receiver.[[DefineOwnProperty]]('myProp', { value: 20, writable: true, enumerable: true, configurable: true })
  9. 这个 [[DefineOwnProperty]] 调用会触发 SetDataProperty 内部创建新属性的逻辑(尽管它不是直接通过 SetDataProperty(obj, 'myProp', 20) 调用,但效果等同于 SetDataProperty 的"属性不存在于 O 自身"分支)。
  10. 结果是,obj 自身创建了一个名为 myProp 的新属性,值为 20遮蔽 (shadow) 了原型上的同名属性。原型上的 myProp 保持不变。

场景五:属性 P 存在于原型链上,为不可写数据属性,且 O 自身没有 P

const proto = {};
Object.defineProperty(proto, 'myProp', {
    value: 10,
    writable: false,
    enumerable: true,
    configurable: true
});
const obj = Object.create(proto);

console.log(obj.myProp); // 10 (从原型继承)

// 严格模式下:
try {
    'use strict';
    obj.myProp = 20; // TypeError
} catch (e) {
    console.error("严格模式错误:", e.message); // 严格模式错误: Cannot assign to read only property 'myProp' of object '#<Object>'
}
console.log(obj.myProp); // 10
console.log(proto.myProp); // 10

// 非严格模式下:
obj.myProp = 30; // 静默失败
console.log(obj.myProp); // 10
console.log(proto.myProp); // 10

底层逻辑:

  1. obj.[[Set]]('myProp', 20/30, obj) 被调用。
  2. obj.[[GetOwnProperty]]('myProp') 返回 undefined
  3. [[Set]] 查找原型链:parent = obj.[[GetPrototypeOf]]() 得到 proto
  4. 递归调用 proto.[[Set]]('myProp', 20/30, obj)
  5. proto.[[GetOwnProperty]]('myProp') 找到 proto 上的 { value: 10, writable: false, ... }
  6. 发现 myProp 是数据属性,但 writablefalse
  7. 此时,proto.[[Set]] 的逻辑会检查 Receiver (即 obj) 是否与 proto 相同。由于 obj !== proto,并且 proto 上的 myProp 是一个不可写的数据属性,规范规定此时不能在 Receiver (即 obj) 上创建新属性。
    • 如果是严格模式,proto.[[Set]] 会抛出 TypeError
    • 如果是非严格模式,proto.[[Set]] 返回 true,但没有实际操作(静默失败)。
  8. SetDataProperty 在此场景下不被直接调用,因为在 proto.[[Set]] 层面就因原型属性不可写而阻止了在 obj 上创建新属性的尝试。

场景六:属性 P 存在于原型链上,为访问器属性,且 O 自身没有 P

const proto = {
    _value: 10,
    set myProp(val) {
        console.log(`Proto: Setting myProp to ${val}`);
        this._value = val;
    },
    get myProp() {
        console.log(`Proto: Getting myProp`);
        return this._value;
    }
};
const obj = Object.create(proto);

console.log(obj.myProp); // Proto: Getting myProp n 10

obj.myProp = 20; // 调用的是原型上的 setter
console.log(obj.myProp); // Proto: Getting myProp n 20
console.log(proto.myProp); // Proto: Getting myProp n 20 (原型上的属性被修改)
console.log(obj._value);   // 20
console.log(proto._value); // 20 (原型上的 _value 被修改)

底层逻辑:

  1. obj.[[Set]]('myProp', 20, obj) 被调用。
  2. obj.[[GetOwnProperty]]('myProp') 返回 undefined
  3. [[Set]] 查找原型链:parent = obj.[[GetPrototypeOf]]() 得到 proto
  4. 递归调用 proto.[[Set]]('myProp', 20, obj)
  5. proto.[[GetOwnProperty]]('myProp') 找到 proto 上的 { get: ..., set: ..., ... }
  6. 发现 myProp 是访问器属性。
  7. proto.[[Set]] 提取 setter 函数 proto.myProp
  8. setter.call(obj, 20) 被调用。注意这里的 this 绑定到 Receiver (obj),而不是 proto
  9. SetDataProperty 在此场景下不被调用setter 函数内部 (this._value = val) 会尝试在 obj 上创建或修改 _value 属性。如果 obj 没有 _value,则会在 obj 上创建 _value;如果 obj_value,则修改 obj 上的 _value。在这个例子中,obj 没有 _value,所以 obj._value 会被创建并赋值为 20

这个例子尤其重要,因为它展示了原型链上访问器属性的 setter 函数在被调用时,this 绑定到最初发起赋值操作的对象(Receiver)。这使得通过原型链上的 setter 可以在派生对象上创建或修改属性。

场景七:属性 P 不存在于 O 自身或原型链上

const obj = {};
obj.newProp = 100;
console.log(obj.newProp); // 100

底层逻辑:

  1. obj.[[Set]]('newProp', 100, obj) 被调用。
  2. obj.[[GetOwnProperty]]('newProp') 返回 undefined
  3. [[Set]] 查找原型链,直到 null
  4. 最终,[[Set]] 发现没有找到属性,需要在 obj 上创建一个新的数据属性。
  5. [[Set]] 调用 obj.[[DefineOwnProperty]]('newProp', { value: 100, writable: true, enumerable: true, configurable: true })
  6. 这个 [[DefineOwnProperty]] 调用会触发 SetDataProperty 内部创建新属性的逻辑(尽管不是直接调用)。
  7. 结果是,obj 自身创建了一个名为 newProp 的新属性,值为 100

严格模式与 SetDataProperty

严格模式对 SetDataProperty 的行为有着显著影响。在非严格模式下,对不可写属性的赋值操作会静默失败,即属性值不会改变,也不会报错。但在严格模式下,相同操作会抛出 TypeError

const strictObj = {};
Object.defineProperty(strictObj, 'fixedProp', {
    value: 50,
    writable: false
});

function trySetStrict() {
    'use strict';
    strictObj.fixedProp = 60; // 在这里会抛出 TypeError
}

try {
    trySetStrict();
} catch (e) {
    console.log("严格模式下,对不可写属性赋值抛出:", e.message);
}

const nonStrictObj = {};
Object.defineProperty(nonStrictObj, 'fixedProp', {
    value: 50,
    writable: false
});

function trySetNonStrict() {
    nonStrictObj.fixedProp = 60; // 在这里静默失败
}

trySetNonStrict();
console.log("非严格模式下,对不可写属性赋值后:", nonStrictObj.fixedProp); // 50

这种差异是 SetDataProperty 内部逻辑的一个关键分支:当检查到 writable: false 时,它会根据当前执行上下文是否为严格模式来决定是抛出错误还是静默返回。


SetDataProperty 与 Exotic Objects (特异对象)

JavaScript中除了普通对象(Ordinary Objects)外,还有一些“特异对象”(Exotic Objects),它们通过自定义内部方法来覆盖普通对象的默认行为。例如:

  • 数组 (Arrays):它们的 length 属性有特殊的行为。
  • Proxy 对象:它们可以完全拦截所有内部方法,包括 [[Set]][[DefineOwnProperty]]

对于这些特异对象,SetDataProperty 的直接调用可能不会发生,因为它们的 [[Set]][[DefineOwnProperty]] 内部方法可能已经被覆盖,从而改变了属性赋值的整个流程。

Proxy 为例:

const target = {
    a: 10
};
const handler = {
    set(target, prop, value, receiver) {
        console.log(`Proxy: Setting ${String(prop)} to ${value}`);
        if (prop === 'a' && value > 100) {
            console.log("Proxy: Value too high for 'a'!");
            return false; // 阻止赋值
        }
        // 调用 Reflect.set 执行默认的赋值行为,这最终可能会触发 SetDataProperty
        return Reflect.set(target, prop, value, receiver);
    }
};
const proxy = new Proxy(target, handler);

proxy.a = 50;
console.log(target.a); // 50 (通过Proxy修改了target)
console.log(proxy.a);  // 50

proxy.a = 200; // 被Proxy拦截并阻止
console.log(target.a); // 50 (target.a 未被修改)
console.log(proxy.a);  // 50

在这个例子中,proxy.a = 200 会首先触发 Proxy 对象的 [[Set]] 内部方法,它会调用 handler.set。在 handler.set 内部,我们可以完全控制赋值逻辑,甚至阻止它。如果 handler.set 最终调用 Reflect.set 并且没有阻止赋值,那么 Reflect.set 将会以 target 为对象,'a' 为属性,200 为值,proxyReceiver 来执行普通的 [[Set]] 逻辑,这可能会最终导致 SetDataProperty 被调用(如果 target.a 是数据属性)。

这表明 SetDataProperty 是普通对象属性赋值的基石,但在特异对象中,这个基石可能被更上层的逻辑所封装或替代。


属性赋值、查找与描述符的底层逻辑总结

SetDataProperty 抽象操作是JavaScript中数据属性赋值和创建的核心机制之一。它在 [[Set]] 抽象操作的协调下工作,负责处理当目标对象上已经存在可写数据属性时的值更新,以及当属性不存在时的新数据属性创建。

理解 SetDataProperty 的关键在于:

  1. 它只处理数据属性。 访问器属性的赋值行为由其 setter 函数直接控制,SetDataProperty 不参与。
  2. 它与 [[DefineOwnProperty]] 紧密相关。 SetDataProperty 实际上是 [[DefineOwnProperty]] 在简单赋值场景下的一个特定封装,利用 [[DefineOwnProperty]] 来执行实际的属性更新或创建。
  3. 原型链的交互复杂。 当属性在原型链上时,赋值操作 obj.prop = value 的行为取决于原型属性的类型(数据/访问器)和可写性,这可能导致在 obj 自身创建新属性(遮蔽),或调用原型上的 setter,或在严格模式下抛出 TypeError
  4. 严格模式的影响。 对不可写数据属性的赋值在严格模式下会抛出 TypeError,而在非严格模式下则会静默失败。
  5. 特异对象可以覆盖此行为。 Proxy 等特异对象可以通过拦截内部方法来完全改变或增强属性赋值的逻辑,从而绕过或修改 SetDataProperty 的默认执行路径。

深入了解 SetDataProperty 及其与 [[Set]][[DefineOwnProperty]] 和原型链的交互,为我们提供了洞察JavaScript对象行为的强大工具。它解释了为何属性赋值有时会创建新属性,有时会修改现有属性,有时会调用函数,有时又会抛出错误或静默失败。掌握这些底层机制,对于编写高效、可预测且无bug的JavaScript代码至关重要。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注