前端面试基础知识总结(三):JavaScript

JavaScript ( JS ) 是一种具有头等函数的轻量级,解释型即时编译型的编程语言。虽然它是作为开发 Web 页面的脚本语言而出名的,但是它也被用到了很多非浏览器环境中,例如 Node.js、 Apache CouchDB 和 Adobe Acrobat。JavaScript 是一种基于原型编程多范式、单线程的动态脚本语言,并且支持面向对象、命令式和声明式(如函数式编程)风格。

开头来自MDN,如果有疑问可以参考下面的关键词解释:

  1. 头等函数(First-class Function):当一门编程语言的函数可以被当作变量一样用时,则称这门语言拥有头等函数。例如,在 JavaScript 中,函数可以被当作参数传递给其他函数,可以作为另一个函数的返回值,还可以被赋值给一个变量。
  2. 解释型(interpreted)编程语言:会将代码一句一句直接运行,不需要像编译语言(如 C++)一样,经过编译器先行编译为机器代码,之后再运行。需要要利用解释器在运行期,动态将代码逐句解释为机器代码,或是已经预先编译为机器代码的子程序,之后再运行。
  3. 即时编译型(just-in-time compiled):即时编译器(JIT compiler)可以理解为解释器(Interpreter)中的一个组件,对编译器进行效率优化,运行已经预先编译为机器代码的子程序。
  4. 基于原型编程(prototype-based):原型编程是一种面向对象编程的风格。通过向其它类的实例(对象)中添加属性和方法来创建类,甚至偶尔使用空对象创建类。
  5. 多范式(multi-paradigm):多种编程范式,常见的有命令式编程(Imperative programming)、函数式编程(Functional programming)、面向对象编程(Object-oriented programming)等。
  6. 动态脚本语言(dynamic language):动态语言是指在运行时才确定数据类型的语言,变量使用之前不需要类型声明。脚本语言是一种解释性的语言,脚本通常以文本来保存,只要在被调用时进行解释和编译。

总结一下 JavaScript 的特点

  • 解释型语言,动态脚本语言
  • 弱类型(不需要类型声明)
  • 单线程(异步事件通过事件循环机制处理)
  • 跨平台(浏览器、服务器端)

大纲

JavaScript核心知识点大纲

数据类型

数据类型-大纲

在 JavaScript 规范中,共定义了七种数据类型:

  • 基本数据类型:Number、String、Boolean、Undefined、Null、Symbol
  • 复杂数据类型:Object

1.String 类型

String 类型用于表示由 n 个 16 位 Unicode 字符组成的字符序列,即字符串。

Unicode 符号集采用 2 个字节(即 16 位 bit)表示一个字符,即最多可以表示 65535 个字符,这样基本上可以覆盖世界上常用的文字。实现了把所有的语言都统一到一套编码中。

1.扩展:UTF-8 和 GBK 的关系?

1.UTF-8 是 Unicode 的实现之一,定义 Unicode 符号集如何存储,是为了提高 16 位编码的使用效率。英文使用一个字节,中文使用三个字节来存储。
2.GBK 英文和中文都使用两个字节来存储。

2.常用操作方法(详见菜鸟教程
  • 字符操作:charAt(index),charCodeAt,fromCharCode
  • 字符串提取:substr(start[,length]),substring ,slice(start[,end])
  • 位置索引:indexOf(searchvalue[,start]) ,lastIndexOf
  • 大小写转换:toLowerCase,toUpperCase
  • 模式匹配:match(regexp)search(searchVal)replace(searchVal,newVal)split([separator[,limit]])
  • 其他操作:concat(string1, string2, ..., stringX),trim,localeCompare
3.基本包装类型(String、Number、Boolean 都是)

为了方便的对字符串进行操作,ECMAScript 提供了一个基本包装类型:String 对象 。它是一种特殊的引用类型,JS 引擎每当读取一个字符串的时候,就会在内部创建一个对应的 String 对象,该对象提供了很多操作字符的方法,这就是为什么能对字符串调用方法的原因。

在访问字符串时,JS 引擎内部会自动完成下列处理:

  • 创建 String 类型的一个实例
  • 在实例上调用指定的方法
  • 销毁这个实例

2.Number 类型

双精度 IEEE754 64 位浮点数。JavaScript 能够准确表示的整数范围在 -2^53 到 2^53 之间(不含两个端点),超过这个范围,无法精确表示这个整数。

1.存储结构

存储结构

2.为什么浮点数运算会有误差?(比如0.1 + 0.2 !== 0.3

事实上,这个问题并不只是在 JavaScript 中才会出现,任何使用二进制浮点数的编程语言都会有这个问题。因为十进制转换成二进制再进行运算会丢失精度,计算结果从二进制转换成十进制也会丢失精度。

解决方案:可以把小数转化成整数运算之后再变回小数来解决。

3.数值转换

Number(value)+ value 只能对字符串进行整体转换

parseInt(string[, radix])parseFloat(string) 可以对字符串进行部分转换,即只转换第一个无效字符之前的字符

4.位运算

ECMAScript 中的数值以 64 位双精度浮点数存储,但位运算只能作用于整数,因此要先将 64 位的浮点数转换成 32 位的整数,然后再进行位运算,最后再将计算结果转换成 64 位浮点数存储。常见的位运算有以下几种:

  • &
  • |
  • ~
  • ^ 异或
  • << 左移
  • >> 算数右移(有符号右移) 移位的时候高位补的是其符号位,整数则补 0,负数则补 1
  • >>> 逻辑右移(无符号右移) 右移的时候高位始终补 0
5.四舍五入
  • 向上取整:Math.ceil
  • 向下取整:Math.floor
  • 四舍五入:Math.round
  • 固定精度(小数点后 x 位):toFixed(x)
  • 固定长度:toPrecision(x)
  • 取整:parseInt、位运算

3.Boolean 类型

Boolean 类型只有两个字面值:true 和 false。在 JavaScript 中,所有类型的值都可以转化为与 Boolean 等价的值。

JavaScript 按照如下规则将变量转换成布尔类型:

  • false、0、空字符串(””)、NaN、null 和 undefined 被转换为 false
  • 所有其他值被转换为 true

除了使用Boolean()强制类型转换,下面四种操作也返回 Boolean 值。

1.关系操作符:>,>=,<,<=

当关系操作符的操作数使用了非数值时,要进行数据转换:

  • 如果两个操作数都是数值,则执行数值比较。
  • 如果两个操作数都是字符串,则逐个比较两者对应的字符编码(charCode),直到分出大小为止 。
  • 如果操作数是其他基本类型,则调用 Number() 将其转化为数值,然后进行比较。
  • NaN 与任何值比较,均返回 false 。
  • 如果操作数是对象,则调用对象的 valueOf 方法(如果没有 valueOf ,就调用 toString 方法),最后用得到的结果,根据前面的规则执行比较。
2.相等操作符: ==,!=,===,!==

=== 和 !== 操作符最大的特点是,在比较之前不转换操作数。不同类型的返回false,相同类型进行如下比较:

  • String:true 仅当两个操作数具有相同顺序的相同字符时才返回。
  • Number:true 仅当两个操作数具有相同的值时才返回。+0 并被-0 视为相同的值。如果任一操作数为 NaN,则返回 false。
  • Boolean:true 仅当操作数为两个 true 或两个 false 时才返回 true。

== 和 != 操作符都会先转换操作数,然后再比较它们的相等性。在转换不同的数据类型时,需要遵循抽象相等比较算法。大致概况如下:

  • 如果两个操作数都是对象,则仅当两个操作数都引用同一个对象时才返回 true。
  • 如果一个操作数是 null,另一个操作数是 undefined,则返回 true。
  • 如果两个操作数是不同类型的,就会尝试在比较之前将它们转换为相同类型:
    • 当数字与字符串进行比较时,会尝试将字符串转换为数字值。
    • 如果操作数之一是 Boolean,则将布尔操作数转换为 1 或 0。
    • 如果操作数之一是对象,另一个是数字或字符串,会尝试使用对象的 valueOf()和 toString()方法将对象转换为原始值。
  • 如果操作数具有相同的类型,则将它们按上面===一样的规则进行比较。
3.布尔操作符:!

Boolean() 的规则
PS:利用 ! 的取反的特点,使用 !! 可以很方便的将一个任意类型值转换为布尔值。

4.条件语句:if,while,?

Boolean() 的规则

4.Undefined 类型

Undefined 是 Javascript 中特殊的原始数据类型,它只有一个值,即 undefined,字面意思是:未定义的值。它的语义是,希望表示一个变量最原始的状态,而非人为操作的结果。这种原始状态会在以下 4 种场景中出现:

  • 声明了一个变量,但没有赋值
  • 访问对象上不存在的属性
  • 函数定义了形参,但没有传递实参
  • 使用 void 对表达式求值:ECMAScript 明确规定 void 操作符 对任何表达式求值都返回 undefined

5.Null 类型

Null 是 Javascript 中特殊的原始数据类型,它只有一个值,即 null,字面意思是:“空值”。它的语义是,希望表示一个对象被人为的重置为空对象,而非一个变量最原始的状态。

6.Symbol 类型

Symbol 是 ES6 新增的一种原始数据类型,它的字面意思是:符号、标记,代表独一无二的值。

Symbol 作为基本类型,没有对应的包装类型,也就是说 Symbol 本身不是对象,而是一个函数。因此,在生成 Symbol 类型值的时候,不能使用 new 操作符 。

7.Object 类型

1.存储方式:堆存储
2.内部特性

ECMA-262 第 5 版定义了一些内部特性(attribute),用以描述对象属性(property)的各种特征。ECMA-262 定义这些特性是为了实现 JavaScript 引擎用的,因此在 JavaScript 中不能直接访问它们。为了表示特性是内部值,该规范把它们放在了两对儿方括号中,例如[[Enumerable]]。 这些内部特性可以分为两种:数据属性和访问器属性。

  • 数据属性
    • [[Configurable]]:能否通过 delete 删除属性从而重新定义属性,或者能否把属性修改为访问器属性。该默认值为 true。
    • [[Enumerable]]:表示能否通过 for-in 循环返回属性。默认值为 true。
    • [[Writable]]:能否修改属性的值。默认值为 true。
    • [[Value]]:包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。默认值为 undefined 。
  • 访问器属性
    • [[Get]]:在读取属性时调用的函数。默认值为 undefined
    • [[Set]]:在写入属性时调用的函数。默认值为 undefined

要修改属性默认的特性,必须使用 ECMAScript 5 的 Object.defineProperty() 方法

3.对象创建方式:
  • 对象自变量、new Object()
  • 工厂模式、构造函数、原型模式
  • 组合使用构造函数模式和原型模式:使用这种模式创建对象,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存

1.工厂模式

1
2
3
4
5
function cteateDog(){
const obj = new Object();
// ...
return obj;
}

2.构造函数

1
2
function Dog() {}
const dog = new Dog();

3.原型模式

1
2
3
4
5
6
function myDog(){}
myDog.prototype.name = 'doggy';
myDog.prototype.attack = function(){
//...
};
var dog = new myDog();
4.深拷贝/浅拷贝

深拷贝:对目标的完全拷贝,不像浅拷贝那样只是复制了一层引用,就连值也都复制了。

  1. 使用 JSON.stringify 和 JSON.parse(但不可以拷贝函数、正则等特殊属性)

    注:如果对象中含有一个函数时(很常见),就不能用这个方法进行深拷贝

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    var obj1 = {
    name: "cat",
    show: function () {
    console.log(this.name);
    },
    };
    var obj2 = JSON.parse(JSON.stringify(obj1));

    obj2.name = "pig";

    console.log(obj1.name);
    console.log(obj2.name);

    obj1.show();
    obj2.show(); //函数被丢失
  2. 写一个递归函数去拷贝

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    //对象
    var obj1 = {
    name: "cat",
    show: function () {
    console.log(this.name);
    },
    };

    //这种深拷贝函数不会丢失
    function deepClone(obj) {
    let result = obj;
    if (typeof obj == "object") {
    result = Array.isArray(obj) ? [] : {};
    for (let key in obj) {
    result[key] =
    typeof obj[key] == "object" ? deepClone(obj[key]) : obj[key];
    }
    }

    return result;
    }

    var obj3 = deepClone(obj1);
    //输出cat
    obj3.show();

浅拷贝:浅拷贝就只是复制对象的引用。

  1. Object.assign()
  2. arr.slice()arr.concat()
1
2
3
4
var obj = {base:{name:'Jack'}};
var obj2 = Object.assign({},obj);
obj2.base.name ='Tom'
console.log(obj.base.name,obj2.base.name);//Tom Tom

数据类型的比较

  • == 如果数值不相等,会进行类型转换
  • === 不进行类型转换
  • Object.is===类似,但是对于 NaN,+0,-0 会有特殊处理
    • Object.is(NaN, NaN) true
    • Object.is(+0, -0) false
  • fun1.toString() === fun2.toString() 判断两个函数相等

判断类型的方法

  1. typeof 能判断出基本数据类型(包括 es6 中的 symbol,除了 null)和 object、function
    • null 被检测为 object:JS 在底层存储变量的时候,会在变量的机器码的低位 1-3 位存储其类型信息。前三位都为 0 的话会被判断为 object 类型,null 的二进制表示全是 0,自然前三位也是 0,所以执行 typeof 时返回”object”,这是 JS 的历史遗留 bug。
  2. instanceof 检测某个实例对象的原型链上是否能找到构造函数的 prototype 属性:[object] instanceof [constructor]
    • 例如f instanceof Foo的逻辑是:f 的__proto__一层一层往上,能否对应到 Foo.prototype
    • 对于number,string,boolean这三种类型,只有通过构造函数定义才能检测出,详见下面代码。
  3. constructor:[object].constructor === [constructor]
    • 弊端就是 constructor 所指向的的构造函数是可以被修改的
  4. Object.prototype.toString.call()
  5. 使用 API:如Array.isArray()

注意一下:

1
2
3
4
5
6
7
let n = 1;
typeof n; // number
n instanceof Number; // false
// ---------------------
let n2 = new Number(1);
typeof n2; // object
n instanceof Number; // true

原型

原型-大纲

原型是什么?

JavaScript 常被描述为一种基于原型的语言——每个对象拥有一个原型对象。

原型-大纲

下面举个例子:

1
2
3
4
function Foo() {}
const f1 = new Foo();
console.log(f1.__proto__); // 指向构造函数的原型对象
console.log(Foo.prototype); // 作为函数,指向原型对象

控制台输出:

f1.__proto__Foo.prototype指向的是同一个东西,即:

1
2
3
4
{
constructor: ƒ Foo(), // constructor指向构造函数
[[prototype]]: Object // 原型链中会介绍
}

prototype

总结:

  • 对象
    • __proto__属性,指向原型对象
    • constructor属性,指向构造函数
  • 函数
    • 也是对象,也有__proto__constructor属性
      • 函数的__proto__指向Function.prototype
      • 函数的constructor指向Function
    • 还有prototype属性,指向原型对象(而这个原型对象的constructor属性又指向该函数)

原型链

当访问一个对象的某个属性时,会先在这个对象本身属性上查找,如果没有找到,则会去它的原型对象上查找,这样的链式结构叫做原型链。

原型链

每个对象的__proto__都是指向它的构造函数的原型对象的:

1
f1.__proto__ === Foo.prototype

构造函数是一个函数对象,是通过 Function 构造器产生的:

1
Foo.__proto__ === Function.prototype

原型对象本身是一个普通对象,而普通对象的构造函数都是Object:

1
Foo.prototype.__proto__ === Object.prototype

所有的构造函数都是函数对象(包括Object),函数对象都是 Function 构造产生的:

1
Object.__proto__ === Function.prototype

Object的原型对象也有__proto__属性指向null,null是原型链的顶端:

1
Object.prototype.__proto__ === null

总而言之,原型链的尽头:

  1. 对象继承自构造函数的原型对象
  2. 构造函数的原型对象本身是一个对象,继承的是Object.prototype
  3. Object.prototype的__proto__最终指向null,null是原型链的顶端。

当我们new一个实例的时候,new做了什么?

  1. 为实例开辟内存空间
  2. 把构造函数体内的this指向1中开辟的内存空间
  3. 将新实例的__proto__这个属性指向对应构造函数的prototype属性
  4. 构造函数会帮你把你创建的实例return出来

作用域

作用域-大纲

作用域:本质上就是程序存储和访问变量的规则。作用域就是在这个规则约束下的一个变量、函数、标识符可以被访问的区域。

三种作用域

  • 全局作用域
  • 块级作用域
    • 函数不是块,在语法中的block是指if/else/for/while语句里2个大括号之间的部分。
    • let和const是块级作用域
  • 函数作用域
    • var是全局或某个函数作用域

var、let和const的区别

  • 使用var声明的变量,函数内部作用域,能重复声明覆盖,且存在变量提升现象;
  • 使用let声明的变量,块级作用域,不能重复声明覆盖,不存在变量提升;
  • 使用const声明的是常量,块级作用域,在后面出现的代码中不能再修改该常量的值;

使用总结:

  • 函数内部用var和let都可以,块级作用域最好用let。
  • 在绝大多数情况下,let是可以代替var的,但是如果需要用到一些特殊功能,比如需要变量提升,需要在外部能引用内部定义的变量( 比如 try{ … }catch{ … } 语句块),需要把变量定义为全局的属性的时候,就需要使用var。所以let不能完全替换var。

作用域链和两种工作模式

作用域链:
如果当前作用域中找不到变量,会探出头到外面的作用域中找。在这个查找过程中,层层递进的作用域,就形成了一条作用域链。

两种工作模型:

  • 词法作用域:在代码书写的时候完成划分,作用域链沿着它定义的位置往外延伸(JS等)
  • 动态作用域:在代码运行时完成划分,作用域链沿着它的调用栈往外延伸(Bash、Perl)

JS中词法作用域的例子:

1
2
3
4
5
6
7
8
9
10
11
12
var name = 'xiuyan';

function showName() {
console.log(name);
}

function changeName() {
var name = 'BigBear';
showName();
}

changeName(); // xiuyan

修改词法作用域

eval能修改词法作用域:

eval 函数的入参是一个字符串。当 eval 拿到一个字符串入参后,它会把这段字符串的内容当做一段 js 代码(不管它是不是一段 js 代码),插入自己被调用的那个位置。看下面这个例子:

1
2
3
4
5
6
7
8
9
function showName(str) {
eval(str)
console.log(name)
}

var name = 'aa'
var str = 'var name = "bb"'

showName(str) // 输出 bb

eval (str) 这行代码被执行后作用域内 name 的值就从 ‘aa’ 变成了 ‘bb’,它成功地 “修改” 了词法作用域规则约束下在书写阶段就划分好的作用域。

闭包

闭包-大纲

闭包是为了:保证局部变量常驻在内存中,只能通过固定的方式访问,不可以被所有人访问。

闭包的应用

  1. 模拟私有变量的实现
  2. 柯里化和偏函数
  3. 防抖节流

应用1:模拟私有变量的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var myObject = (function () {
var value = 0; //闭包:可以保护内部值value不被非法更改
return {
increment: function (inc) {
value += typeof inc === 'number' ? inc : 1;
},
getValue: function () {
return value;
}
}
}());
console.log(myObject.getValue()); // 0
myObject.increment(5);
console.log(myObject.getValue()); // 5

应用2:柯里化和偏函数

柯里化:将一个 n 个参数的函数转换成 n 个单参数函数,例如把fn(a,b,c)变成fn(a)(b)(c)

偏函数:固定你函数的某一个或几个参数,然后返回一个新的函数(这个函数用于接收剩下的参数)。偏函数应用是不强调 “单参数” 这个概念的,它的目标仅仅是把函数的入参拆解为两部分。

原有的函数形式与调用方法:

1
2
3
4
5
6
function generateName(prefix, type, itemName) {
return prefix + type + itemName
}

// 调用时一口气传入3个入参
var itemFullName = generateName('大卖网', '母婴', '奶瓶')

偏函数应用改造:

1
2
3
4
5
6
7
8
function generateName(prefix) {
return function(type, itemName) {
return prefix + type + itemName
}
}

// 把3个参数分两部分传入
var itemFullName = generateName('大卖网')('母婴', '奶瓶')

柯里化应用改造:

1
2
3
4
5
6
7
8
9
function generateName(prefix) {  
return function(type) {
return function (itemName) {
return prefix + type + itemName
}
}
}

var itemFullName = generateName('洗菜网')('生鲜')('菠菜')

应用3:防抖节流

防抖:如果短时间内大量触发同一事件,只会执行最后一次函数

1
2
3
4
5
6
7
8
9
function debounce(fn, delay) {
let timer = null;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}

节流:如果短时间内大量触发同一事件,那么在函数执行一次之后,该函数在指定的时间期限内不再工作,直至过了这段时间才重新生效。

1
2
3
4
5
6
7
8
9
10
11
function throttle(fn, delay) {
let timer = null;
return function(...args){
if(!timer){
timer = setTimeout(() => {
timer = null;
fn.apply(this, args);
}, delay)
}
}
}

闭包的问题

由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

this

this-大纲

this是当前执行上下文的一个属性。执行上下文有:全局上下文、函数上下文、Eval上下文。

this的指向

  1. 指向调用它所在方法的那个对象(在调用时决定的)。

    当调用方法没有明确对象时,this 就指向全局对象。在浏览器中,指向 window;在 Node 中,指向 Global。(严格模式下,指向 undefined)

  2. 箭头函数this由书写位置决定(继承执行上下文的this)。
  3. 在立即指向函数,setTimeout/setInterval,this指向window。

    严格模式下,立即执行函数中this=undefined,setTimeout和setInterval中this=window。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 声明位置
var me = {
name: 'aa',
hello: function() {
console.log(`你好,我是${this.name}`)
}
}

var name = 'bb'
var hello = me.hello

// 调用位置
me.hello() // 你好,我是aa
hello() // 你好, 我是bb

稍微改动一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 声明位置
var me = {
name: 'me',
hello: function() {
console.log(`你好,我是${this.name}`)
}
}

var you = {
name: 'you',
hello: function() {
var targetFunc = me.hello
targetFunc()
}
}

var name = 'bb'

// 调用位置
you.hello() // 你好,我是bb

乍一看,会输出you,而不是bb。但为什么会输出bb呢?

看我们例题中的 targetFunc 这个方法,大家之所以第一直觉会认为它的 this 应该指向 you 这个对象,其实还是因为把 “声明位置” 和 “调用位置” 混淆了。我们看到虽然 targetFunc 是在 you 对象的 hello 方法里声明的,但是在调用它的时候,我们是不是没有给 targetFunc 指明任何一个对象作为它前缀? 所以 you 对象的 this 并不会神奇地自动传入 targetFunc 里,js 引擎仍然会认为 targetFunc 是一个挂载在 window 上的方法,进而把 this 指向 window 对象。

其实原理很简单,无论调用函数包了多少层,this是看最后一层调用函数,这个函数被哪个对象调用。

改变this的指向

this绑定的优先级:

  1. 构造函数 new 实例时绑定
  2. 显式绑定
  • 仅改变:bind
  • 改变后直接调用:callapply
    • fn.call(target,arg1,arg2, ...)
    • fn.apply(target, [arg1, ...])

内存管理

内存管理-大纲

内存的数据结构

  • 栈(Stack):函数调用会在内存形成一个”调用记录”,又称”调用帧”(call frame)。所有的调用记录,就形成一个”调用栈”(call stack)。先进后出。
  • 堆(Heap):对象被分配在堆中,堆是一个用来表示一大块(通常是非结构化的)内存区域的计算机术语。
  • 队列(Queue):一个 JavaScript 运行时包含了一个待处理消息的消息队列。先进先出。

内存的生命周期

不管什么程序语言,内存生命周期基本是一致的:

  1. 分配你所需要的内存
  2. 使用分配到的内存(读、写)
  3. 不需要时将其释放\归还

所有语言第二部分都是明确的。第一和第三部分在底层语言中是明确的,但在像JavaScript这些高级语言中,大部分都是隐含的。

1
2
3
var a = 20;  // 在内存中给数值变量分配空间
alert(a + 100); // 使用内存
a = null; // 使用完毕之后,释放内存空间

JS在初始化变量时就会自动分配内存空间(1、2),那么我们主要要探究的就是如何垃圾回收(3)。

垃圾回收机制

垃圾回收算法的中心思想是判断内存是否不再需要

  1. 引用计数垃圾收集

    这是最初级的垃圾收集算法。如果没有引用指向该对象(零引用),对象将被垃圾回收机制回收。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //但是,对象被循环引用时内存发生泄漏
    //下面这个例子:即使这个DOM元素从DOM树中删除了,所占用的内存空间也永远无法释放
    var div;
    window.onload = function(){
    //循环引用
    div = document.getElementById("myDivElement");
    div.circularReference = div;
    //占据大量内存空间
    div.lotsOfData = new Array(10000).join("*");
    };
  2. 标记-清除算法

    这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。

    • 标记阶段:在此阶段,垃圾回收器会从根对象开始遍历,标识可到达对象
    • 清处阶段:会对堆内存从头到尾进行线性遍历,清处没被标识为可到达对象的对象

    循环引用将不再是问题,因为两个对象都无法获取的情况下,将会被垃圾回收器回收。从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法。

JS中的堆内存和栈内存

在js引擎中对变量的存储主要有两种位置,堆内存和栈内存

栈内存主要用于存储各种基本类型的变量,包括Boolean、Number、String、Undefined、Null以及对象变量的指针,这时候栈内存给人的感觉就像一个线性排列的空间,每个小单元大小基本相等。

而堆内存主要负责像对象Object这种变量类型的存储,如下图:

栈内存和堆内存

面试题

有几种方法可以判断是否是数组

  1. 首先 typeof 肯定不行,判读一些引用类型时,不能具体到具体哪一种类型。

  2. 使用instanceofconstructor

1
2
3
const a = [1, 2, 3];
a instanceof Array; // true
a.constructor === Array; // true
  1. 使用 Array.isArray
  2. 使用 Object.prototype.toString.call()

参考

  1. MDN:JavaScript
  2. What is the Difference Between Interpreter and JIT Compiler
  3. 细说 JavaScript 七种数据类型
  4. MDN:相等(==)
  5. 帮你彻底搞懂JS中的prototype、__proto__与constructor(图解)
  6. 慕课网专栏:解锁前端面试体系核心攻略
  7. 浅析JS中的堆内存与栈内存
  8. yangbo5207的文章:内存空间
  9. 由JavaScript堆栈溢出引出的函数式编程思想
  10. MDN:内存管理
  11. MDN:并发模型与时间循环
  12. MDN let/var/const
  13. 比较var和let的区别,理解函数作用域与块级作用域
  14. JavaScript 里的闭包是什么?应用场景有哪些? - tianlu 简单直白的例子