《JavaScript 闯关记》之垃圾回收和内存管理

垃圾回收和内存管理

JavaScript 具有自动垃圾收集机制(GC:Garbage Collecation),也就是说,执行环境会负责管理代码执行过程中使用的内存。而在 C 和 C++ 之类的语言中,开发人员的一项基本任务就是手工跟踪内存的使用情况,这是造成许多问题的一个根源。

在编写 JavaScript 程序时,开发人员不用再关心内存使用问题,所需内存的分配以及无用内存的回收完全实现了自动管理。这种垃圾收集机制的原理其实很简单:找出那些不再继续使用的变量,然后释放其占用的内存。为此,垃圾收集器会按照固定的时间间隔(或代码执行中预定的收集时间),周期性地执行这一操作。

正因为垃圾回收器的存在,许多人认为 JavaScript 不用太关心内存管理的问题,但如果不了解 JavaScript 的内存管理机制,我们同样非常容易成内存泄漏(内存无法被回收)的情况。

垃圾回收机制

内存的分配场景

1
2
3
4
5
// 1.对象
new Object();
new MyConstructor();
{ a: 4, b: 5 }
Object.create();
1
2
3
// 2.数组 
new Array();
[ 1, 2, 3, 4 ];
1
2
3
// 3.字符串,JavaScript 的字符串和 .NET 一样,使用资源池和 copy on write 方式管理字符串。
new String("hello hyddd");
"<p>" + e.innerHTML + "</p>"
1
2
3
// 4.函数
var x = function () { ... }
new Function(code);
1
2
3
4
5
6
7
// 5.闭包 
function outer(name) {
var x = name;
return function inner() {
return "Hi, " + name;
}
}

内存的生命周期

下面我们来分析一下函数中局部变量的正常生命周期。

  • 内存分配:局部变量只在函数执行的过程中存在。而在这个过程中,会为局部变量在栈(或堆)内存上分配相应的空间,以便存储它们的值。
  • 内存使用:然后在函数中使用这些变量,直至函数执行结束。
  • 内存回收:此时,局部变量就没有存在的必要了,因此可以释放它们的内存以供将来使用。

通常,很容易判断变量是否还有存在的必要,但并非所有情况下都这么容易就能得出结论(例如:使用闭包的时)。垃圾收集器必须跟踪哪个变量有用哪个变量没用,对于不再有用的变量打上标记,以备将来收回其占用的内存。用于标识无用变量的策略可能会因实现而异,但具体到浏览器中的实现,则通常有两个策略:标记清除引用计数

阅读全文

《JavaScript 闯关记》之原型及原型链

原型链是一种机制,指的是 JavaScript 每个对象都有一个内置的 __proto__ 属性指向创建它的构造函数的 prototype(原型)属性。原型链的作用是为了实现对象的继承,要理解原型链,需要先从函数对象constructornewprototype__proto__ 这五个概念入手。

函数对象

前面讲过,在 JavaScript 里,函数即对象,程序可以随意操控它们。比如,可以把函数赋值给变量,或者作为参数传递给其他函数,也可以给它们设置属性,甚至调用它们的方法。下面示例代码对「普通对象」和「函数对象」进行了区分。

普通对象:

1
2
var o1 = {};
var o2 = new Object();

函数对象:

1
2
3
function f1(){};
var f2 = function(){};
var f3 = new Function('str','console.log(str)');

简单的说,凡是使用 function 关键字或 Function 构造函数创建的对象都是函数对象。而且,只有函数对象才拥有 prototype (原型)属性。

constructor 构造函数

函数还有一种用法,就是把它作为构造函数使用。像 ObjectArray 这样的原生构造函数,在运行时会自动出现在执行环境中。此外,也可以创建自定义的构造函数,从而自定义对象类型的属性和方法。如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
console.log(this.name);
};
}

var person1 = new Person("Stone", 28, "Software Engineer");
var person2 = new Person("Sophie", 29, "English Teacher");

在这个例子中,我们创建了一个自定义构造函数 Person(),并通过该构造函数创建了两个普通对象 person1person2,这两个普通对象均包含3个属性和1个方法。

你应该注意到函数名 Person 使用的是大写字母 P。按照惯例,构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头。这个做法借鉴自其他面向对象语言,主要是为了区别于 JavaScript 中的其他函数;因为构造函数本身也是函数,只不过可以用来创建对象而已。

阅读全文

《JavaScript 闯关记》之作用域和闭包

作用域和闭包是 JavaScript 最重要的概念之一,想要进一步学习 JavaScript,就必须理解 JavaScript 作用域和闭包的工作原理。

作用域

任何程序设计语言都有作用域的概念,简单的说,作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期。在 JavaScript 中,变量的作用域有全局作用域和局部作用域两种。

全局作用域(Global Scope)

在代码中任何地方都能访问到的对象拥有全局作用域,一般来说以下三种情形拥有全局作用域:

