变量、作用域、内存
1. 基本类型和引用类型
如前所述,JS 基本类型有6种:Undefined
, Null
, Bollean
, Number
, String
, Symbol
。
引用类型有1种: Object
。
引用类型存储在内存中,由于 JS 不允许直接访问内存,所以对象的操作实际上都是通过操作地址来完成的。
1.1 动态属性
引用类型可以赋予新属性:
let person = new Object();
person.name = "Nicholas";
console.log(person.name); // "Nicholas"
基本类型不能赋予新属性,但赋予新属性也不报错。
let name = "Nicholas";
name.age = 27;
console.log(name.age); // undefined
同样是 String
类型,使用 new
关键字可以使其变成一个对象。
let name1 = "Nicholas";
let name2 = new String("Matt");
name1.age = 27;
name2.age = 26;
console.log(name1.age); // undefined
console.log(name2.age); // 26
console.log(typeof name1); // string
console.log(typeof name2); // object
1.2 函数传参
函数中传的参数都是值,函数外的参数,都会赋值一份传到函数内来。
引用类型传参:
var person = {name:'Mike'};
function foo(obj) {
obj.name = 'Matt';
}
foo(person);
console.log(person.name); // Matt
常见问题:
function setName(obj) {
obj.name = "Nicholas";
obj = new Object();
obj.name = "Greg";
}
let person = new Object();
setName(person);
console.log(person.name); // "Nicholas"
1.3 instanceof
判断对象是否是某个引用类型。
console.log(person instanceof Object); // is the variable person an Object?
console.log(colors instanceof Array); // is the variable colors an Array?
console.log(pattern instanceof RegExp); // is the variable pattern a RegExp?
2. 执行上下文和作用域
本节部分内容参考自彻底理解作用域链.
执行上下文定义了变量或函数可以访问的数据,以及其行为。所有的执行上下文都有用一个相关的对象,这个对象上储存了所有的变量及函数。
执行上下文保存着函数执行所需要的重要信息,其中有三个属性:变量对象(variable object),作用域链(scope chain),this 指针(this value),它们影响着变量的解析、变量作用域、函数的this指向。
2.1 执行上下文 — 变量对象
每次执行一个函数前,都会创建一个上下文对象。这个上下文对象有一个重要属性:变量对象。变量对象的创建过程如下:
- 将
arguments
对象放入变量对象; - 将要执行的函数内的所有函数放入变量对象;
- 将要执行的函数内的所有变量放入变量对象。
函数执行时,变量对象就被称作活动对象(activation object)了。
变量对象使得 JS 有变量提升的特性:
-
在函数执行前,JS 引擎会先扫一遍代码,将
arguments
、变量和函数都放入变量对象;此时变量对象中所有的变量为undefined
, 所有的函数都有一个地址。 -
在函数执行时,遇到活动对象中相应的属性,就会直接从活动对象取出使用,而不用等该属性赋值(因为没赋值,所以是
undefined
)。
function fun1(arg) {
//执行前创建变量对象:{arg:666, fun2:fun2的地址, a:undefined}
console.log(a); // undefined, 虽然还没碰到a,但活动对象中已经有a了(变量提升)
var a = 123; // 活动对象中的 a 变为 123
console.log(a); // 123
fun2(); // Hello
return; // 即使是在return之后的声明,也会被放入变量对象!
function fun2() {
console.log('Hello');
}
}
fun1(666);// undefined 123 Hello
在浏览器中,全局的变量对象就是 window
对象。
2.2 执行上下文 — 作用域链
作用域链就是变量对象的数组。作用域链的第一个是当前函数的活动对象,第二个是当前函数父级上下文的活动对象,第三个是当前函数爷爷级上下文的活动对象……最后一个是全局上下文活动对象。当 js 执行过程中解析一个变量名时,会沿着当前执行函数的作用域链查找,如果在某个活动对象中找到了它,则使用它,找不到则报错。
例如:
var global_var = -1;
function outter() {
var outter_var = 111;
console.log(`globle_var = ${global_var}, outer_var = ${outter_var}`);
function inner() {
var inner_var = 222;
console.log(`global_var = ${global_var}, outer_var = ${outter_var}, inner_var = ${inner_var}`)
}
}
outer();
// in outter, global_var = -1, outter_var = 111
// in inner, global_var = -1, outter_var = 111, inner_var = 222
用上述代码解释作用域链:
- 开始执行
outter
时,作用域链为:[outter
的变量对象,全局的变量对象]。
-
outter
作用域中找不到globle_var
变量,于是沿着作用域链去父级变量对象中找。 - 在父级变量对象中找到了
globle_var
,能正确输出。
- 开始执行
inner
时,作用域链为: [inner
的变量对象,outter
的变量对象, 全局变量对象]执行过程和上面类似。
用作用域链可以更好地理解闭包的思想。
3. 垃圾回收
JS 具有自动垃圾回收机制,会定期对不再使用的变量、对象所占的内存进行释放。局部变量一般在函数执行完成后会被回收,但局部变量在函数执行完仍被使用时,局部变量不会被回收。
function fun1() {
const obj = {};
}
function fun2() {
const obj2 = {};
return obj2;
}
const a = fun1();
const b = fun2();
上述代码中,obj1
对象在函数执行完后被回收,obj2
由于函数执行完后仍在使用(b
指向了 obj2
的地址),则不会被回收
JS 有两种垃圾回收策略:标记清除和引用计数。
3.1 标记清除
本节部分内容参考自javascript 垃圾回收机制.
当变量进入执行上下文时被标记为“进入环境”(in-context),当变量离开执行上下文时被标记为“离开环境”(out-of-context);前者不能被回收,后者可以被回收。
function fun() {
const a = 1; // a 被标记为 进入环境
}
fun(); // 函数执行完毕 a 被标记为 离开环境
3.2 引用计数
统计引用类型变量声明后被引用的次数,当次数为 0 时,该变量将被回收。
function func4 () {
const c = {}; // 引用类型变量 c的引用计数为 0
let d = c; // c 被 d 引用 c的引用计数为 1
let e = c; // c 被 e 引用 c的引用计数为 2
d = {}; // d 不再引用c c的引用计数减为 1
e = null; // e 不再引用 c c的引用计数减为 0 将被回收
}
但是引用计数的方式,有一个相对明显的缺点——循环引用
function func5 () {
let f = {};
let g = {};
f.prop = g;
g.prop = f;
// 由于 f 和 g 互相引用,计数永远不可能为 0
}
像上面这种情况就需要手动将变量的内存释放
f.prop = null;
g.prop = null;
3.3 管理内存
内存占用越小,页面性能越高。当变量不再使用时,最好将其赋值为 null
.
function createPerson(name) {
let localPerson = new Object();
localPerson.name = name;
return localPerson;
}
let globalPerson = createPerson("Nicholas");// do something with globalPersonglobalPerson = null;
注意:将不再使用的变量赋值为 null
并不能自动回收与其关联的内存,这种做法的目的是确保下次发生垃圾回收时,将其移出上下文。
使用 const 和 let 能够提高性能。因为二者是块级作用域,当块级作用域执行完毕后,会提醒GC回收垃圾。
3.4 内存泄漏
- 不必要的的引用会导致内存泄漏。
function setName() {
name = 'Sara'
}
上述代码产生了一个全局变量name
,当函数执行完后,name仍占用内存,这就导致了内存泄漏。所以函数内的变量一定要加上var
,let
,cost
。
- 定时器会导致内存泄漏
let name = 'Jake';
setInterval(() => {
console.log(name);
}, 100);
在定时器执行过程中,GC无法清除正在使用的外部变量的内存。
- 闭包导致内存泄漏
let outer = function() {
let name = 'Jake';
return function() {
return name;
};
};
outer
函数执行完后,name的内存不会清空。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。