kuitos.github.io icon indicating copy to clipboard operation
kuitos.github.io copied to clipboard

ES规范解读之赋值操作符&属性访问器

Open kuitos opened this issue 9 years ago • 13 comments

ES规范解读之赋值操作符&属性访问器

事情起源于某天某妹子同事在看angular文档中关于Scope的说明Understanding Scopes(原文) 理解angular作用域(译文)时,对于文章中的例子有一点不理解,那个例子抽离细节之后大致是这样的:

// 一个标准的构造函数
function Scope(){}
Scope.prototype.array = [1,2,3];
Scope.prototype.string = 'Scope';

// 生成Scope实例
var scopeInstance = new Scope();

当我们访问scopeInstance上的属性时,假如scopeInstance上不存在该属性,则js解释器会从原型链上一层层往上找,直到找到有该属性,否则返回undefined。

// get对象上某一属性时会触发原型链查找
console.log(scopeInstance.string); // 'Scope'
console.log(scopeInstance.name); // undefined

而当我们往scopeInstance上某一属性设值时,它并不会触发原型链查找,而是直接给对象自身设值,如果对象上没有该属性则创建一个该属性。

scopeInstance.string = 'scopeInstance';
scopeInstance.array = [];
console.log(scopeInstance.string);  // 'scopeInstance'
console.log(scopeInstance.array);   // []
console.log(Scope.prototype.string); // 'Scope'
console.log(Scope.prototype.array); // [1,2,3]

总结起来,关于对象的属性的set和get操作看上去有这样一些特性:

  1. 读(get)操作会触发原型链查找,解释器会从原型链一层层往上查找,直到找不到返回undefined.
  2. 写(set)操作不会触发原型链查找,写操作会直接在对象上进行,没有这个属性会新建一个属性。

没错,这是最基本的原型链机制,我以前一直是这么理解的,然后我也是这么跟妹子解释的,然而文章后面的例子打了我脸。。。例子大致是这样的:

var scope2 = new Scope();
scope2.array[1] = 1;
console.log(scope2.array); // [1,1,3]
console.log(Scope.prototype.array); // [1,1,3]

WTF!!!
按照我的理解,写操作跟原型链无关,在对象自身操作。
顺着这个思路,那么 scope2.array[1]=1这行代码压根就会报错啊,因为scope2在创建array属性之前压根就没有自身的array属性啊!可是它竟然没报错还把Scope.prototype给改了!
于是我又在想,是不是这种引用类型(array,object)都会触发原型链查找,所以会出现这个结果? 然而我又想起前面那段代码:

scopeInstance.array = [];
console.log(scopeInstance.array);   // []
console.log(Scope.prototype.array); // [1,2,3]

这下彻底斯巴达了😂 从表象来看,scopeInstance.array[1]的读写操作都会触发原型链查找,而为啥scopeInstance.array的写操作就不会触发。如果说引用类型都会触发,那么scopeInstace.array=[]就等价于Scope.prototype.array = [],但是事实并不是这样。。。

碰到这种时候我只有祭出神器了(ecmascript),google什么的绝对不好使相信我。
翻到ecmascript关于赋值操作符那一小节,es是这样描述的

Simple Assignment (= )

The production AssignmentExpression : LeftHandSideExpression = AssignmentExpression is evaluated as follows:

  1. Evaluate LeftHandSideExpression.
  2. Evaluate AssignmentExpression.
  3. Call GetValue(Result(2)).
  4. Call PutValue(Result(1), Result(3)).
  5. Return Result(3).

前面三步都知道,关键点在第四步, PutValue(Result(1), Result(3))
我们再来看看PutValue干了啥

PutValue(V, W)

  1. If Type(V) is not Reference, throw a ReferenceError exception.
  2. Call GetBase(V).
  3. If Result(2) is null, go to step 6.
  4. Call the [[Put]] method of Result(2), passing GetPropertyName(V) for the property name and W for the value.

...

第二步有一个GetBase(V)操作,然后第四步依赖第二步的计算结果做最终赋值。 那么GetBase(V)究竟做了什么呢(V即我们赋值操作时候的左值)

GetBase(V)

GetBase(V). Returns the base object component of the reference V.

翻译下来就是:返回引用V的基础对象组件。
那么什么是基础对象组件呢,举两个例子:

GetBase(this.array) => this
GetBase(this.info.name) => this.info
GetBase(this.array[1]) => this.array

我们再来看看属性访问器(Property Accessors),就是括号[]操作符及点号.操作符都做了什么

属性访问器(Property Accessors)

MemberExpression . Identifier is identical in its behaviour to MemberExpression [ ]

也就是说括号跟点号对解释器而言是一样的。

The production MemberExpression : MemberExpression [ Expression ] is evaluated as follows:

  1. Evaluate MemberExpression.
  2. Call GetValue(Result(1)).
    ...

跟到GetValue

GetValue(V)

  1. If Type(V) is not Reference, return V.
  2. Call GetBase(V).
  3. If Result(2) is null, throw a ReferenceError exception.
  4. Call the [[Get]] method of Result(2), passing GetPropertyName( V) for the property name.

