引言
由于js是一门只有在声明变量后才能明确类型的语言,并且在任意时刻都可以改变数据类型。这也引起了一些问题
原始值与引用值
原始值就是基本数据类型,引言值就是复杂数据类型
变量在赋值的时候。js会判断如果是原始值,访问时就是按值访问。如果是引用值,访问就是按引用访问
在很多语言中,字符串是使用对象表示的,因此被认为是引用类型。ECMAScript打破了这个惯例。
动态属性
原始值不能动态的crud属性和方法
let name = "Nicholas";
name.age = 27;
console.log(name.age); // undefined
引用值可以动态的crud属性和方法
let person = new Object();
person.name = "Nicholas";
console.log(person.name); // "Nicholas"
原始类型如果通过new关键字赋值,本质上还是Object类型
let name = new String("Matt");
name.age = 26;console.log(name.age); // 26
console.log(typeof name); // object
复制值
原始值之间的赋值是单纯的复制,俩个互不干扰
int num1=5;
int num2=num1;
引用值之间的赋值是地址的复制,地址指向同一片内存空间。操作的也是同一片内存空间
let obj1 = new Object();obj1.name = "Nicholas";let obj2 = obj1;
传递参数
ECMAScript中所有函数的参数都是按值传递的。js的函数参数当接到传递过来的值后,会开辟一个空间将值赋值给函数参数
改变外部的person可能会错误的认为这是按引用传值,其实这是错误的理解
function setName(obj) {obj.name = "Nicholas";}let person = new Object();setName(person);console.log(person.name); // "Nicholas"
如果是引用传值,那么person.name应该是Greg。
function setName(obj) {obj.name = "Nicholas";obj=newObject();obj.name="Greg";}let person = new Object();setName(person);console.log(person.name); // "Nicholas"
确定类型
typdeof对原始类型非常有用,但是我们想知道一个引用类型的具体类型。typdeof就无能无力了
所有变量和Object匹配都会返回true,因为所有引用类型都是用Object实现的。
如果用instanceof检测原始值,则始终会返回false,因为原始值不是对象。
console.log(person instanceof Object); // 变量person是Object吗?
console.log(colors instanceof Array); // 变量colors是Array吗?
console.log(pattern instanceof RegExp); // 变量pattern是RegExp吗?
执行上下文与作用域
变量或函数的上下文决定了它们可以访问哪些数据,以及它们的行为
全局上下文是最外层的上下文,在浏览器中,全局上下文就是我们常说的window对象,因此所有通过var定义的全局变量和函数都会成为window对象的属性和方法。全局上下文在应用程序退出前才会被销毁
局部上下文在其所有代码都执行完毕后会被销毁,包括定义在它上面的所有变量和函数
搜索过程始终从作用域链的最前端开始,然后逐级往后,直到找到标识符。(如果没有找到标识符,那么通常会报错。)
每个上下文都可以到上一级上下文中去搜索变量和函数,但任何上下文都不能到下一级上下文中去搜索。
变量声明
1. 使用var的函数作用域声明
在使用var声明变量时,变量会被自动添加到最接近的上下文
如果变量未经声明就被初始化了,那么它就会自动被添加到全局上下文(在调用函数后会被创建)
function add(num1, num2) {var sum = num1 + num2;return sum;
}
let result = add(10, 20); // 30
console.log(sum); // 报错:sum在这里不是有效变量
var声明会被拿到函数或全局作用域的顶部,位于作用域中所有代码之前
提升让同一作用域中的代码不必考虑变量是否已经声明就可以直接使用(不要使用)
console.log(name); // undefined
var name = 'Jake';
function() {console.log(name); // undefinedvar name = 'Jake';
}
2. 使用let的块级作用域声明
let和var最大的区别是作用域是块级的,换句话说,if块、while块、function块,甚至连单独的块也是let声明变量的作用域
if (true) {let a;
}
console.log(a); // ReferenceError: a没有定义
while (true) {let b;
}
console.log(b); // ReferenceError: b没有定义
function foo() {let c;
}
console.log(c); // ReferenceError: c没有定义// 这没什么可奇怪的// var声明也会导致报错
// 这不是对象字面量,而是一个独立的块
// JavaScript解释器会根据其中内容识别出它来
{let d;
}
console.log(d); // ReferenceError: d没有定义
let与var的另一个不同之处是在同一作用域内不能声明两次
let的行为非常适合在循环中声明迭代变量。使用var声明的迭代变量会泄漏到循环外部,这种情况应该避免
3. 使用const的常量声明
使用const声明的变量必须同时初始化为某个值。一经声明,在其生命周期的任何时候都不能再重新赋予新值。
const a; // SyntaxError:常量声明时没有初始化
const b = 3;
console.log(b); // 3
b = 4; // TypeError: 给常量赋值
const除了要遵循以上规则,其他方面与let声明是一样的:
赋值为对象的const变量不能再被重新赋值为其他引用值,但对象的键则不受限制。
const o1 = {};
o1 = {}; // TypeError:给常量赋值
const o2 = {};
o2.name = 'Jake';
console.log(o2.name); // 'Jake'
垃圾回收
JavaScript是通过自动内存管理实现内存分配和闲置资源的语言。确定哪个变量不会再使用,然后释放它占用的内存。这个过程是周期性的,即垃圾回收程序每隔一定时间就会自动运行
标记清理
从根上下文(通常是全局上下文或当前执行的函数)开始,遍历所有的变量和对象。将所有可达(被引用)的对象标记为“存活”。在标记阶段之后,遍历整个内存空间,将未标记为“存活”的对象判定为垃圾,进行清除。这些未标记的对象会被销毁,其占用的内存将被释放。
引用计数
对每个值都记录它被引用的次数。声明变量并给它赋一个引用值时,这个值的引用数为1。如果同一个值又被赋给另一个变量,那么引用数加1,如果保存对该值引用的变量被其他值给覆盖了,那么引用数减1。当一个值的引用数为0时,就说明没办法再访问到这个值了,因此可以安全地收回其内存了。垃圾回收程序下次运行的时候就会释放引用数为0的值的内存。
这种方法没有办法解决循环引用的问题,即俩个对象互相引用
function problem() {let objectA = new Object();let objectB = new Object();objectA.someOtherObject = objectB;objectB.anotherObject = objectA;
}
内存管理
将内存占用量保持在一个较小的值可以让页面性能更好。优化内存占用的最佳手段就是保证在执行代码时只保存必要的数据。
解除引用。这个建议最适合全局变量和全局对象的属性。局部变量在超出作用域后会被自动解除引用
function createPerson(name){let localPerson = new Object();localPerson.name = name;return localPerson;
}
let globalPerson = createPerson("Nicholas");
// 解除globalPerson对值的引用,
globalPerson = null;
解除引用的关键在于确保相关的值已经不在上下文里了,因此它在下次垃圾回收时会被回收。
1. 通过const和let声明提升性能
由于const和let的块级作用域,让他们可能更早被垃圾回收
2. 隐藏类和删除操作
js引擎V8会在后台配置,让这两个类实例共享相同的隐藏类,因为这两个实例共享同一个构造函数和原型
function Article() {this.title = 'Inauguration Ceremony Features Kazoo Band';
}
let a1 = new Article();
let a2 = new Article();
如果在后面添加如下代码后,此时两个Article实例就会对应两个不同的隐藏类。根据这种操作的频率和隐藏类的大小,这有可能对性能产生明显影响。
a2.author = 'Jake';
为了避免性能的消耗,最好的法子就在构造函数中一次性声明完毕
function Article(opt_author) {this.title = 'Inauguration Ceremony Features Kazoo Band';this.author = opt_author;
}
let a1 = new Article();
let a2 = new Article('Jake');
在代码结束后,即使两个实例使用了同一个构造函数,它们也不再共享一个隐藏类。动态删除属性与动态添加属性导致的后果一样
function Article() {this.title = 'Inauguration Ceremony Features Kazoo Band';this.author = 'Jake';
}
let a1 = new Article();
let a2 = new Article();
delete a1.author;
最佳实践是把不想要的属性设置为null。这样可以保持隐藏类不变和继续共享,同时也能达到删除引用值供垃圾回收程序回收的效果。
function Article() {this.title = 'Inauguration Ceremony Features Kazoo Band';this.author = 'Jake';
}
let a1 = new Article();
let a2 = new Article();
a1.author = null;
3. 内存泄漏
在内存有限的设备上,或者在函数会被调用很多次的情况下,内存泄漏可能是个大问题。
最常见的内存泄露,方法被调用后会在window上挂载一个name属性。只要window不被清除,那么这个属性就一直存储。使用let,const可以避免这种情况的发生
function setName() {name = 'Jake';
}
由于定时器的回调函数一直调用name,所以name永远不会被清除
let name = 'Jake';
setInterval(() => {console.log(name);
}, 100);
js中的闭包很容易造成内存泄露的问题,调用outer()会导致分配给name的内存被泄漏。以上代码执行后创建了一个内部闭包,只要返回的函数存在就不能清理name,因为闭包一直在引用着它
let outer = function() {let name = 'Jake';return function() {return name;};
};