资讯专栏INFORMATION COLUMN

理解引用

curlyCheng / 1565人阅读

摘要:我会解释里面神秘的引用,一旦你理解了引用,你就会明白通过引用来了解的绑定是多么轻松,你也会发现读的规范容易得多了。二理论把引用定义成。看看运算符的说法这也就是为什么我们对一个无法解析的引用使用操作符的时候并不会报错。

Know thy reference

(原文:know thy reference - kangax)

一、前言

翻译好不是件容易的事儿,我尽量讲得通顺,一些术语会保留原词,翻出一篇拗口的文章实在是得不偿失。

二、用大量的抽象来解释"this"

之前的一个星期天的早上,我躺床上看HackerNews,有一篇叫「This in JavaScript」的文章,我稍微扫了两眼。不出意外,就是函数调用、方法调用、显式绑定、构造函数实例化这档子事。这篇文章特别长,我越看就越觉得,这一大堆的解释和例子会给一个不了解this机制的人带来多大的心理阴影啊。

我想起来几年前我第一次看DC的「JavaScript The Good Parts」,当时看完书里的相关总结之后觉得无比清晰,书里简要地列出了这几条:

The this parameter is very important in object oriented programming, and its value is determined by the invocation pattern.There are four patterns of invocation in JavaScript:the method invocation pattern,the function invocation pattern,the constructor invocation pattern and the apply invocation pattern.The patterns differ in how the bonus parameter this is initialized.

只由调用方式决定,而且只有四种情况。看看,这说得多简单。

于是我去评论里看有没有人说 HackerNews 的这篇文章讲得太复杂了。果然,很多人都搬出了「JavaScript The Good Parts」里的总结,其中一个人提炼了一下:

The keyword this refers to whatever is left of the dot at call-time.

If there"s nothing to the left of the dot,then this refers to the root scope(e.g. Window)

A few functions change the behavior of this - bind,call and apply

The keyword new binds this to the object just created

简直精辟。但是我注意到里面的一句话-"whatever is left of the dot at call-time"。乍一看很有道理嘛,比方说foo.bar(),this指向foo;又比方说foo.bar.baz(),this指向foo.bar。但是(f = foo.bar)()呢?在这里所谓的「Whatever is left of the dot at call-time」就是foo,那this就指向foo咯?

为了拯救前端程序员于水火之中,我留言说,所谓的「句号左边的东西」可能没这么简单,要真的理解this,你可能需要理解引用和它的base values

也是这一次经历我才真的意识到引用的概念其实很少被提到。我去搜了一下"JavaScript reference",结果出来一些关于"pass-by-reference vs. pass-by-value"的讨论。不行,我得出来救场了。

这就是为什么我要来写这篇博客。

我会解释ECMAScript里面神秘的引用,一旦你理解了引用,你就会明白通过引用来了解this的绑定是多么轻松,你也会发现读ECMAScript的规范容易得多了。

一、关于引用

老实说,看到关于引用的讨论那么少我也多多少少可以理解,毕竟这也并不是语言本身的一部分。引用只是一种机制,用来描述ECMAScript里的特定行为。它对于解释引擎的实现至关重要,但是它们在代码里是看不见摸不着的。

当然,理解它对于写代码完完全全是必要的。

回到我们之前的问题:

foo.bar()
(f = foo.bar)()

到底为什么第一个的this指向foo,而第二个指向全局对象呢?

你可能会说,“括号左边的表达式里面完成了一次对 f 的赋值,赋值完了之后就相当于调用 f() ,这样的话就是一次函数调用,而不是方法调用了。”

好的,那这个呢:

(1, foo.bar)()

“噢,这是个圆括号运算符嘛!它完成从左边到右边的求值,所以它肯定和 foo.bar() 是一样的,所以它的this指向foo。”

var foo = {
  bar: function() {
    "use strict"
    return this
  }
}
(1, foo.bar)() //undefined

“呃......真是奇怪啊”

那这个呢:

(foo.bar)()

“呃,考虑到上一个例子,肯定也是undefined吧,应该是圆括号搞了什么鬼。”

(foo.bar)()  //{bar: function(){ ... }}

“好吧......我服了。”

二、理论

ECMAScript把引用定义成「resolved name binding」。这是由三个部分组成的抽象实体 - base, namestrict flag,第三个好懂,现在咱们聊前两个就够了。

创建引用有两种情况:

Identifier resolution

property access

比方说吧,foo创建了一个引用,foo.bar也创建了一个引用。而像1, "foo", /x/, { }, [ 1,2,3 ]这些字面量值和函数表达式(function(){})就没有。