1.最外层函数和在最外层函数外面定义的变量拥有全局作用域,例如:

1
2
3
4
5
6
7
8
var global = "global";     // 显式声明一个全局变量
function checkscope() {
var local = "local"; // 显式声明一个局部变量
return global; // 返回全局变量的值
}
console.log(global); // "global"
console.log(checkscope()); // "global"
console.log(local); // error: local is not defined.

上面代码中,global 是全局变量,不管是在 checkscope() 函数内部还是外部,都能访问到全局变量 global

2.所有末定义直接赋值的变量自动声明为拥有全局作用域,例如:

1
2
3
4
5
6
function checkscope() {
var local = "local"; // 显式声明一个局部变量
global = "global"; // 隐式声明一个全局变量(不好的写法)
}
console.log(global); // "global"
console.log(local); // error: local is not defined.

上面代码中,变量 global 未用 var 关键字定义就直接赋值,所以隐式的创建了全局变量 global,但这种写法容易造成误解,应尽量避免这种写法。

3.所有 window 对象的属性拥有全局作用域

一般情况下,window 对象的内置属性都拥有全局作用域,例如 window.namewindow.locationwindow.top 等等。

阅读全文

如何排版 微信公众号「代码块」之 MarkEditor

前段时间写过一篇文章 如何排版微信公众号「代码块」,讲的是如何使用浏览器插件 Markdown Here 来排版代码块。虽然用 Markdown Here 排版出来的样式还不错,但存在一个问题,就是代码之间的换行会全部丢失,需要手动进行调整。如果文章中代码较多的话,调整起来还是挺费劲的。

而我近期写的文章,常常会罗列大量代码,导致每次在公众号发文,都要花1个多小时来调整样式,真是难受想哭。

双11期间,看到 池建强老师 公众号 MacTalk 的文章 如何优雅的购买 Mac 软件。不出我意料,他果然推荐了一家专卖正版 Mac 软件的淘宝店铺 数码荔枝。秉承「老池推荐必精品」的原则,我如愿的淘到了微信公众号排版神器 MarkEditor。当时,该软件有着「双11特惠」和「老池特惠」双重加持,我仅用了 76.5 就拿下了官网售价 128 的 Pro 版本(得意的笑)。

阅读全文

《JavaScript 闯关记》之事件

JavaScript 程序采用了异步事件驱动编程模型。在这种程序设计风格下,当文档、浏览器、元素或与之相关的对象发生某些有趣的事情时,Web 浏览器就会产生事件(event)。例如,当 Web 浏览器加载完文档、用户把鼠标指针移到超链接上或敲击键盘时,Web 浏览器都会产生事件。如果 JavaScript 应用程序关注特定类型的事件,那么它可以注册当这类事件发生时要调用的一个或多个函数。请注意,这种风格并不只应用于 Web 编程,所有使用图形用户界面的应用程序都采用了它,它们静待某些事情发生(即,它们等待事件发生),然后它们响应。

请注意,事件本身并不是一个需要定义的技术名词。简而言之,事件就是 Web 浏览器通知应用程序发生了什么事情,这种在传统软件工程中被称为观察员模式。

事件流

当浏览器发展到第四代时(IE4 及 Netscape Communicator 4),浏览器开发团队遇到了一个很有意思的问题:页面的哪一部分会拥有某个特定的事件?要明白这个问题问的是什么,可以想象画在一张纸上的一组同心圆。如果你把手指放在圆心上,那么你的手指指向的不是一个圆,而是纸上的所有圆。两家公司的浏览器开发团队在看待浏览器事件方面还是一致的。如果你单击了某个按钮,他们都认为单击事件不仅仅发生在按钮上。换句话说,在单击按钮的同时,你也单击了按钮的容器元素,甚至也单击了整个页面。

事件流描述的是从页面中接收事件的顺序。但有意思的是,IE 和 Netscape 开发团队居然提出了差不多是完全相反的事件流的概念。IE 的事件流是事件冒泡流,而 Netscape Communicator 的事件流是事件捕获流。

事件冒泡

IE 的事件流叫做事件冒泡(event bubbling),即事件开始时由最具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点(文档)。以下面的HTML页面为例:

1
2
3
4
5
6
7
8
9
<!DOCTYPE html>
<html>
<head>
<title>Event Bubbling Example</title>
</head>
<body>
<div id="myDiv">Click Me</div>
</body>
</html>

如果你单击了页面中的 <div> 元素,那么这个 click 事件会按照如下顺序传播:

  1. <div>
  2. <body>
  3. <html>
  4. document

也就是说,click 事件首先在 <div> 元素上发生,而这个元素就是我们单击的元素。然后,click 事件沿 DOM 树向上传播,在每一级节点上都会发生,直至传播到 document 对象。下图展示了事件冒泡的过程。

阅读全文