Igor Cheglakov
Igor Cheglakov

Reputation: 555

Why exactly setter is required here?

HTML-snippet

 <div id="container">

    <span class="no"></span>
    <span class="yes"></span>
    <span class="no"></span>
    <span class="no"></span>

 </div>

So, a getter, that returns an element .yes and provides lazy evaluation (I think that's the term, right?

class Something{
    constructor(){
        this.elem = container;
    }

    get childYes(){
        let span = this.elem.querySelector(".yes");
        this.childYes = span;//Cannot set property clientName of #<Docum> which has only a getter
        return span;
    }   
}


var object = new Something();
console.log(object.childYes);

But if I add an empty setter it works fine:

class Something{
    constructor(){
        this.elem = container;
    }

    get childYes(){
        let span = this.elem.querySelector(".yes");
        this.childYes = span;
        return span;
    }   
    set childYes(a){};
}


var object = new Something();
console.log(object.childYes); // <span class="yes"></span>

I don't need a setter there, what exactly does browser want from me?

Upvotes: 1

Views: 57

Answers (1)

T.J. Crowder
T.J. Crowder

Reputation: 1074545

Cannot set property clientName of #<Docum> which has only a getter

By doing this.childYes = ... on a property defined as an accessor, you're trying to use a setter. To define the property on the instance, use Object.defineProperty:

get childYes(){
    let span = this.elem.querySelector(".yes");
    Object.defineProperty(this, "childYes", {
        value: span
    });
    return span;
}   

class Something{
    constructor(){
        this.elem = container;
    }

    get childYes(){
        console.log("Getter ran");
        let span = this.elem.querySelector(".yes");
        Object.defineProperty(this, "childYes", {
            value: span
        });
        return span;
    }   
}

var object = new Something();
console.log(object.childYes); // Runs the getter
console.log(object.childYes); // Uses the data property
<div id="container">
    <span class="no"></span>
    <span class="yes"></span>
    <span class="no"></span>
    <span class="no"></span>
</div>


In a comment you've asked:

So, correct me if I'm wrong: After object.childYes is called, the program looks for own .childYes property of object first; fails; goes to the prototype; finds getter; starts to execute the getter and when the line this.childYes = span; appears the program looks for it "here" e.g. in prototype and fails, right?

It's not because of where the this.childYes = span; line is, but yes, other than that it's right. When you assign to an object property, what happens depends on whether the property exists on the object or its prototypes and, if it does, whether the property is a data property or an accessor property:

  1. If the property doesn't exist at all (on the object or any of its prototypes), the JavaScript engine creates it as a data property on the original object, giving it the value being assigned.
  2. If it exists as a data property (on the object or any of its prototypes), the engine creates or updates it on the original object, giving it the value being assigned.
  3. If it exists as an accessor property, then
    1. If it has a setter, it calls the setter
    2. If not, it throws an error

In your original code, you ended up at Step 3.2 above, because the property existed as an accessor property on the prototype.

Here's an example of those various scenarios:

"use strict";

// A function to tell us if an object has a property and, if so
// what kind of property it is
function getPropertyType(obj, propName) {
    const descr = Object.getOwnPropertyDescriptor(obj, propName);
	if (!descr) {
		return "none";
	}
	if (descr.hasOwnProperty("get") || descr.hasOwnProperty("set")) {
		return "accessor";
	}
	if (descr.hasOwnProperty("value") || descr.hasOwnProperty("writable")) {
		return `data (${descr.value})`;
	}
	return "generic"; // Unlikely, but the spec allows for it
}

// An object to use as a prototype
const proto = {
    dataProperty1: "dataProperty1 value",

    _readWrite: "readWriteAccessor default value",
    get readWriteAccessor() {
      return this._readWrite;
    },
    set readWriteAccessor(value) {
      this._readWrite = value;
    },
    
    get readOnlyAccessor() {
      return "readOnlyAccessor value";
    }
};

// Create an object using `proto` as its prototype
const obj = Object.create(proto);

console.log(`obj dataProperty2:       ${getPropertyType(obj, "dataProperty2")}`);
console.log(`proto dataProperty2:     ${getPropertyType(proto, "dataProperty2")}`);

console.log(`--- Before obj.dataProperty1 = "dataProperty1 updated";`);
console.log(`obj dataProperty1:       ${getPropertyType(obj, "dataProperty1")}`);
console.log(`proto dataProperty1:     ${getPropertyType(proto, "dataProperty1")}`);
obj.dataProperty1 = "dataProperty1 updated";
console.log(`--- After obj.dataProperty1 = "dataProperty1 updated";`);
console.log(`obj dataProperty1:       ${getPropertyType(obj, "dataProperty1")}`);
console.log(`proto dataProperty1:     ${getPropertyType(proto, "dataProperty1")}`);

console.log(`--- Before obj.dataProperty2 = "dataProperty2 updated";`);
console.log(`obj dataProperty2:       ${getPropertyType(obj, "dataProperty2")}`);
console.log(`proto dataProperty2:     ${getPropertyType(proto, "dataProperty2")}`);
obj.dataProperty2 = "dataProperty2 updated";
console.log(`--- After obj.dataProperty2 = "dataProperty2 updated";`);
console.log(`obj dataProperty2:       ${getPropertyType(obj, "dataProperty2")}`);
console.log(`proto dataProperty2:     ${getPropertyType(proto, "dataProperty2")}`);

console.log(`--- Before obj.readWriteAccessor = "readWriteAccessor updated";`);
console.log(`obj readWriteAccessor:   ${getPropertyType(obj, "readWriteAccessor")}`);
console.log(`proto readWriteAccessor: ${getPropertyType(proto, "readWriteAccessor")}`);
obj.readWriteAccessor = "readWriteAccessor updated";
console.log(`--- After obj.readWriteAccessor = "readWriteAccessor updated";`);
console.log(`obj readWriteAccessor:   ${getPropertyType(obj, "readWriteAccessor")}`);
console.log(`proto readWriteAccessor: ${getPropertyType(proto, "readWriteAccessor")}`);

console.log(`obj readOnlyAccessor:    ${getPropertyType(obj, "readOnlyAccessor")}`);
console.log(`proto readOnlyAccessor:  ${getPropertyType(proto, "readOnlyAccessor")}`);
console.log(`--- Before obj.readOnlyAccessor = "readOnlyAccessor updated";`);
try {
	obj.readOnlyAccessor = "readOnlyAccessor updated"; // Would fail silently in loose mode, but we're using strict
	console.log(`Worked!`);
} catch (e) {
	console.error(`Assignment failed: ${e.message}`);
}
console.log(`--- After obj.readOnlyAccessor = "readOnlyAccessor updated";`);
console.log(`obj readOnlyAccessor:    ${getPropertyType(obj, "readOnlyAccessor")}`);
console.log(`proto readOnlyAccessor:  ${getPropertyType(proto, "readOnlyAccessor")}`);
.as-console-wrapper {
  max-height: 100% !important;
}

Upvotes: 5

Related Questions