Example Reference? Notes
"foo" NO
123 NO
/x/ NO
({}) NO
(function(){}) NO
foo YES Could be unresolved reference if foo is not defined
foo.bar YES Property reference
(123).toString YES Property reference
(function(){}).toString YES Property reference
(1, foo.bar)() NO Already evaluated, BUT see grouping operator exception
(f = foo.bar)() NO Already evaluated, BUT see grouping operator exception
(foo) YES Grouping operator does not evaluate reference
(foo.bar) YES Grouping operator does not evaluate reference

先别管后面四个,我们待会再看。

每次一个引用创建的时候,它的组成部分base,name,strict都被赋上值。name就是解析的标识符或者属性名,base就是属性对象或者环境对象。

可能把引用理解成一个没有原型的JavaScript对象会比较好,它就只有base, namestrict三个属性。下面举两个例子:

//when foo is defined earlier

foo

var Reference = {
  base: Environment,
  name: "foo",
  strict: false
}
----------------
foo.bar

//这就是所谓的「Property Reference」
var Reference = {
  base: foo,
  name: "bar",
  strict: false
} 

还有第三种情况,即不可解析引用。如果在作用域里找不到标识符,引用的base就会设为undefined:

//when foo is not defined

foo

var Reference = {
  base: undefined,
  name: "foo",
  strict: false
}

你肯定见过,解析不了的引用可能会导致引用错误-("foo is not defined").

本质上来说,引用就是一种代表名称绑定的简单机制,它把对象的属性解析和变量解析抽象出一个类似对象的数据结构:

var reference = {
  base: Object or Environment,
  name: name
}

现在我们知道ECMAScript底层做了什么了,但是这对解释this的指向有什么用呢?

三、函数调用

看看函数调用的时候发生了什么:

Let ref be the result of evaluating MemberExpression.

Let func be GetValue(ref).

Let argList be the result of evaluating Arguments, producing an internal list of argument values (see 11.2.4).

If Type(func) is not Object, throw a TypeError exception.

If IsCallable(func) is false, throw a TypeError exception.

If Type(ref) is Reference, then

If IsPropertyReference(ref) is true, then

Let thisValue be GetBase(ref).

Else, the base of ref is an Environment Record

Let thisValue be the result of calling the ImplicitThisValue concrete method of GetBase(ref).

Else, Type(ref) is not Reference.

Let thisValue be undefined.

Return the result of calling the [[Call]] internal method on func, providing thisValue as the this value and providing the list argList as the argument values.

加粗的第六步基本上就解释了DC四条里面的1、2两条:

//foo.bar()
- `foo.bar`是个属性引用吗?
- 是的
- 那取它的base,也就是`foo`作为`this`吧

//foo()
- `foo`是个属性引用吗?
- 不是
- 那你的base就是undefined

//(function(){})()
- 什么?你连引用都不是啊,那不用看了,undefined
四、赋值,逗号,圆括号运算符

有了前面的了解,我们看看能不能解释得了下面这几个函数调用的this指向。

(f = foo.bar)()

(1, foo.bar)()

(foo.bar)()

从第一个赋值运算说起,括号里是一个简单赋值操作,如果我们看看简单赋值做了些什么的话,我们可能可以看出点端倪:

Let lref be the result of evaluating LeftHandSideExpression.

Let rref be the result of evaluating AssignmentExpression.

Let rval be GetValue(rref).

Throw a SyntaxError exception if the following conditions are all true:

Type(lref) is Reference is true

IsStrictReference(lref) is true

Type(GetBase(lref)) is Environment Record

GetReferencedName(lref) is either "eval" or "arguments"

Call PutValue(lref, rval).

Return rval.

注意到右边的表达式在赋值之前通过内部的GetValue()求值。在我们的例子里面,foo.bar引用被转换成了一个函数对象,那么以非引用方式调用函数的话,this就指向了undefined。所以深入剖析的话,比起来foo.bar(),(f = foo.bar)()其实更像是(function(){})()。就是说,它是个求过值的表达式,而不是一个拥有base的引用。

第二个逗号运算就类似了:

Let lref be the result of evaluating Expression.

Call GetValue(lref).

Let rref be the result of evaluating AssignmentExpression.

Return GetValue(rref).

通过了GetValue,引用转换成了函数对象,this指向了undefined.

最后是圆括号运算符:

Return the result of evaluating Expression. This may be of type Reference.

