抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

摘要:本文主要总结了JS中容易混淆的知识点。

1 数据类型的判断

1.1 typeof

使用typeof关键字,得到字符串类型的返回结果:

js
1
2
3
4
5
6
7
8
9
10
// 基本类型
console.log(typeof '123');// string
console.log(typeof 123);// number
console.log(typeof false);// boolean
console.log(typeof null);// object
console.log(typeof undefined);// undefined
// 对象|数组|日期|包装类型,均返回object
console.log(typeof {test:123});// object
// 方法返回function
console.log(typeof function(){});// function

凡是通过对象的方式创建的变量,其使用typeof得到的都是object字符串。

另外,基本数据类型中的null使用typeof判断也会得到object字符串。

1.2 instanceof

使用instanceof关键字判断右侧显示调用的原型是否在左侧隐式调用的原型链上:

js
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
26
27
// 基本类型,左侧非对象,没有原型链,直接返回false
console.log('123' instanceof String);// false
console.log(123 instanceof Number);// false
console.log(false instanceof Boolean);// false
console.log(null instanceof Object);// false
console.log(undefined instanceof Object);// false
// 对象返回true
console.log({test:123} instanceof Object);// true
// 数组返回true
console.log([1,2,3] instanceof Array);// true
// 日期返回true
console.log(new Date() instanceof Date);// true
// 包装类型返回true
console.log(new String('123') instanceof String);// true
console.log(new Number(123) instanceof Number);// true
console.log(new Boolean(false) instanceof Boolean);// true
// 构造方法
console.log(Object instanceof Function);// true Object.__proto__ == Function.prototype
console.log(Function instanceof Function);// true Function.__proto__ == Function.prototype
console.log(Object instanceof Object);// true Object.__proto__.__proto__ == Object.prototype
console.log(Function instanceof Object);// true Function.__proto__.__proto__ == Object.prototype
function Func() {}
var func = new Func();
console.log(func instanceof Func);// true func.__proto__ == Func.prototype
console.log(func instanceof Object);// true func.__proto__.__proto__ == Object.prototype
console.log(Func instanceof Function);// true Func.__proto__ == Function.prototype
console.log(Func instanceof Object);// true Func.__proto__.__proto__ == Object.prototype

在创建对象时,会将显示调用赋值给隐式调用。在修改了显示调用的引用地址后,显示调用和隐式调用得到的原型对象将不再相同。

2 函数

2.1 创建方式

创建函数:

js
1
2
3
function func() {
console.log(this);
}

创建匿名函数并赋值给变量:

js
1
2
3
var func = function() {
console.log(this);
}

创建函数对象:

js
1
var func = new Function("console.log(this);");

创建构造函数:

js
1
2
3
function Func() {
console.log(this);
}

创建箭头函数:

js
1
var func = () => {console.log(this);};

2.2 调用方式

创建函数并调用:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 创建函数
function func() {
console.log(this);
}
// 直接调用函数
func();
// 创建构造函数
function Func() {
console.log(this);
}
// 调用构造函数
new Func();
// 创建对象方法
function Person() {
this.func = function() {console.log(this);}
}
// 调用对象方法
new Person().func();

2.3 回调函数

回调函数在定义以后不需要主动调用,当某个事件触发时才会调用。

常见的回调函数:

  • 定时函数,包括使用setTimeout()方法和setInterval()方法传入的函数
  • DOM事件触发的函数,比如onclick()方法绑定的函数
  • AJAX请求回调函数
  • 生命周期回调函数

3 this关键字

任何函数本质上都是通过某个对象来调用的,所有函数内部都有一个this变量,它的值是调用函数的当前对象。

明确this的值:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function func() {
console.log(this);
}
func();// Window {} 直接调用函数,this指向window对象
var func = new Function("console.log(this);");
func();// Window {} 直接调用函数对象,this指向window对象
function Func() {
console.log(this);
}
var func = new Func();// Func {} 调用构造函数,this指向新创建的对象
function Person() {
this.func = function() {console.log(this);}
}
new Person().func();// Person {} 调用对象方法,this指向调用方法的对象
var func = () => {console.log(this);};
func();// Window {} 调用箭头函数,this指向定义函数的环境
var func = function() {
console.log(this);
}
func.call(new String());// String {''} 使用call和apply调用函数,this指向入参指定的对象
console.log(this);// Window {} 在全局作用域中,this指向window对象

4 执行上下文和执行上下文栈

4.1 执行上下文

代码按生命周期分为两种:

  • 全局代码:在全局任意位置有效
  • 局部代码:仅在某段代码内有效,比如代码块、函数等,局部代码内可以使用全局代码

执行上下文分为全局执行上下文和函数执行上下文两种。

