Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
kuitos opened this issue Aug 30, 2015 · 13 comments
Open

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

kuitos opened this issue Aug 30, 2015 · 13 comments

Comments

@kuitos
Copy link
Owner

kuitos commented Aug 30, 2015

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面试题引发的思考

@ynCode
Copy link

ynCode commented Sep 1, 2015

我来水一发

@kuitos
Copy link
Owner Author

kuitos commented Sep 6, 2015

只水不star都是耍流氓 @ynCode

@codezyc
Copy link

codezyc commented Sep 24, 2015

Nice Post.

@kuitos
Copy link
Owner Author

kuitos commented Oct 14, 2015

@codezyc thx

@tthallos
Copy link

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

let foo
foo = 'hello'

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

@kuitos
Copy link
Owner Author

kuitos commented May 23, 2017

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

@tthallos
Copy link

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

@tthallos
Copy link

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

@kuitos
Copy link
Owner Author

kuitos commented May 23, 2017

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

@tthallos
Copy link

tthallos commented May 23, 2017

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

@kuitos
Copy link
Owner Author

kuitos commented May 23, 2017

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

@tthallos
Copy link

tthallos commented May 24, 2017

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

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

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

@zhangenming
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants