变量与作用域
原始值与引用值
保存原始值(6种)的变量是按值访问的,保存引用值(对象)的变量是按引用访问的。
- 数据存储位置
- 值存储——栈
- 引用存储——引用信息存储在栈,内容存储在堆
- 原因
- 基本类型大小固定,占用空间小
- 引用类型大小不固定,影响性能
原始值不能有属性,尽管尝试给原始值添加属性不会报错。
对于原始值和引用值的复制其实都是值复制,但原始值是复制值本身,引用值复制的是值存储的位置,仍然是引用同个对象。
函数传参只有按值传递,不管是原始值还是引用值。
let obj = {};
function setName(obj){
// 地址1的obj
obj.name = 'obj1';
// 生成地址2的obj,并赋值给obj变量,原来的指向还是没有改变。
obj = new Object();
obj.name = 'obj2'
}
setName(obj);
console.log(obj.name) // 'obj1'
几个概念
- 引擎:从头到尾负责整个JavaScript程序的编译和执行过程。
- 编译器:负责语法分析及代码生成的内容。
- 执行上下文:关联着一个变量对象,包含在这个上下文定义的所有变量和函数。
- 全局上下文(windows对象)就是最外层的上下文,let或const声明的不会定义在全局上下文中,但作用域链的解析结果是一样的
- 当函数进入执行流时,函数的上下文会被推进一个上下文栈中。
- 作用域:负责收集和维护所有声明的变量组成的查询,并实施一套严格的规则来控制当前执行代码对变量的访问权限。
- 作用域链:决定各级上下文访问变量和函数的顺序。在函数定义的时候已经确定,在函数对象中的scoped属性中,具体内容取决于定义的所在的位置。
引擎
查询标识符,分为LHS查询和RHS查询:
- LHS:找到某个变量的容器本身,并对它赋值
- 例如在第一个🌰中,
a = 2
就是一个LHS查询
- 例如在第一个🌰中,
- RHS:查找某个变量,获取它的值
- 例如
console.log(a)
就是一个RSH查询
- 例如
编译器
编译过程主要分为三个步骤:
- 词法分析:将字符组成的字符串分解为代码块(词组单元)
- 语法分析:将词组单元流转化为AST树(抽象语法🌲)
- 代码生成:将AST树转化为机器可执行的代码。
举第一个🌰:
var a = 1 ;
变量的赋值操作,实际上分为两步:
- 编译器会在当前的作用域中声明一个变量(如果没声明过),也就是
var a
;- 在运行时,引擎会在作用域中查找该变量,如果能找到就进行赋值,也就是
a = 2
,找不到就会报错。
词法作用域
- 词法作用域(
Lexical Scopes
)是javascript
中使用的作用域类型,词法作用域 也可以被叫做 静态作用域,与之相对的还有 动态作用域(bash语言使用)。 - 词法作用域为函数定义时所在的作用域。
- 无论函数在哪里,在何时被调用,它的词法作用域都只在函数被声明时所处的位置决定。
例子
var a = 3;
function c(){
alert(a);
}
(function(){
var a = 4;
c();
})();
js中变量的作用域链与定义时的环境有关,与执行时无关。
欺骗词法(不推荐使用)
- eval(..)函数
- 通常被用来执行动态创建的代码
- 接受一个字符串,插入的字符串会被当做原本就在那里一样来处理。
- 可能对原本的词法作用域进行了修改。
- with 关键字
- 通常被当做重复引用同一个对象的多个属性快捷方式,可以不用重复引用对象本身
var obj = {
a: 1,
b: 2,
c: 3
}
with(obj) {
a = 3;
b = 4;
c = 5;
}
- with声明实际上是根据传递的对象,凭空创建了一个全新的词法作用域,当对象中的标识符在全局作用域也找不到时,会自动创建一个全局变量。
函数作用域
属于这个函数的全部变量都可以在整个函数的范围内使用及复用。
- 在外部使用一个函数包裹,可以隐藏内部实现。
- 规避冲突
- 使用全局命名空间
- 使用模块管理
(function fn(){...})
作为函数表达式,说明foo只能在...所代表的位置中被访问,外部作用域则不行,不会污染外部作用域。- 匿名函数表达式和具名函数表达式
- 匿名的缺点
- 在栈追踪中不会显示有意义的函数名,调试困难
- 不方便自身引用
- 代码可读性差
- 匿名的缺点
- 立即执行函数表达式(IIFE)
(function(){})()
- 用法
- 通过函数作用域解决命名冲突
- 把他们当做函数调用并传递参数进去
- 倒置代码的运行数据
作用域嵌套
- 实际情况中,通常需要同时顾及几个作用域。在当前作用域找不到时,引擎就会沿着作用域链在外层嵌套的作用域中继续查找,直到在全局作用域也找不到时,查找停止。
- 作用域中的对象也有一个原型链,因此搜索可能设计每个对象的原型链。
可能发生的两种异常类型:
- ReferenceError:作用域判别失败相关
- TypeError:作用域判别成功,但对结果的操作是不合法的。
变量声明
var
- 变量提升
- 出现变量提升是因为Js的执行机制是 先编译再执行 。
- 在编译阶段,变量和函数会被存放到变量环境中,变量的默认值会被设置为undefined;在代码执行阶段,JavaScript 引擎会从变量环境中去查找自定义的变量和函数。
- 对于同名函数和变量的处理
- 如果是同名的函数,JavaScript编译阶段会选择最后声明的那个。
- 如果变量和函数同名,那么在编译阶段,变量的声明会被忽略。(函数提升优先于变量提升)
- 函数提升会把整个函数挪到作用域顶部,变量提升只会把声明挪到作用域顶部
- 每个作用域都会进行提升操作。
- 重复的var声明会被忽略
例子
(function(){
var x = y = 1;
})();
var z;
console.log(y); // 1
console.log(z); // undefined
console.log(x); // Uncaught ReferenceError: x is not defined
var x = y = 1; 实际上这里是从右往左执行的,首先执行y = 1, 因为y没有使用var声明,所以它是一个全局变量,然后第二步是将y赋值给x,讲一个全局变量赋值给了一个局部变量,最终,x是一个局部变量,y是一个全局变量,所以打印x是报错。
function a() {
var temp = 10;
function b() {
console.log(temp); // 10
}
b();
}
a();
function a() {
var temp = 10;
b();
}
function b() {
// 报错 Uncaught ReferenceError: temp is not defined
console.log(temp);
}
a();
第二个函数a会覆盖第一个函数a,b函数中temp是全局中的temp变量,没有定义,所以为undefined
f = function() {return true;};
g = function() {return false;};
(function() {
if (g() && [] == ![]) {
f = function f() {return false;};
// function g() {return true;}
// 如果放在此处会报错:Uncaught TypeError: g is not a function
// 放在此处。函数声明会提前,但是函数体不会提前,为undefined
}
function g() {return true;} // 放在此处,函数声明会提前
})();
console.log(g()); // false
console.log(f()); // false
- 自执行函数内,函数不会覆盖外面的函数
[] == ![]
:首先![]
会转化为!true
,为false
,为0
,而左边[]
会被求值,为''
,为0
,所以条件成立。
let和const
用于定义块作用域,简单来说,花括号内 {...}
的区域就是块级作用域区域。
- 重复的let声明会报错SyntaxError
- 严格来讲,let在运行时也会被提升,但由于存在暂时性死区,所以实际中不能在声明之前使用。
- 暂时性死区:用let/const声明的变量会先在作用域中被创建出来,但因此时还未进行词法绑定,所以是不能被访问的,如果访问就会抛出错误。因此,在这运行流程进入作用域创建变量,到变量可以被访问之间的这一段时间,就称之为暂时性死区。
- const声明只应用于顶层对象,也就是说,声明对象,对象的属性还是可以修改的。
- v8优化:运行时编译器会将所有实例都替换成实际的值,而不会通过查询表进行变量的查询。
闭包
什么是闭包
函数 A 内部有⼀个函数 B,函数 B 可以访 问到函数 A 中的变量,那么函数 B 就是闭包
形成闭包的原因
内部函数引用外部作用域的变量
js如何实现闭包
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
var foo = checkscope();
foo();
我们知道 f 执行上下文维护了一个作用域链:
fContext = {
Scope: [AO, checkscopeContext.AO, globalContext.VO],
}
就是因为这个作用域链,f 函数依然可以读取到 checkscopeContext.AO
的值,说明当 f 函数引用了 checkscopeContext.AO
中的值的时候,即使 checkscopeContext
被销毁了,但是 JavaScript 依然会让 checkscopeContext.AO
活在内存中,f 函数依然可以通过 f 函数的作用域链找到它,正是因为 JavaScript 做到了这一点,从而实现了闭包这个概念。
闭包的作用
- 保护函数的私有变量不受外部的干扰。形成不销毁的栈内存。
- 保存,把一些函数内的值保存下来。闭包可以实现方法和属性的私有化。
闭包的场景
- return一个函数
var n = 10
function fn(){
var n =20
function f() {
n++;
console.log(n)
}
return f
}
var x = fn()
x() // 21
fn()就是一个闭包,存在上级作用域的引用,即 n = 20,输出结果为21
2、函数作为参数
var a = 'merlin'
function foo(){
var a = 'foo'
function fo(){
console.log(a)
}
return fo
}
function f(p){
var a = 'f'
p()
}
f(foo())
/* 输出
* foo
/
fo()就是闭包,存在上级作用域的引用,即a = 'foo',console.log 结果为foo
3、自执行函数
var n = 'merlin';
(function p(){
console.log(n)
})()
/* 输出
* merlin
/
同样存在上级作用域的引用,即n = 'merlin',console.log 结果为merlin
4、循环赋值
for(var i = 0; i<10; i++){
(function(j){
setTimeout(function(){
console.log(j)
}, 1000)
})(i)
}
因为存在闭包,所以能依次输出0-9,闭包形成了10个互不干扰的私有作用域。每个作用域里的i都不同
如果去掉自执行函数,就不存在外部作用域的引用,那么每一个setTimeout调用的都是全局的i。
由于js是单线程的,遇到异步代码会先入栈,不先执行,等到同步代码执行完后才执行,此时的全局i为10,所以输入的为10个10
5、回调函数
window.name = 'merlin';
setTimeout(function timeHandler(){
console.log(window.name);
}, 100);
闭包需要注意什么
容易导致内存泄漏。闭包会携带包含其它的函数作用域,因此会比其他函数占用更多的内存。
过度使用闭包会导致内存占用过多,所以要谨慎使用闭包。
例子
function fun(n, o) {
console.log(o)
return {
fun: function(m){
return fun(m, n);
}
};
}
var a = fun(0); a.fun(1); a.fun(2); a.fun(3);
// a: {fun:function(m){ return fn(m, 0); }}
var b = fun(0).fun(1).fun(2).fun(3);
// b: {fun:function(m){ return fn(m, 0); }}
// b: {fun:function(m){ return fn(m, 1); }}
// b: {fun:function(m){ return fn(m, 2); }}
var c = fun(0).fun(1); c.fun(2); c.fun(3);
// b: {fun:function(m){ return fn(m, 0); }}
// b: {fun:function(m){ return fn(m, 1); }}