NOTE This algorithm does not apply GetValue to the result of evaluating Expression. The principal motivation for this is so that operators such as delete and typeof may be applied to parenthesised expressions.

那很明白了,圆括号运算符没有对引用进行转换,这也就是为什么它的this指向了foo.

五、typeof运算符

既然都聊到这儿了,干脆聊一聊别的。看看typeof运算符的说法:

Let val be the result of evaluating UnaryExpression.

If Type(val) is Reference, then

If IsUnresolvableReference(val) is true, return "undefined".

Let val be GetValue(val).

Return a String determined by Type(val) according to Table 20.

这也就是为什么我们对一个无法解析的引用使用typeof操作符的时候并不会报错。但是如果不用typeof运算符,直接做一个声明呢:

Expression Statement:

Let exprRef be the result of evaluating Expression.

Return (normal, GetValue(exprRef), empty).

GetValue():

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

Let base be the result of calling GetBase(V).

If IsUnresolvableReference(V), throw a ReferenceError exception.

看到了吧,过不了GetValue这一关,所以说出现了没法解析的声明直接就报错了。

六、delete运算符

长话短说:

如果不是个引用,返回true(delete 1,delete /x/)

如果是没法解析的引用(delete iDontExist)

严格模式,报错

否则返回true

如果确实是个属性引用,那就删了它,返回true

如果是全局对象作为base的属性

严格模式,报错

否则,删除,返回true

三、后记

这篇文章都是基于ES5的,ES2015可能会有些变化。

另外,结果我还是翻出来一篇拗口的文章,Oops!

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/79037.html

相关文章

  • 理解Javascritp中的"引用"

    摘要:二逻辑判断中的引用通常我们会把理解为值相同,把理解为值相同且类型相同。但是这种理解不是完全准确的。只能确保定义的变量的引用地址不会被改变。 Author: bugall Wechat: bugallF Email: 769088641@qq.com Github: https://github.com/bugall 一: 函数中的引用传递 我们看下下面的代码的...

    用户83 评论0 收藏0
  • 几分钟理解 Jdk - Reference

    摘要:一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被垃圾收集器回收时收到一个系统通知。在之后提供了类来实现虚引用参考深入理解虚拟机 GC $TODO$ 一个对象的生命周期 一个对象的生命周期从它被创建开始,此时虚拟机会给它置一个内部标识finalizable,当 GC 到达某一个安全点并检验该...

    JohnLui 评论0 收藏0
  • PHP中引用传递+unset+global理解,希望大神指正

    摘要:即产生了相当于这样的效果,所以改变的值也同时改变了的值。不要用返回引用来增加性能,引擎足够聪明来自己进行优化。只能从函数返回引用变量没别的方法。 关键是对global的误解,之前以为在函数中global变量,就是把函数外部的变量拿进函数内部使用,但似乎我错了引用传递+unset+global理解 php的引用(就是在变量、函数、对象等前面加上&符号)在PHP中引用的意思是:不同的名字访...

    ConardLi 评论0 收藏0
  • 理解-PHP引用

    摘要:引用本身概念好理解性能也很好但是用好它还是存在着一定的门槛不太好写。写本文的起因是这几天碰到非常好的一个解决方案,让我重新理解了引用。如果下面的代码,你看完就能理解了,说明你引用真是学到家了你也可以直接跳过本文哈。 起因: 日常开发中,我们会碰到构造树的需求,通过id,pid的关系去构建一个树结构,然后对树进行遍历等操作。其实现方式分为两种: 1. 递归, 2. 引用而这两个方法的优缺...

    ermaoL 评论0 收藏0
  • 前端基础进阶(一):内存空间详细图解

    摘要:一栈数据结构与不同,中并没有严格意义上区分栈内存与堆内存。引用数据类型的值是保存在堆内存中的对象。不允许直接访问堆内存中的位置,因此我们不能直接操作对象的堆内存空间。为了更好的搞懂变量对象与堆内存,我们可以结合以下例子与图解进行理解。 showImg(https://segmentfault.com/img/remote/1460000009784102?w=1240&h=683); ...

    _Suqin 评论0 收藏0
  • 深入理解js对象的引用

    JavaScript 有七种内置类型,其中: 基本类型 • 空值(null) • 未定义(undefined) • 布尔值( boolean) • 数字(number) • 字符串(string) • 符号(symbol,ES6 中新增) 引用类型 • 对象(object) 对于基本类型,赋值(=)是值的拷贝,比较(===)的是实际的值,而对于引用类型(Array也是一种Object),赋值(=)...

    hedge_hog 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<