作用域、变量、this
JS中的词法作用域(静态的作用域)
作用域的定义
定义变量的区域,作用域规定了如何查找变量。
JS 中采用的词法作用域,也就是静态作用域(与之对应的是动态作用域),也就是说变量在定义的时候已经确定。
查找规则是从内到外,依次查找。
函数作用域,是基于函数创建的位置。
- 函数的作用域在函数定义的时候就确定了
- 变量在定义的时候已经确定了
思考题(巩固)
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope();
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()();
最终输出都是"local scope"。然而它们到底有什么不同呢?
执行上下文
这一段是自己的理解,建议查看原文 @冴羽
执行上下文栈
JS逐行扫描按段执行,按段执行是什么意思呢?段是指可执行的代码,可执行代码分为:全局代码、函数代码、eval代码。
如何按照顺序执行的?JS引擎创建一个执行栈,首先遇到的是全局代码,作为全局上下文压入栈底,开始从上开始寻找可执行函数代码,如果函数调用了其他函数将依次入栈。
当一个函数执行完毕后,从执行栈弹出。
function fun3() {
console.log('fun3')
}
function fun2() {
fun3();
}
function fun1() {
fun2();
}
fun1();
// 伪代码
// fun1()
ECStack.push(<fun1> functionContext);
// fun1中竟然调用了fun2,还要创建fun2的执行上下文
ECStack.push(<fun2> functionContext);
// 擦,fun2还调用了fun3!
ECStack.push(<fun3> functionContext);
// fun3执行完毕
ECStack.pop();
// fun2执行完毕
ECStack.pop();
// fun1执行完毕
ECStack.pop();
// javascript接着执行下面的代码,但是ECStack底层永远有个globalContext
变量对象
是执行上下文相关的数据作用域,包含函数参数、声明和变量
全局上下文
全局变量对象就是全局对象
- 在客户端中就是window
this === window - 是Object的一个实例对象
window instanceof Object // true - 作为全局变量的宿主
this.a === a === window.a
补充容易出错的点
let a = 10;
var b = 100;
c = 1000; // 不会变量提升,创建为全局变量,可删除
a // 10
this.a // undefined
this.b // 100
this.c // 1000
delete a; // false
delete b; // false
delete c; // true
函数上下文
在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象。在进入函数上下文时被创建。
执行过程
-
进入执行上下文
- 创建所有形参,并实参赋值(按指传递),如果没有实参,值则为undefined
- 函数声明
- 变量声明
- 函数声明和参数或变量冲突,覆盖
- 变量声明和参数冲突或函数声明冲突,互不影响
-
执行代码
代码执行阶段会顺序执行代码,根据代码修改变量值
思考题
function foo() {
console.log(a);
a = 1; // 没有var 不会变量提升
}
foo(); // not defined
function bar() {
a = 1;
console.log(a);
}
bar(); // 1
console.log(foo); // 输出函数,变量声明不会影响函数声明
function foo(){
console.log("foo");
}
var foo = 1;
function foo(fn){
console.log(fn)
function fn(){
console.log(10)
}
var fn = 100;
}
foo(1000)
// 输出
// fn(){
// console.log(10)
// }
function foo(fn){
console.log(fn)
var fn = 100;
console.log(fn)
}
foo(1000)
// 1000
// 10
作用域链
当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
从规范的角度上理解 this
最终由谁调用就指向谁
var value = 1;
var foo = {
value: 2,
bar: function () {
return this.value;
}
}
//示例1
console.log(foo.bar()); // 2
//示例2
console.log((foo.bar)()); // 2
//示例3
console.log((foo.bar = foo.bar)()); // 1
//示例4
console.log((false || foo.bar)()); // 1
//示例5
console.log((foo.bar, foo.bar)()); // 1
function foo() {
console.log(this)
}
foo(); // window
闭包
两种形式
- 函数作为返回值
- 函数作为参数
表现: 闭包函数携带,函数创建时候的上下文,也就是变量对象。
思考题
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0]();
data[1]();
data[2]();
for (var i = 0; i < 3; i++) {
data[i] = (function (i) {
return function(){
console.log(i);
}
})(i);
}
data[0]();
data[1]();
data[2]();
参数按值传递?
先看例子:
- 例1,确实互不影响,按值传递
var value = 1;
function foo(v) {
v = 2;
console.log(v); //2
}
foo(value);
console.log(value) // 1
- 例2,明显外部obj被修改了
var obj = {
value: 1
};
function foo(o) {
o.value = 2;
console.log(o.value); //2
}
foo(obj);
console.log(obj.value) // 2
- 例3,obj却保持了不变
var obj = {
value: 1
};
function foo(o) {
o = 2;
console.log(o); //2
}
foo(obj);
console.log(obj.value) // 1
实际上就是一次变量赋值的过程
昨天我理解偏了,回到正轨,我们看一下变量的赋值运算。
var value = 1;
var v = value;
v = 2;
console.log(value); // 1
var obj = {value: 1};
var o = obj;
o.value = 2;
console.log(obj.value); // 2
var obj = {value: 1};
var o = obj;
o = 2; // 重新分配了
console.log(obj.value); // 1
1、2、3的变量赋值分别对应上面的参数传递,结果一致。 这块不用想复杂了,就是个赋值操作没有特殊的区别。
在准备执行上下文环境的时候:
1. 首先创建形参并根据实参赋值,
2. 函数声明
3. 变量声明
值传递总结
- 赋值运算按值传递;
- 常量类型直接给值;
- 引用类型传指针地址;
- 如果修改的内部属性,相同的引用的变量都会改变
- 对变量直接赋值,则分配了新的引用地址或常量,断开了之前的联系。