第四步的私有方法[[Get]]是关键:

[[Get]]

When the [[Get]] method of O is called with property name P, the following steps are taken:

  1. If O doesn't have a property with name P, go to step 4.
  2. Get the value of the property.
  3. Return Result(2).
  4. If the [[Prototype]] of O is null, return undefined.
  5. Call the [[Get]] method of [[Prototype]] with property name P.
  6. Return Result(5).

意思很明显,[[Get]]会触发原型链查找.
我们再回到赋值操作符的PutValue操作,走到第四步

Call the [[Put]] method of Result(2), passing GetPropertyName(V) for the property name and W for the value.

这里的Result(2)就是GetBase(V)的结果,拿上面的例子也就是GetBase(this.array[2]) == this.array 再看看[[Put]]操作干了什么事情:

[[Put]]

When the [[Put]] method of O is called with property P and value V, the following steps are taken:

  1. Call the [[CanPut]] method of O with name P.
  2. If Result(1) is false, return.
  3. If O doesn't have a property with name P, go to step 6.
  4. Set the value of the property to V. The attributes of the property are not changed.
  5. Return.
  6. Create a property with name P, set its value to V and give it empty attributes.
  7. Return.

很简单,就是给对象o的属性P赋值时,o存在属性P就直接覆盖,没有就新建属性。此时无关原型链。

此时再结合我们自己的案例来看,scopeInstance.array[1]=2scopeInstance.array=[]究竟都干了啥(忽略不相关细节):

scopeInstance.array[1]=2

  1. GetBase(scopeInstance.array[1]) == scopeInstance.array
  2. GetValue(scopeInstance.array) => 触发scopeInstace.array的[[Get]]方法,此时触发原型链查找 => 找到 Scope.prototype.array
  3. 设值操作 Scope.prototype.array.[Put];

scopeInstance.array=[]

  1. GetBase(scopeInstance.array) == scopeInstance
  2. GetValue(scopeInstance) => scopeInstance object
  3. 设值操作 scopeInstance.[[Put]]('array', []);

完美解释所有现象!

如果思考的比较深入的同学可能会问,scopeInstance又从哪儿取来的呢?也是类似原型链这样一层层往上查出来的么?这涉及到另一点知识,js中的作用域,具体可以看我的另一篇文章一道js面试题引发的思考

kuitos avatar Aug 30 '15 17:08 kuitos

我来水一发

ynCode avatar Sep 01 '15 09:09 ynCode

只水不star都是耍流氓 @ynCode

kuitos avatar Sep 06 '15 03:09 kuitos

Nice Post.

codezyc avatar Sep 24 '15 06:09 codezyc

@codezyc thx

kuitos avatar Oct 14 '15 07:10 kuitos

Simple Assignment 中的第一步的 Evaluate LeftHandSideExpression 一直不太明白

let foo
foo = 'hello'

在上面这个例子中,Evaluate LeftHandSideExpression 的结果不是 undefined 吗?这样还能执行第四步的 PutValue

tthallos avatar May 22 '17 15:05 tthallos

@mage3k Evaluate 操作并不取值,只是拿到表达式,只有执行了 GetValue 才会取值

kuitos avatar May 23 '17 02:05 kuitos

@kuitos 感谢回复。最近被网上那个连续赋值问题弄的有点困惑,网上也有不少分析文章的,但是都没有从规范上讲通的。这几天一直在看规范,从赋值到属性访问到引用,总算是明白了。这篇文章在这个过程中起了很大的作用。🙏

tthallos avatar May 23 '17 03:05 tthallos

@kuitos 如果 Evaluate 不取值的话,Call PutValue(Result(1), Result(3)) 中的 Result(1) 是什么呢?

tthallos avatar May 23 '17 03:05 tthallos

你的例子里,Result(1) 就是 foo 再比如 o['a' + 'b'] 的 Evaluate 的结果就是 o['ab'],只要没有执行 GetValue, 就不会对 o['ab'] 取值

kuitos avatar May 23 '17 04:05 kuitos

fooo['ab'] 可以理解成字符串吧,或者是规范里一个特殊的东西?

tthallos avatar May 23 '17 06:05 tthallos

@mage3k 不是,你可以理解成它们的 Reference

kuitos avatar May 23 '17 07:05 kuitos

@kuitos 你说的 Reference 和规范里的 Reference 应该不是同一个东西吧。 如果是一个东西的话,那规范当中的 GetValue就不太好理解了。

  1. If Type(V) is not Reference, return V

如果都是是规范里的 Reference,这一步是不是没什么意义。

tthallos avatar May 24 '17 03:05 tthallos

规范现在读起来还是有点吃力 而您的最初的问题我倒是感觉很自然就应该是这样的结果 但您为什么会花费如此功夫去深究这个问题 以使我怀疑我现在到底有没有完全理解这个问题

zhangenming avatar Apr 14 '18 11:04 zhangenming