最近经常有人在群里问一些简单的问题,比如贴出一段代码乱七八糟的代码,然后说上面的某个变量是什么。 例如,这是一个很好的例子:
那么可能会有这样的疑问:
为什么 this.arg 未定义? 为什么两次调用 fn 的 this 不同?
为此,我认为作为一个看似不成熟的后端,我还是应该教导和解释我能做的事情。 因此,在本文中,我计划从非常肤浅的层面解释 javascript 中的对象搜索是如何工作的。
注意,本文只是从一个幻觉中介绍了对象查找的过程。 文章中的观点并不都是正确的,甚至还存在一些谬误,但这也是为了初学者更好地理解对象查找的过程。 我认为如果太具体、太深入的话,会造成一些负面影响。 如果三天后,你回来发现这篇文章不是那么正确,那么恭喜你,那时你已经可以找到正确的前进方向,并且这篇文章中的错误将不再对你产生任何影响。
物体分类
所谓对象搜索,就是在一段可执行代码的范围内找到当前需要的对象。 在javascript中,需要搜索的对象大致可以分为三类:
区分这3种类型的对象查找是首要任务,您可以根据以下原则进行区分:
看一下这段代码:
var foo = this;
foo.bar();
这两行代码演示了 3 种类型的对象查找,即:
找到 this 对象并给 foo 变量一个形式参数。 查看 foo 变量。 找到 foo 变量下的 bar 属性并将其作为 function.variable 查找调用
当某个对象的搜索被确定为变量搜索后,就可以按照变量搜索的规则进行查看。
变量查找,即作用域链上的查找。 作用域链是 javascript 中两个著名的链之一。 以下代码显示了标准作用域链:
var foo = 1;
function a() {
var bar = 2;
function b() {
foo = 3;
function c() {
alert(foo + ',' + bar); // 注意这一行
}
c();
}
b();
}
a();
在c函数中,一共查找了2个变量,分别是foo和bar。
变量查找可以简单地遵循“自下而下”的原则,即:
在函数 c 的范围内搜索 foo 和 bar。 显然,c上没有foo和bar的声明,搜索失败。 在包含c的函数(即函数b)范围内搜索foo和bar。 可以看到b上只有foo的形参,没有声明,所以查找失败。 在包含b的函数范围内,即函数a,搜索foo和bar,可以找到bar的声明,因此判断bar为2。由于a没有被任何函数包含,那么寻找在全局作用域中查看foo,发现有foo的声明,所以判断foo的值是1。但是因为在函数b中,这个tgcode foo有一个形参,所以foo的值就变成了3完全确定foo的tgcode值为3,bar的值为2,所以输出“3,2”。
综上所述,变量的搜索是沿着作用域链进行的。 作用域链可以简单地看成是函数之间的包含关系。 当变量在包含的函数中不存在时,将在包含该变量的函数中搜索该变量,直到全局范围。
属性查找
当确定对象的查找为属性查找时,可以根据属性查找的规则进行检查。
属性查找,即原型链上的查找,这是javascript双链的另一种,一个原型链可以表示如下:
var a = function() {};
var b = function() {};
var c = function() {};
b.prototype = new a();
c.prototype = new b();
a.prototype.foo = 1;
b.prototype.bar = 2;
c.prototype.foo = 3;
var o = new c();
alert(o.foo + ',' + o.bar); // 这一行进行查找
属性查找是一个不断寻找原型的过程,即:
检查c.prototype中是否定义了foo和bar,发现定义了foo,其值为3。发现c.prototype是new b()得到的对象,然后搜索b.prototype可以看到如果定义了bar,发现定义了,其值为2,因此判断foo的值为3,bar的值为2,输出“3,2”。
综上所述,属性搜索是沿着原型链进行的。 原型链的具体知识这里就不详细讲解了,可以找另一篇文章参考。 对于所有对象来说,原型链最终都会是Object.prototype。
这次查找
搜索这个是很多人的苦恼点,很多人可能会有这个不稳定的想法javascript对象定义,这实在是让人无语。 this的查找可以说是三个对象查找中最简单的一个,因为虽然这个对象的判断根本没有“查找”的过程。
首先,this 对象只需要在一个函数中确定。 如果是在全局域中,这将始终是全局对象,通常是浏览器中的窗口对象。 在javascript中,函数调用有四种形式:
函数调用模式
诸如 foo() 之类的调用方法称为函数调用模式,这是最直接的使用函数的方式。 请注意,此处 foo 显示为单独的变量,而不是属性。
在这些模式下,foo函数体中的this始终是Global对象,即浏览器中的window对象。
方法调用模式
诸如“foo.bar()”之类的调用方法称为方法调用模式。 注意,它的特点是被调用的函数作为对象的属性出现,并且必须有“.”等关键符号。 或者 ”[]”。
在这些模式下,bar函数体中的this始终是“.”之前的对象。 或“[”,如上例,它必须是 foo 对象。
构造函数模式
new foo() 的调用被称为构造函数模式,它的关键字 new 可以很好地说明问题,并且非常容易识别。
在这些模式下javascript对象定义,foo 函数内的 this 始终是 new foo() 返回的对象。
应用图案
`foo.call(thisObject)` 和 `foo.apply(thisObject)` 方法称为Apply Pattern,它使用外部`call` 和`apply` 函数。
在这些模式下,`call` 和 `apply` 的第一个参数是 foo 函数体中的 this,如果 thisObject 为 `null` 或 `undefined`,那么它将被做成一个 Global 对象。
应用上面四种方法来判断一个函数是用什么样的Pattern来调用的,就可以很容易地判断出这是什么。
另外,这永远不会为“搜索”过程扩展作用域链或原型链,并且只有在调用函数时才会得到完全确认。
总结
对于对象的查找:
确定它是变量查找、属性查找还是此查找。 如果是变量查找,则扩展作用域链来查找它。 如果找不到,就会出现ReferenceError。 如果是属性搜索,就会沿着原型链去搜索,如果找到了,那就是未定义的。 如果是this搜索,找到调用该函数的代码,根据调用方式判断是哪个this。
注意拆分一个查找过程,比如this.foo.bar.yahoo(),可以拆分成这样的代码,这样更清晰:
var o = this; // this查找
var foo = o.this; // 属性查找
var bar = foo.bar; // 属性查找
bar.yahoo(); // 属性查找,加Method Invocation Pattern
最后,如果你能花三天时间理解这种东西,这篇文章对你的用户来说就不会很重要: