前序
笔者因为一直在做 java
,所有在了解 javascript
的时候,总是在与 java
进行对比和联系。但是 js
和 java
在编程语言的底层有很多不同的地方,导致一直联系一直懵逼,与旧的知识产生不到共鸣。
直到最近,了解到js
内存结构,从底层了解 js
的运行机制,对一些概念就从底层可以理解。特意写文章来总结一下。
本文阅读思路是,罗列一些有理解上有困扰的代码(分别是,上下文、this指针、方法是第一等公民、闭包等概念),描述分析过程,对比 java
语言的差异,加深 javascript
的理解。
困惑的代码
取哪个变量
讨论 方法上下文,或者说是 变量作用域 ( 记得 与
this
指针无关 )。
// step1
var a = 1;
var f = function(){
console.log(a);
}
f(); // 1
// step2
var f1 = function(k) {
var a = 22;
k();
}
f1(f); //1
Q:
step2 中 为什么 a 不是取 k()
运行代码前定义的值 22 的吗?
A:
- step1 中,定义
f@function
的时候,方法上下文 是windows
,找a@variable
的时候,会在上下文中找,所以值是1
- step2 中,
f@function
作为入参给到f1@function
,并且在f1@function
中执行,且f1@function
也是定义了a = 22
, 但是f@function
运行后console
打印 还是1
.
因为 方法上下文 是 定义时 已指定了,并不是 运行时指定。所以 k@function
运行的时候,还是会找回 定义时的 方法上下文 变量a的值,输出 1
。
用代码简写一下:
define function f {
f.context = windows
}
找变量就从 context
中找。
var k = function() {
var a = 22;
var f = function(){
console.log(a);
}
return f;
}
var a = 1;
k()(); //22
定义 f@function
的时候,指定了 方法上下文 是 k@function
( PS: 不是 k@object
), 所以 找到 k@function.a
的值。
用代码简写:
define function k {
define function f {
f.context = k@function
}
}
结论
上下文
(不论是 方法上下文,还是下文提到的 运行时上下文 )或者说是 变量的作用域,只有两种可能:1.windows
, 2.function
。换句话,变量的作用域只存在这两个主体内。方法上下文
已经在 定义时 已经是决定了,即 定义时的 windows 和 function 中。- 找变量的值(PS:没有 this关键字 指定),都是在
方法上下文
中找
扩展
与 java
一样, 在一片内存中分配变量且赋值。
但是 java
是以 object
为单位 划分内存,javascript
是以 function
为单位划分内存 (windows 为父内存单位 )。
有 this
指针后,取哪个变量
讨论
this
对读取变量的影响
// 前置条件
var a = 2;
var v = {
a: 1,
b: function(){
console.log(this.a)
}
}
// 验证
v.b(); // 1 //step1
var f = v.b; //step2
f(); // 2
Q:
同样是调用一样的方法,step1 取到 a 的变量是 1 ,step2 取到 a 的变量是 2?
A:
- step1 中,
v.b()
就是指,先找到v@obj
,再调用b@function
。所以他的 _运行时上下文 (调用时)_是v@obj
,runningContext = v@obj
,同时 this 会指向runningContext
, 所以this.a = v.a
,所以值是1
. - step2 中,
f = v.b
就是,把 v.b 方法的地址赋值给f
,再运行f@function
,等于在 windows上下文中运行f@function
,所以他的 _运行时上下文 是windows
, _runningContext = windows
_。_所以this.a = windows.a
可能换种写法,会更加的清晰。
var f = function(){
console.log(this.a)
}
var a = 2;
var v = {
a: 1,
b: f // 这种写法,仅仅是属性的赋值,定义还是在外部的上下文
}
// 验证
v.b(); // 1
var f = v.b;
f(); // 2
this
是动态的。根据方法绑定在哪个 运行时上下文,他就会在哪里去找变量。
结论
this
指针指向的变量,都是取自绑定的 _运行时上下文 _this
是 动态的,绑定不同的对象,就有不同的 运行时上下文
扩展
- 如果想强制的绑定上下文,固定
this
指向,可以使用call
apply
和bind
等方法。 this
概念,是javascript
与java
有大的差异点。java this
,是指向本对象内部,一个对象生成了,this
的指向就确定了;js this
是指 运行时上下文,随着绑定的 运行时上下文不同,他的指向的变量都不同。
结合第一个示例,得到下面的结论:
- 方法内的变量,如果没有
this
指定的变量,是在 定义上下文 中找值 - 方法内的变量,如果有
this
指定的变量,是在 运行时上下文 中找值
方法是第一等公民
在 javascript 中,function 有特殊的用法
var f = function(){
console.log(this.a)
}
var a = 2;
var v = {
a: 1,
b: f // step1
}
// 验证
v.b(); // 1 // step2
var fn = v.b; // step3
fn(); // 2
结论
- 方法调用,对比
java
的方法调用,必须是经过对象,再调起方法;javascript 的 方法,可以有自己的内存地址,可以直接的调用,同时 方法又可以赋值给其他的变量。
在 javascript
中 function
是一等公民。这个是经常听到的一句话。在理解上可以有两个衍生的层次:
function
可以有自己的内存地址function
可以作为入参,传入其他的方法function
可以直接调用,不用依附于某个对象function
可以绑定在不同的 运行时上下文 中,动态改变this
的值
function
定义的时候,就已经有自己的 定义时上下文- 从底层就是能够开辟了一个内存空间
扩展:对比 java
- 方法的调用
java
,必须是obj.function()
,必须是经过对象才找到方法,并且调用js
,function()
,可以是直接的调用,也可以绑定某个运行时上下文后再调用
- 方法的定义
java
,必须obj
内部js
,在在 不同的上下文中,即是windows
或者 不同的function
中
- 内存的划分
java
,分配内存的单位 生成对象(不是定义类),就可以指定一片内存js
,分配内存的单位 定义方法,就可以划分了一片内存
dive in 内存结构
js function 是有地址的,并且可以单独调用。
var f = function(){}
var v = {
b: f
}
方法作为入参
主要是讨论方法地址
var f = function(){
console.log(this.a)
}
var a = 2;
var v = {
a: 1,
b: f // step0
}
var callback = function(fn) {
fn();
}
// step1
callback(v.b); //2
// step2
callback(()=>{v.b()}); //1
Q:
大家都是调用 v.b,为什么会有差异呢?
A:
- step0 中,定义 v.b 的属性,是 f@function 的地址
- step1 中,调起 callback@function,入参 fn 会复制 v.b 的值,而 v.b 的值就是 f@function 的值,即是直接调用了 f@function,所以就会看到 this 绑定了 windows,与预期调用不一致
- step2 中,入参是匿名方法,方法体内,显式调用
v.b()
,f 里面的 this 就会绑定 v@obj 上,符合预期
结论
- function 有独立的地址,可以直接调用,也可以传参;也可以赋值给对象 obj 的属性,经过 obj 调用仅仅是改变 this 的值,就是运行时上下文
- 解决 this 指针不符合预期,可以在外层封装一个函数,把 v.b() 显示的调起,这样就符合调用的预期
闭包
涉及到 方法的定义上下文
var getCalculator = function(num){ // step1
return function(){ // step2
return num +=1
}
}
var inc = getCalculator(7); //step3
inc(); // 8 //step4
inc(); // 9
var a = inc();
console.log(a); // 10
Q:
为什么没有传参给 inc
,但是 num
却可以一直保持
A:
- step1, 定义了
getCalculator@function
,开辟了 定义上下文(简称getCalculator@context
),在 上下文中就有 num 定义 和 匿名函数 定义 - step2,匿名函数 持有
getCalculator@context
,所以 num 一直对 匿名函数 accessable - step3,调用
getCalculator@function
,进入栈帧,入参值 7 赋值给 num,同时返回 匿名函数 到外部且 inc 持有。 inc 一直被持有,即getCalculator@context
一直保持,即 num 一直保持 - step4,调用
inc@function
,持续更新getCalculator@context.num
的值
内存结构
不论是深绿和浅绿色,都是开辟的内存。外部的绿色,就是 anonymous@function
的定义上下文,inc 可以随意进入,找变量的值。
结论
- 持有方法,就等于持有了定义这方法时的 定义上下文
- _定义上下文,_包含了 父方法定义的变量 和 父方法的入参变量
结论
- js 中的 function 是 第一等公民,他有 独立的内存空间 和 内存地址,可以直接的调用,也可以作为入参传递
- 找变量的时候,没有 this,就找 定义上下文
- 找变量的时候,有 this,就找 运行时上下文