全局执行上下文:

  • 在执行全局代码前将window确定为全局执行上下文
  • 对全局数据进行预处理:
    1. 将全局变量的声明提升,赋值为undefined,添加为window的属性
    2. 将全局函数添加为window的方法
    3. 将this对象赋值为window
  • 开始执行全局代码

函数执行上下文:

  • 在调用函数, 准备执行函数体之前, 创建对应的函数执行上下文对象
  • 对局部数据进行预处理:
    1. 将传入的实参赋值到函数定义的实参,添加为执行上下文的属性
    2. 对函数的arguments内部属性赋值实参列表,添加为执行上下文的属性
    3. 将局部变量的声明提升,赋值为undefined,添加为执行上下文的属性
    4. 将函数内声明的函数添加为执行上下文的方法
    5. 将this对象赋值为调用函数的对象
  • 开始执行函数体代码

在计算代码执行产生的上下文时,函数每次被调用都会生成一个函数执行上下文,在代码执行时会生成一个全局执行上下文。

4.2 执行上下文栈

在创建执行上下文之前,JavaScript引擎会创建一个栈来存储管理所有的执行上下文对象:

  1. 在确定全局执行上下文后,将其压入到栈中
  2. 在创建函数执行上下文后,将其压入到栈中
  3. 在当前函数执行完后,将栈顶的对象移除
  4. 在所有代码执行完后,栈中只剩下全局执行上下文

5 作用域和作用域链

5.1 作用域

作用域是某段代码所在的区域,使用目的是为了隔离变量,不同的作用域之间,同名变量不会有冲突,比如覆盖。

按照代码的声明周期可以将作用域分为三种:

  • 全局作用域:全局代码区域
  • 函数作用域:在函数中的代码区域
  • 块作用域:在代码块中的区域,ES6之后才有这个概念

相对于上下文来说,作用域是静止的,在编写代码时就确定了,作用域的确定时间要早于上下文的创建时间。

在统计作用域的数量时,每个函数作为一个函数作用域,整个代码作为一个全局作用域。在ES6以后,每个代码块是一个块作用域。

5.2 作用域链

作用域链是在发生函数嵌套时产生的,使用目的是为了查找变量,特别是当发生了变量冲突时确定变量的值。

在发生函数嵌套时,外层函数作为上级,内层函数作为下级,作用域链的查找方向是从下往上的。

6 闭包

在发生函数嵌套时,并且内部函数引用了外部函数的变量或函数,就会在内部函数产生闭包:

js
1
2
3
4
5
6
7
8
function outer() {
var num = 0;
function inner() {
console.log(num);
}
inner();
}
outer();

闭包是在创建函数执行上下文时产生的对象,作为内部函数的属性,封装了在内部函数中使用的外部函数的变量或函数。

使用闭包的流程:

  • 执行内部函数定义时产生闭包:
    001-闭包产生
  • 在执行内部函数时,会将闭包对象作为函数执行上下文的变量:
    002-闭包使用

闭包的作用:

  1. 延长局部变量的生命周期,局部变量在外部函数执行完后, 仍然存活在内存中
  2. 允许在外部函数之外,通过内部函数修改外部函数的局部变量

闭包的生命周期:

  • 在执行内部函数定义时产生,即在创建内部函数执行上下文时产生
  • 在内部函数被视为垃圾对象时死亡,即不存在指向内部函数的变量引用

闭包会产生内存泄漏的可能,外部函数执行后,局部变量未能被马上释放:

js
1
2
3
4
5
6
7
8
9
10
function outer() {
var num = 0;
function inner() {
console.log(++num);
}
return inner;
}
var inner = outer();
inner();// 1
inner();// 2

解决办法:

  • 尽量少用闭包
  • 在使用闭包后及时手动将内部函数的引用清空,释放局部变量:
    js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function outer() {
    var num = 0;
    function inner() {
    console.log(++num);
    }
    return inner;
    }
    var inner = outer();
    inner();// 1
    inner = null;
    inner();// 报错

7 对象

7.1 创建

创建对象的几种方式:

  • 直接创建:
    js
    1
    2
    3
    4
    5
    6
    7
    8
    var obj = {
    name: '张三',
    setName: function(name) {
    this.name = name;
    }
    };
    obj.setName('李四');
    console.log(obj);// {name: '李四', setName: ƒ}
  • 通过Object构造函数创建:
    js
    1
    2
    3
    4
    5
    6
    7
    var obj = new Object();
    obj.name = '张三';
    obj.setName = function(name) {
    this.name = name;
    }
    obj.setName('李四');
    console.log(obj);// {name: '李四', setName: ƒ}
  • 通过工厂模式创建:
    js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function createPerson(name) {
    var person = {
    name: name,
    setName: function(name) {
    this.name = name;
    }
    };
    return person;
    }
    var person = createPerson('张三');
    person.setName('李四');
    console.log(person);// {name: '李四', setName: ƒ}
  • 通过自定义构造函数创建,推荐:
    js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    function Person(name) {
    this.name = name;
    this.__proto__.setName = function(name) {
    this.name = name;
    };
    }
    var person = new Person('张三');
    person.setName('李四');
    console.log(person);// Person {name: '李四4'}

