数据结构与类型
数据类型 最新是ECMAScript标准定义了8种数据类型:
- 七种基本数据类型:
- Boolean布尔值,分别为true和false。
- String字符串,表示一串文本值的字符序列。
- null 一个表示null值的特殊关键字。
- undefined 表示变量未被定义时候的关键字
- Number数字,整型或浮点型数字。
- Bigint 任意精度的整数,可以安全的存储和操作大整数,甚至可以超过数字类型的安全整数限制
- Symbol 表示一种实例是唯一且不可改变的数据类型,只能通过Symbol()函数生成。
- 以及引用数据类型Object,包括我们常用到的Array,Function,Date,RegExp等都属于Object。
注意: 基础数据类型的值是不可变的,引用数据类型的值是可变的
如何理解上面这局话,且看两种数据类型的复制。
基础数据类型的复制
var num1 = 5;
var num2 = num1;
基础数据类型的复制是给新的变量分配一个新的地址,新值是被复制变量的一个副本,变量之间互相独立,互不影响
引用数据类型的复制
var obj1 = new Object();
var obj2 = obj1;
obj1.name = "silent";
alert(obj2.name); //"silent"
这也是为什么我们经常复制一个独立对象的时候要通过深克隆的方式,而不是像基础类型直接赋值完成。
变量的类型检测
1. typeof
typeof是我们最常用的检测数据类型的方法,看看上面说的8种数据类型使用typeof会是什么结果。
不是说null是基本数据类型吗?为什么这里返回的是object?答案其实就是历史原因:
在 JavaScript 最初的实现中,JavaScript 中的值是由一个表示类型的标签和实际数据值表示的。对象的类型标签是 0。由于 null 代表的是空指针(大多数平台下值为 0x00),因此,null 的类型标签是 0,typeof null 也因此返回 "object"。曾有一个 ECMAScript 的修复提案,但被拒绝了。该提案会导致 typeof null === 'null'。
注意:除
Function外的所有构造函数的类型都是object,除非是为了区分基本数据类型和引用类型,最好使用其他方法来判断数据类型。
var str = new String('String');
var num = new Number(100);
typeof str; // 返回 'object'
typeof num; // 返回 'object'
var func = new Function();
typeof func; // 返回 'function'
2.instanceof
instanceof运算符用于检测构造函数的prototype是否存在某个实例对象的原型链中。
new String('a') instanceof String //true
'a' instanceof String //false
我们字符串'a'使用instanceof 判断String为false,而new String('a')却可以?因为字符串'a'检查原型链找到的是undefined。
而String()构造函数是从Object衍生而来。
注意:instanceof也并非完全可靠,1.由于变量的原型并不是一层不变的,一旦原型被修改,就可能返回false。2.无法判断多全局对象,比如在多窗口之间进行原型判断,多窗口意味着多全局对象,拥有不同的内置构造函数,比如[] instanceof window.frames[0].Array会返回false
小提示:
要检测对象不是某个构造函数的实例时,你可以这样做:
if (!(mycar instanceof Car)) {
// Do something, like mycar = new Car(mycar)
}
而不要用
if (!mycar instanceof Car)
这段代码永远会得到 false(!mycar 将在 instanceof 之前被处理,所以你总是在验证一个布尔值是否是 Car 的一个实例)。
3. constructor
'javascript中一切皆为对象',而constructor是返回实例对象的构造函数的引用。所有对象都会从它的原型上继承一个 constructor 属性。
注意:null和undefined没有constructor。同样,对象的constructor也是可以改变的,比如:
var a=[];
a.constructor=new Number();
console.log(a.constructor);//Number
function Parent() {}
Parent.prototype.parentMethod = function parentMethod() {};
function Child() {}
Child.prototype = Object.create(Parent.prototype); // 重新定义了Child的prototype属性,会导致Child的construcor属性也发生变化
Child.prototype.constructor = Child; // 还原Child构造函数的constructor属性指向其本身
4. Object.prototype.toString()
该方法会返回一个表示该对象的字符串
变量的内存管理
定义一个变量即给该变量分配一个内存地址,一个原则是:基本数据类型存储于栈中,而引用数据类型存储于堆中。
关于内存的生命周期
- 分配内存
- 使用内存
- 释放内存
内存的分配
let myNumber = 23
JavaScript在执行上面代码时候流程如下:
- 为myNumber定义唯一标识符(identifier);
- 在内存中分配一个地址(运行时候分配);
- 将值23存储到分配的地址中。
内存的使用
在JavaScript中使用分配的内存意味着在其中读写,这可以通过读取或写入变量或对象属性的值,或者将参数传递给函数来实现。
内存的释放
这里最困难的地方是确定何时不再需要分配的内存,它通常要求开发人员确定程序中哪些地方不再需要内存的并释放它。一般来说,我们在函数中定义一个变量,函数使用完后会自动释放变量使用的内存。
垃圾收集
-
标记清除
JavaScript 中最常用的垃圾收集方式是标记清除(mark-and-sweep)。当变量进入环境(例如,在函数中声明一个变量)时,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。而当变量离开环境时,则将其标记为“离开环境”。这个算法由以下步骤组成:
- 这个垃圾收集器构建一个“roots”列表。Root是全局变量,被代码中的引用所保存。在 JavaScript中,“window”就是这样的作为root的全局变量的例子。
- 所有的root都会被监测并且被标志成活跃的(比如不是垃圾)。所有的子代也会递归地被监测。所有能够由root访问的一切都不会被认为是垃圾。
- 所有不再被标志成活跃的内存块都被认为是垃圾。这个收集器现在就可以释放这些内存并将它们返还给操作系统。
-
引用计数
这是最简单的垃圾收集器算法。如果没有引用指向这个对象的时候,这个对象就被认为是“可以作为垃圾收集”。
var o = {
a: {
b:2
}
};
// 两个对象被创建,一个作为另一个的属性被引用,另一个被分配给变量o
// 很显然,没有一个可以被垃圾收集
var o2 = o; // o2变量是第二个对“这个对象”的引用
o = 1; // 现在,“这个对象”的原始引用o被o2替换了
var oa = o2.a; // 引用“这个对象”的a属性
// 现在,“这个对象”有两个引用了,一个是o2,一个是oa
o2 = "yo"; // 最初的对象现在已经是零引用了
// 他可以被垃圾回收了
// 然而它的属性a的对象还在被oa引用,所以还不能回收
oa = null; // a属性的那个对象现在也是零引用了
// 它可以被垃圾回收了
循环引用导致的问题
当遇到循环的时候就会有一个限制。在下面的实例之中,创建两个对象,并且互相引用,因此就会产生一个循环。当函数调用结束之后它们会走出作用域之外,因此它们就没什么用并且可以被释放。但是,基于引用计数的算法认为这两个对象都会被至少引用一次,所以它俩都不会被垃圾收集器收集。
function f(){
var o = {};
var o2 = {};
o.a = o2; // o 引用 o2
o2.a = o; // o2 引用 o
return "azerty";
}
f();
JavaScript中几种常见的内存泄漏
内存一直被使用而不释放,累计下去就会导致内存泄漏。
- 全局变量
一个未声明变量的引用会在全局对象内部产生一个新的变量。在浏览器的情况,这个全局变量就会是window。
function foo(arg) {
bar = "some text";
}
等同于:
function foo(arg) { window.bar = "some text"; }
- 计时器和回调函数
setInterval 在 JavaScript 中是经常被使用的。大多数提供观察者和其他模式的回调函数库都会在调用自己的实例变得无法访问之后对其任何引用也设置为不可访问。 但是在setInterval的情况下,这样的代码很常见。
var serverData = loadData();
setInterval(function() {
var renderer = document.getElementById('renderer');
if(renderer) {
renderer.innerHTML = JSON.stringify(serverData);
}
}, 5000); //每5000ms执行一次
renderer所代表的对象在未来可能被移除,让部分interval 处理器中代码变得不再被需要。然而,这个处理器不能够被收集因为setInterval依然活跃的。如果这个setInterval处理器不能够被收集,那么它的依赖也不能够被收集。这意味这存储大量数据的severData也不能够被收集。
- 闭包 闭包的特性是有权访问外部的自由变量。
var sayName = function(){
var name = 'jozo';
return function(){
alert(name);
}
};
var say = sayName();
say();
sayName返回了一个匿名函数,该函数又引用了sayName的局部变量name,sayName 调用后变量name应该被回收,但是由于say继续引用,导致无法回收。
- 脱离DOM的引用
有时,保存 DOM 节点内部数据结构很有用。假如你想快速更新表格的几行内容,把每一行 DOM 存成字典(JSON 键值对)或者数组很有意义。此时,同样的 DOM 元素存在两个引用:一个在 DOM 树中,另一个在字典中。将来你决定删除这些行时,需要把两个引用都清除。
var elements = {
button: document.getElementById('button'),
image: document.getElementById('image'),
text: document.getElementById('text')
};
function doStuff() {
image.src = 'http://some.url/image';
button.click();
console.log(text.innerHTML);
}
function removeButton() {
document.body.removeChild(document.getElementById('button'));
// 此时,仍旧存在一个全局的 #button 的引用
// elements 字典。button 元素仍旧在内存中,不能被 GC 回收。
}
小结
-
- JavaScript中的数据类型有8种,7种基本变量类型:boolean,string,number,null,undefined,bigInt,symbol和引用数据类型object。
-
- 检测数据类型的方法有typeof,instanceof,constructor,Object.prototype.toString()等;
-
- 基本数据类型存储于栈中,引用数据类型存储于堆中,复制基本数据类型是值的拷贝,互不影响,复制引用类型数据是地址指针的拷贝,互相影响。
-
- 垃圾收集的方法:标记清除和引用计数。
-
- JavaScript常见的内存泄漏:全局变量,没有及时清除的计时器和回调函数,闭包,脱离DOM的引用