7.2 继承

当需要在一个类中使用另一个类的属性和方法时,将没有相关属性和方法的类称为子类,将具有相关属性和方法的类称为父类,子类的属性和方法继承自父类。

使用原型链继承:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 定义父类
function Parent() {
this.parent = 'parent property';
}
// 定义子类
function Child() {
this.child = 'child property';
}
// 将父类的对象赋值给子类的显示调用的原型,这样子类的对象就可以通过隐式调用的原型访问父类定义的属性和方法
Child.prototype = new Parent()
// 子类显示调用的原型指向了父类的对象,其constructor属性也指向了父类的构造方法,所以还需要改为子类的构造方法
Child.prototype.constructor = Child
// 创建子类的实例
var child = new Child()
// 通过子类调用父类的方法
console.log(child.parent);// parent property
// 通过子类调用自身的方法
console.log(child.child);// child property

使用父类的构造方法继承:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义父类
function Parent(name) {
this.name = name;
}
// 定义子类
function Child(name) {
// 调用父类的构造方法,将父类的属性设置到子类的对象上
Parent.call(this, name);
}
// 创建子类的实例
var child = new Child('张三')
// 查看子类的属性
console.log(child.name);// 张三

组合继承,同时使用原型链和构造函数:

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 定义父类
function Parent() {
this.parent = 'parent property';
}
// 定义子类
function Child() {
this.child = 'child property';
// 调用父类的构造方法,将父类的属性设置到子类的对象上
Parent.call(this, name);
}
// 将父类的对象赋值给子类的显示调用的原型,这样子类的对象就可以通过隐式调用的原型访问父类定义的属性和方法
Child.prototype = new Parent()
// 子类显示调用的原型指向了父类的对象,其constructor属性也指向了父类的构造方法,所以还需要改为子类的构造方法
Child.prototype.constructor = Child
// 创建子类的实例
var child = new Child()
// 通过子类调用父类的方法
console.log(child.parent);// parent property
// 通过子类调用自身的方法
console.log(child.child);// child property

8 线程机制

进程:

  • 进程是程序的一次执行,是程序在计算机中运行的实体。
  • 进程是系统进行资源分配的最小单位,每个进程都有各自独立的内存空间,各个进程互不干扰。

线程:

  • 线程是进程的子任务,是进程实际的运作单位。
  • 线程是CPU调度的最小单位,线程有独立的栈空间和局部变量,但没有独立的内存空间,一个进程内的多个线程共享进程的存储空间。

JavaScript采用的是单线程机制,作为浏览器脚本语言,主要用途是与用户互动以及操作DOM,使用单线程不需要考虑复杂的同步问题。

使用多线程的系统可以更好的利用CPU资源,提高CPU的运行效率,但也会因为需要频繁创建销毁线程导致占用了一部分性能。

线程池:

  • 线程池可以复用线程,减少线程创建和销毁的次数,减少了性能浪费,提高程序的运行效率。
  • 线程池可以控制线程的最大并发数,避免过高的并发量导致系统卡死。

9 事件机制

定时器的调用问题:

js
1
2
3
4
5
6
7
8
9
10
11
var time = Date.now();
setTimeout(function(name) {
console.log('1秒后打印 ' + name);
console.log(Date.now() - time);
}, 1000, 'test');
setTimeout(function(name) {
console.log('2秒后打印 ' + name);
console.log(Date.now() - time);
}, 2000, 'test');
for(var i = 0; i < 2000000000; i++) {
}

第一个定时函数不是在1秒后执行的,第二个定时函数不是在2秒后执行的,并且两个定时函数是同时执行的。

这是因为JavaScript是单线程的,定时器回调函数只有在运行栈中的代码全部执行完后才有可能执行。

JavaScript引擎执行基本流程:

  • 执行初始化代码,包括对变量赋值、执行普通方法、执行代码、设置定时器、绑定监听事件、发送请求
  • 执行回调函数,包括定时器回调、事件回调、请求回调

事件管理模块:

  • 定时模块,处理定时任务绑定的回调函数
  • DOM模块,处理DOM元素绑定的回调函数
  • AJAX模块,处理请求返回触发的回调函数

事件管理模型运转流程:

  • 在执行初始化代码时,将事件回调函数交给对应的模块管理
  • 当事件发生时,管理模块会将回调函数及其数据添加到回调列队中
  • 只有当初始化代码执行完后,才会遍历读取回调队列中的回调函数执行

评论