JavaScript学习笔记

学习来源:尚硅谷

学习时间:2022年2月13日

注:基础篇内容较简略,只对重点内容做笔记

1-6为基础篇内容,6以后为高级篇内容

0 补充知识

0.1 DOM和BOM

0.1.1 JS组成

JavaScript的实现包括以下3个部分:

ECMAScript(核心) 描述了JS的语法和基本对象。
文档对象模型 (DOM) 处理网页内容的方法和接口
浏览器对象模型(BOM) 与浏览器交互的方法和接口
  1. DOM 是 W3C 的标准;[所有浏览器公共遵守的标准]
  2. BOM 是 各个浏览器厂商根据 DOM在各自浏览器上的实现;[表现为不同浏览器定义有差别,实现方式不同]
  3. window 是 BOM 对象,而非 js 对象;javacsript是通过访问BOM(Browser Object Model)对象来访问、控制、修改客户端(浏览器)

ECMAScript

  1. ECMAScript是一个标准,JS只是它的一个实现,其他实现包括ActionScript。
  2. “ECMAScript可以为不同种类的宿主环境提供核心的脚本编程能力……”,即ECMAScript不与具体的宿主环境相绑定,如JS的宿主环境是浏览器,AS的宿主环境是Flash。
  3. ECMAScript描述了以下内容:语法、类型、语句、关键字、保留字、运算符、对象。

0.1.2 DOM DOCUMENT BOM WINDOW的区别

  • DOM 是为了操作文档出现的 API,document 是其的一个对象;
  • BOM 是为了操作浏览器出现的 API,window 是其的一个对象。

BOM是浏览器对象模型,DOM是文档对象模型,前者是对浏览器本身进行操作,而后者是对浏览器(可看成容器)内的内容进行操作。

image-20220213135449156

以上图为例:

DOM管理的:

  • E区(document。由web开发人员呕心沥血写出来的一个文件夹,里面有index.html,CSS和JS什么鬼的,部署在服务器上,我们可以通过浏览器的地址栏输入URL然后回车将这个document加载到本地,浏览,右键查看源代码等。)

BOM管理的:

  • A区(浏览器的标签页,地址栏,搜索栏,书签栏,窗口放大还原关闭按钮,菜单栏等等)
  • B区(浏览器的右键菜单)
  • C区(document加载时的状态栏,显示http状态码等)
  • D区(滚动条scroll bar)

1 JS简介

1.1 标准实现

为了确保不同的浏览器上运行的JavaScript标准一致,所以几个公司共同定制了JS的标准名命名为ECMAScript

  • ECMAScript是一个标准,而这个标准需要由各个浏览器厂商去实现

  • 不同的浏览器厂商对该标准会有不同的实现

浏览器 JS实现方式
FireFox SpiderMonkey
IE Chakra
Safari JavaScriptCore
Chrome V8
Carakan Carakan

1.2 三大组成

我们已经知道ECMAScript是JavaScript标准,所以一般情况下这两个词我们认为是一个意思。但是实际上JavaScript的含义却要更大一些。

一个完整的JavaScript实现应该由以下三个部分构成:ECMAScript,DOM,BOM。

image-20220213124217207

1.3 特点

  • JS的特点
    • 解释型语言:JavaScript是一门解释型语言,所谓解释型值语言不需要被编译为机器码再执行,而是直接执行。
    • 类似于 C 和 Java 的语法结构
    • 动态语言
    • 基于原型的面向对象

2 基本语法

2.1 编写位置

JS代码需要编写到<script>标签中。一般将script标签写到head中。

  • 标签属性

    • type:默认值text/javascript,可以不写。

      1
      2
      3
      <script type="text/javascript">  
      //编写js代码
      </script>
    • src:当需要引入一个外部的js文件时,使用该属性指向文件的地址。注意script标签一旦用于引入外部文件了,就不能在编写代码了,即使编写了浏览器也会忽略,如果需要则可以在创建一个新的script标签用于编写内部代码。

      1
      2
      3
      <script type="text/javascript" src="文件路径">
      // 即使写js代码也会被忽略
      </script>

对于分号,可加可不加,但要根据项目配置来添加分号,保持代码风格统一。

对于引号,一般情况下(没有嵌套)在js中单引号和双引号作用是一样的,可凭自己习惯使用,建议养成只使用一种的习惯,有利于代码统一性及可维护。

1
2
3
4
// 以下三种用法均可
console.log("Hello World");
console.log('Hello World');
console.log("Hello" + 'World');

碰到嵌套的时候才会同时用两种引号,规则是外层如果是单引号,内层就是双引号,如果想要在引号内引用变量,则需要跟外层的符号一致。

2.2 变量

变量的作用是给某一个值或对象标注名称。使用var关键字声明一个变量。

1
var a = "Hongyi";

2.3 数据类型

JavaScript中一共有5种基本数据类型:

  1. 字符串型(String
  2. 数值型(Number
  3. 布尔型(Boolean
  4. 空值(Null
  5. 未定义(Undefined

这5种之外的类型都称为Object(对象,引用数据类型),所以总的来看JavaScript中共有六种数据类型。

可以使用typeof操作符来检查一个变量的数据类型。

1
2
3
4
console.log(typeof "123"); // string
console.log(typeof 123); // number
console.log(typeof null); //object
console.log(typeof undefined); //undefined

2.3.1 String

JS中的字符串需要使用引号引起来双引号或单引号都行。

1
var s = "Hongyi";

字符串拼接使用+号。

在字符串中使用\作为转义字符。

1
2
3
4
5
\'  ==> '  
\" ==> "
\n ==> 换行
\t ==> 制表符
\\ ==> \

2.3.2 Number

Number 类型用来表示整数和浮点数,最常用的功能就是用来表示10进制的整数和浮点数。

1
var n = 10;

Number表示的数字大小是有限的,范围是:± 1.7976931348623157e+308,如果超过了这个范围,则会返回± Infinity

1
Number.MAX_VALUE = 1.7976931348623157e+308;

NaN,即非数值(Not a Number)是一个特殊的数值,JS中当对数值进行计算时没有结果返回,则返回NaN。

2.3.3 Boolean

布尔值主要用来进行逻辑判断,布尔值只有两个

  • true 逻辑的真

  • false 逻辑的假

使用typeof检查一个布尔值时,会返回boolean

1
var flag = true; 

2.3.4 Null

空值专门用来表示为空的对象,Null类型的值只有一个

  • null

注意:使用typeof检查一个Null类型的值时会返回object

1
2
var a = null;
console.log(typeof a); // object
  • 从语义上看null表示的是一个空的对象。所以使用typeof检查null会返回一个Object。
  • undefined值实际上是由null值衍生出来的,所以如果比较undefined和null是否相等,会返回true;

2.3.5 Undefined

如果声明一个变量但是没有为变量赋值此时变量的值就是undefined

该类型的值只有一个 undefined

使用typeof检查一个Undefined类型的值时,会返回undefined

1
2
3
4
5
var a;
console.log(typeof a); // undefined

var b = undefined;
console.log(typeof b); // undefined

2.4 类型转换

2.4.1 转换为string

方式①:调用被转换数据的toString()方法(这个方法不适用于null和undefined)

1
2
var a = 123;
a = a.toString();

方式②:调用String()函数

1
2
var a = 123;
a = String(a);

原理:对于Number Boolean String都会调用他们的toString()方法来将其转换为字符串,对于null值,直接转换为字符串”null”。对于undefined直接转换为字符串”undefined”

方式③:隐式的类型转换:任意的数据类型 +""

1
2
var a = true;
a = a + "";

2.4.2 转换为number

方式①:调用Number()函数

1
2
var s = "123";
s = Number(s);

方式②:调用parseInt()parseFloat()。这两个函数专门用来将一个字符串转换为数字的。

1
2
var a = "123";
a = parseInt(a);

方式③:使用一元的+来进行隐式的类型转换

1
2
var a = "123";  
a = +a;

2.5 运算符

2.5.1 逻辑运算符

  • !

  • &&

  • ||

2.5.2 相等运算符

相等,判断左右两个值是否相等,如果相等返回true,如果不等返回false。

相等会自动对两个值进行类型转换,如果对不同的类型进行比较,会将其转换为相同的类型然后再比较,转换后相等它也会返回true,注意:null == undifined

  • !=
    • 不等,判断左右两个值是否不等,如果不等则返回true,如果相等则返回false。不等会做自动的类型转换。
  • ===
    • 全等,判断左右两个值是否全等,它和相等类似,只不过它不会进行自动的类型转换,如果两个值的类型不同,则直接返回false
  • !==
    • 不全等,和不等类似,但是它不会进行自动的类型转换,如果两个值的类型不同,它会直接返回true

一些例子

表达式
null == undefined true
“NaN” == NaN false
5 == NaN false
NaN == NaN true
false == 0 true
true == 1 true
true == 2 false
undefined == 0 false
null == 0 false
“5” == 5 true
“5” === 5 false

3 对象

对象是JS中的引用数据类型。对象是一种复合数据类型,在对象中可以保存多个不同数据类型的属性和方法。使用typeof检查一个对象时,会返回object。

对象除了可以创建自有属性,还可以通过从一个名为原型的对象那里继承属性。

  • 基本数据类型的数据,变量是直接保存的它的值。
  • 引用数据类型的数据,变量是保存的对象的引用(内存地址)

对象分类:

  • 内建对象:由ES标准中定义的对象,在任何的ES的实现中都可以使用。比如:Math String Number Boolean Function Object….
  • 宿主对象:由JS的运行环境提供的对象,目前来讲主要指由浏览器提供的对象,比如 BOM DOM
  • 自定义对象:由开发人员自己创建的对象

3.1 对象操作

3.1.1 创建对象和添加属性

方式①:

1
2
3
var person = new Object();
person.name = "Hongyi";
person.age = 20;

方式②:

1
2
3
4
var person = {
name: "Hongyi",
age: 18
};

3.1.2 访问属性

  • 对象.属性名
  • 对象[“属性名”]
1
2
console.log(person.name);
console.log(person['name']);

循环访问属性

使用for in循环可以遍历对象的属性,例如:

1
2
3
4
5
var obj = {
name: "Hongyi",
age: 18,
gender: "male"
}

遍历:

1
2
3
4
// n为属性名,name,age,gender
for(var n in obj) {
console.log(obj[n]);
}

3.2 函数

函数是由一连串的子程序(语句的集合)所组成的,可以被外部程序调用。向函数传递参数之后,函数可以返回一定的值。

通常情况下,JavaScript 代码是自上而下执行的,不过函数体内部的代码则不是这样。如果只是对函数进行了声明,其中的代码并不会执行。只有在调用函数时才会执行函数体内部的代码。

使用typeof检查一个函数时会返回function。

注意的是JavaScript中的函数也是一个对象。所以函数也有自己的属性。

3.2.1 函数的声明和调用

方式①:函数声明:

1
2
3
function 函数名([形参1,形参2...形参N]){  
// 语句...
}

例如:

1
2
3
function sum(a, b) {
return a + b;
}

调用:

1
var res = sum(1, 1);

方式②:函数表达式:

1
2
3
var 函数名 = function([形参1,形参2...形参N]){  
// 语句...
};

例如:

1
2
3
var sum = function(a, b) {
return a + b;
}

上边的例子就是创建了一个函数对象,并将函数对象赋值给了sum这个变量。其中()中的内容表示执行函数时需要的参数,{}中的内容表示函数的主体。

调用同①。

3.2.2 返回值和方法

return后可以跟任意类型的值,可以是基本数据类型,也可以是一个对象。

如果return后不跟值,或者是不写return则函数默认返回undefined。

1
2
3
4
var sum = function(a, b) {
return ;
}
// return undefined
1
2
3
4
var sum = function(a, b) {
console.log(a + b);
}
// return undefined

方法(method):可以将一个函数设置为一个对象的属性,当一个对象的属性是一个函数时,我们称这个函数是该对象的方法

1
2
3
4
5
6
7
8
9
var obj = new Object();
obj.name = "Hongyi";
// 函数作为对象的一个属性
// 称为该对象的方法
obj.sayName = function() {
console.log(obj.name);
}
// 调用该方法
obj.sayName();

3.2.3 作用域

作用域简单来说就是一个变量的作用范围。

在JS中作用域分成两种:

  • 全局作用域:

    • 直接在script标签中编写的代码都运行在全局作用域中
    • 全局作用域在打开页面时创建,在页面关闭时销毁。
    • 全局作用域中有一个全局对象window,window对象由浏览器提供,可以在页面中直接使用,它代表的是整个的浏览器的窗口。
    • 在全局作用域中创建的变量都会作为window对象的属性保存

      1
      2
      3
      4
      5
      6
      <script>
      var a = 10;
      console.log(a);
      console.log(window.a);
      // 两个语句本质相同
      </script>
    • 在全局作用域中创建的函数都会作为window对象的方法保存

      1
      2
      3
      4
      <script>
      alert("Hello World!");
      window.alert("Hello World!");
      </script>
    • 在全局作用域中创建的变量和函数可以在页面的任意位置访问。

    • 在函数作用域中也可以访问到全局作用域的变量。
    • 尽量不要在全局中创建变量

变量的声明提前

在全局作用域中,使用var关键字声明的变量会在所有的代码执行之前被声明,但是不会赋值。不使用var关键字声明的变量不会被声明提前。

例如:

1
2
3
4
<script>
console.log(a); // undefined
var a = "Hongyi";
</script>

上面这个写法同下:

1
2
3
4
5
<script>
var a; // 声明
console.log(a); // undefined
a = "Hongyi"; // 赋值
</script>

又如:

1
2
3
4
<script>
console.log(a); // 报错:Uncaught ReferenceError: a is not defined
a = "Hongyi";
</script>

函数的声明提前

在全局作用域中,使用函数声明创建的函数(function fun(){}),会在所有的代码执行之前被创建,但是使用函数表达式(var fun = function(){})创建的函数没有该特性。

例如:

1
2
3
4
5
6
<script>
fun();
function fun() {
console.log("Hello World");
}; // 正常执行
</script>

又如:

1
2
3
4
5
6
<script>
fun();
var fun = function() {
console.log("Hello World");
}; // 报错
</script>
  • 函数作用域:
    • 函数作用域是函数执行时创建的作用域,每次调用函数都会创建一个新的函数作用域。
    • 函数作用域在函数执行时创建,在函数执行结束时销毁。
    • 在函数作用域中创建的变量,不能在全局中访问。
    • 当在函数作用域中使用一个变量时,它会先在自身作用域中寻找,如果找到了则直接使用,如果没有找到则到上一级作用域中寻找,如果找到了则使用,找不到则继续向上找,一直会找到全局作用域的变量为止。

3.2.4 函数的属性和方法

属性

在函数内部,有两个特殊的隐含参数:

  • arguments
    • 该对象实际上是一个数组,用于保存函数的实参。注意,即使函数不定义形参,也可以使用arguments来保存实参。
    • 同时该对象还有一个属性callee来表示当前函数。
1
2
3
4
5
6
function fun() {
console.log(arguments.length); // 2
console.log(arguments[0]); // "hello"
};

fun("hello", true);
1
2
3
4
5
6
7
8
function fun() {
console.log(arguments.length); // 2
console.log(arguments[0]); // "hello"
console.log(arguments.callee); // 为函数本身
console.log(arguments.callee == fun); // true
};

fun("hello", true);
  • this
    • this 引用的是一个对象。对于最外层代码与函数内部的情况,其引用目标是不同的。
    • 此外,即使在函数内部,根据函数调用方式的不同,引用对象也会有所不同。需要注意的是,this 引用会根据代码的上下文语境自动改变其引用对象。

方法

函数作为对象,内部也有方法,有apply()call()

这两个方法都是函数对象的方法需要通过函数对象来调用

当对函数调用这两个方法时,都会调用函数执行:

1
2
3
4
5
6
7
8
function fun() {
console.log("Hello World");
};

// 以下三种调用等价
fun();
fun.call();
fun.apply();

不同的是,这两个方法可以通过第一个实参来指定函数中this指向的对象

1
2
3
4
5
6
7
8
9
function fun() {
console.log(this);
};

var obj = {};

fun(); // this --> window
fun.call(obj); // this --> obj
fun.apply(obj); // this --> obj
  • call方法可以将实参在对象之后依次传递
  • apply方法需要将实参封装到一个数组中
1
2
3
4
5
6
7
8
function fun(a, b) {
console.log(this);
console.log(a + b);
};
var obj = {};

fun.call(obj, 1, 2);
fun.apply(obj, [1, 2]);

3.2.5 this上下文对象

浏览器在调用函数时,每次都会向函数内部传递进一个隐含的参数,这个参数就是this

1
2
3
4
function fun(a, b) {
console.log(this);
}
fun();

image-20220213195505882

根据函数的调用方式不同会执向不同的对象

  • 以函数的形式调用时,this是window,例如上例
  • 以方法的形式调用时,this是调用方法的对象
1
2
3
4
5
6
7
8
9
10
11
function fun(a, b) {
// 这里的this,为调用方法的对象obj
console.log(this);
console.log(this.name); // "Hongyi"
}
var obj = {
name: "Hongyi",
func: fun
};

obj.func();

image-20220213202700437

  • 以构造函数的形式调用时,this是新建的那个对象
  • 使用call和apply调用时,this是指定的那个对象
  • 在全局作用域中this代表window

image-20220213203508337

3.2.6 构造函数

构造函数是用于生成对象的函数,像之前调用的Object()就是一个构造函数。

一个构造函数我们也可以称为一个类

通过一个构造函数创建的对象,我们称该对象时这个构造函数的实例

构造函数就是一个普通的函数,只是他的调用方式不同,如果直接调用,它就是一个普通函数;如果使用new来调用,则它就是一个构造函数。

在开发中,通常会区分用于执行的函数和构造函数。

创建一个构造函数和new调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 构造函数首字母一般大写
function Person(name , age , gender){
// 这里的this指向要创建的对象,这里为person
this.name = name;
this.age = age;
this.gender = gender;
// 添加一个方法
this.sayName = function(){
alert(this.name);
};
}
// new调用
var person = new Person("Hongyi", 20, "male");
// 普通调用
var person2 = Person("Hongyi", 20, "male");

构造函数的执行流程:

  1. 创建一个新的对象
  2. 将新的对象作为函数的上下文对象(this)
  3. 执行函数中的代码
  4. 将新建的对象返回

intanceof关键字

作用:用来检查一个对象是否是一个类的实例。如果该对象时构造函数的实例,则返回true,否则返回false

注意:Object是所有对象的祖先,所以任何对象和Object做instanceof都会返回true

1
console.log(person instanceof Person); // true

3.2.7 prototype原型

JS是一门面向对象的语言,而且它还是一个基于原型的面向对象的语言。

创建一个函数以后,解析器都会默认在函数中添加一个属性prototype。prototype属性又指向的是一个对象,这个对象我们称为原型对象,当然,原型对象作为对象而言,自己也有许多的属性。

  • 当函数作为普通函数调用时,prototype没有任何作用和意义
  • 当函数作为构造函数使用,它所创建的对象中都会有一个隐含的属性执行该原型对象。这个隐含的属性可以通过对象.__proto__来访问。

例如:

1
2
3
4
5
6
7
function MyClass() {

};

var mc = new MyClass();

console.log(mc.__proto__ == MyClass.prototype); // true

image-20220213205653962

原型对象也是对象,所以也有prototype属性,所以完整的原型链图如下:

image-20220213211725661

又如:

1
2
console.log(MyClass.prototype);
console.log(mc.__proto__);

image-20220213210012880

原型对象就相当于一个公共的区域,凡是通过同一个构造函数创建的对象他们通常都可以访问到相同的原型对象。

我们可以将对象中共有的属性和方法统一添加到原型对象中,这样我们只需要添加一次,就可以使所有的对象都可以使用。当我们去访问对象的一个属性或调用对象的一个方法时,它会先自身中寻找,如果在自身中找到了,则直接使用。如果没有找到,则去原型对象中寻找,如果找到了则使用,如果没有找到,则去原型的原型中寻找,依此类推。直到找到Object的原型为止,Object的原型的原型为null,如果依然没有找到则返回undefined。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function MyClass() {

};
// 向原型中添加属性a
MyClass.prototype.a = 123;
// 向原型里添加方法
MyClass.prototype.sayHello = function() {
console.log("Hello World");
};

var mc = new MyClass();
console.log(mc.a); // 123

// 向mc中添加a
mc.a = 456;
console.log(mv.a); // 456

// 调用原型里的方法
mc.sayHello();

hasOwnProperty()

作用:这个方法可以用来检查对象自身中是否含有某个属性

1
2
3
4
5
6
7
8
9
10
function MyClass() {

};
// 向原型中添加属性a
MyClass.prototype.a = 123;
var mc = new MyClass();
console.log(mc.hasOwnProperty("a")); // false

mc.a = 456;
console.log(mc.hasOwnProperty("a")); // true

3.3 垃圾回收

不再使用的对象的内存将会自动回收,这种功能称作垃圾回收。

所谓不再使用的对象,指的是没有被任何一个属性(变量)引用的对象。

垃圾回收的目的是,使开发者不必为对象的生命周期管理花费太多精力。

3.4 引用数据类型

上边我们说到JS中除了5种基本数据类型以外其余的全都是对象,也就是引用数据类型。但是虽然全都是对象,但是对象的种类却是非常繁多的。比如我们说过的Array(数组),Function(函数)这些都是不同的类型对象。实际上在JavaScript中还提供了多种不同类型的对象。

3.4.1 Object

目前为止,我们看到的最多的类型就是Object,它也是我们在JS中使用的最多的对象。

创建Object对象有两种方式:

1
2
3
4
var obj = new Object();
var obj = {
// ...
}

但是第一种我们使用了一个new关键字和一个Object()函数。上边的两种方式都可以返回一个Object对象。

这个函数就是专门用来创建一个Object对象并返回的,像这种函数我们称为构造函数。

3.4.2 Array暂略

数组也是一个对象,是一个用来存储数据的对象和Object类似,但是它的存储效率比普通对象要。

Array用于表示一个有序的数组。JS的数组中可以保存任意类型的数据。

1) 创建数组

创建一个数组的方式有两种:

  • 使用构造器:
1
2
3
var arr = new Array(10); // 10为数组长度
var arr = new Array(123, "Hello", true);
console.log(typeof arr); // object
  • 使用[]
1
2
var arr = []; // 创建一个空数组
var arr = [123, "Hello", false];

暂略

3.4.3 Date暂略

暂略

3.4.4 Function

Function类型代表一个函数,每一个函数都是一个Function类型的对象。而且都与其他引用类型一样具有属性和方法。

由于函数是对象,因此函数名实际上也是一个指向函数对象的指针,不会与某个函数绑定。

3.4.5 包装类

在JS中为我们提供了三个包装类:String() Boolean() Number()。通过这三个包装类可以创建基本数据类型的对象。

1
2
3
var num = new Number(2);  
var str = new String("hello");
var bool = new Boolean(true);

实际开发中不用包装类

4 DOM

4.1 概述

DOM,全称Document Object Model文档对象模型。

JS中通过DOM来对HTML文档进行操作。只要理解了DOM就可以随心所欲的操作WEB页面。

  • 文档:文档表示的就是整个的HTML网页文档
  • 对象:对象表示将网页中的每一个部分都转换为了一个对象
  • 模型:使用模型来表示对象之间的关系,这样方便我们获取对象

image-20220214104648109

4.2 节点

节点Node,是构成我们网页的最基本的组成部分,是构成HTML文档最基本的单元。网页中的每一个部分都可以称为是一个节点。

常用节点分为四类:

  1. 文档节点:整个HTML文档
  2. 元素节点:HTML文档中的众多HTML标签
  3. 属性节点:元素节点的属性
  4. 文本节点:HTML标签中的文本内容

image-20220214105003876

  • 节点的属性:nodeName,nodeType,nodeValue
nodeName nodeType nodeValue
文档节点 #document 9 null
元素节点 标签名 1 null
属性节点 属性名 2 属性值
文本节点 #text 3 文本内容

文档节点document

文档节点document,代表的是整个HTML文档,网页中的所有节点都是它的子节点。

document对象作为window对象的属性存在的,我们不用获取可以直接使用。

通过该对象我们可以在整个文档访问内查找节点对象,并可以通过该对象创建各种节点对象。

元素节点Element

HTML中的各种标签都是元素节点,这也是我们最常用的一个节点。

浏览器会将页面中所有的标签都转换为一个元素节点,我们可以通过document的方法来获取元素节点。

例如:document.getElementById():根据id属性值获取一个元素节点对象。

文本节点Text

文本节点表示的是HTML标签以外的文本内容,任意非HTML的文本都是文本节点。

文本节点一般是作为元素节点的子节点存在的。

属性节点Attr

属性节点表示的是标签中的一个一个的属性,这里要注意的是属性节点并非是元素节点的子节点,而是元素节点的一部分

可以通过元素节点来获取指定的属性节点

例如:元素节点.getAttributeNode("属性名")

注意:一般不使用属性节点

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
<body>
<button id="btn">我是一个按钮</button>
<script>
// 获取document对象
console.log(document);
// 获取button对象
var btn = document.getElementById("btn");
console.log(btn);

// 修改按钮的文字
btn.innerHTML = "Hello World";
</script>
</body>

4.3 DOM加载

浏览器在加载一个页面时,是按照自上向下的顺序加载的,加载一行执行一行。如果将js代码编写到页面的上边(例如写在<head>中),当代码执行时,页面中的DOM对象还没有加载,此时将会无法正常获取到DOM对象,导致DOM操作失败。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script>
var btn = document.getElementById("btn");
btn.onclick = function() {
alert("hello");
};
</script>
</head>
<body>
<button id="btn">点我一下</button>
</body>
</html>

执行结果:

image-20220216172606020

理由:按顺序加载至<body>中的script代码时,button尚未生成,因此var btn = null,绑定单击事件失败。

解决

方式①:可以将js代码编写到body的下边

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button id="btn">点我一下</button>
<script>
var btn = document.getElementById("btn");
btn.onclick = function() {
alert("hello");
};
</script>
</body>
</html>

方式②:将js代码编写到window.onload = function(){}中,window.onload 对应的回调函数会在整个页面加载完毕以后才执行,所以可以确保代码执行时,DOM对象已经加载完毕了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script>
window.onload = function() {
var btn = document.getElementById("btn");
btn.onclick = function() {
alert("hello");
};
};
</script>
</head>
<body>
<button id="btn">点我一下</button>
</body>
</html>

4.4 DOM查询暂略

5 事件

我们通过为指定事件绑定回调函数的形式来处理事件,当指定事件触发以后我们的回调函数就会被调用,这样我们的页面就可以完成和用户的交互了。

5.1 事件处理程序

我们可以通过两种方式为一个元素绑定事件处理程序:

  • 通过HTML元素指定事件属性来绑定
  • 通过DOM对象指定的属性来绑定(推荐)

这两种方式都是我们日常用的比较多的,但是更推荐使用第二种方式。

还有一种方式比较特殊我们称为设置事件监听器。使用如下方式:

1
元素对象.addEventListener()

5.1.1 通过HTML标签的属性设置

通过HTML属性来绑定事件处理程序是最简单的方式。

1
<button onclick="alert('hello');alert('world')">按钮</button>

这种方式当我们点击按钮以后,onclick属性中对应的JS代码将会执行,也就是点击按钮以后,页面中会弹出两个提示框。

这种方式我们直接将代码编写到了onclick属性中,可以编写多行js代码,当然也可以事先在外部定义好函数。

这种方式的优点在于,设定步骤非常简单,并且能够确保事件处理程序会在载入时被设定。

5.1.2 通过DOM对象的属性绑定

其实上面的写法虽然简单,但却将JS和HTML的代码编写到了一起,并不推荐使用,我们更推荐如下的写法:

1
2
3
4
5
var btn = document.getElementById('btn');
// 通过DOM对象的属性绑定
btn.onclick = function(){
alert("hello");
};

这种写法将HTML代码和JS写在不同的位置,维护起来更加容易。

5.1.3 设置事件监听器

前边两种方式都可以绑定事件处理程序,但是它们都有一个缺点就是都只能绑定一个程序,而不能为一个事件绑定多个程序。

这时我们就可以使用addEventListener()来处理,这个方法需要两个参数:一个是事件字符串,一个是响应函数

1
2
3
btn.addEventListener("click", function() {
alert("hello");
});

5.1 事件对象

当响应函数被调用时,浏览器每次都会将一个事件对象event作为实参传递进响应函数中,这个事件对象中封装了当前事件的相关信息,比如:鼠标的坐标,键盘的按键,鼠标的按键,滚轮的方向等等。

DOM标准的浏览器会将一个event对象传入到事件的处理程序当中。无论事件处理程序是什么都会传入一个event对象。

可以在响应函数中定义一个形参,来接收该事件对象,但是在IE8以下浏览器中事件对象没有做完实参传递,而是作为window对象的属性保存。可以通过这种方式获取:

1
2
3
4
btn.onclick = function(event){
alert(event.type);
// alert(window.event);
};

Event对象包含与创建它的特定事件有关的属性和方法。触发的事件类型不一样,可用的属性和方法也不一样。

Event对象的通用属性/方法

image-20220216174721695

以下略

6 BOM


以下为高级篇内容

7 基础深入总结

7.1 数据类型

7.1.1 基本数据类型

  1. String: 任意字符串
  2. Number: 任意的数字
  3. boolean: true/false
  4. undefined: undefined
  5. null: null —>注意使用typeof时返回object
  6. symbol(ECMAScript2016新增)。 —>Symbol 是基本数据类型的一种,Symbol 对象是 Symbol原始值的封装 (en-US) 。
  7. bigint —>BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数。

加上下方的 [ 对象 ] 类型,目前 javaScript 有八种数据类型

7.1.2 对象(引用)数据类型

  1. Object: 任意对象
  2. Function: 一种特别的对象(可以执行) —内部包含可运行的代码
  3. Array: 一种特别的对象(key为数值下标属性,内部数据是有序的)

注意:对于引用类型的变量,存储的不是数据本身,而是指向该数据的一个地址值。基本类型的变量保存的就是基本类型的数据。

什么是实例

由构造函数new出来的对象,即为该构造函数的实例对象,简称实例。

1
2
3
4
5
6
7
8
9
10
11
// 构造函数 或称 类型
function Person(name, age) {
this.name = name;
this.age = age;
}
// 根据类型创建的实例对象,简称实例
var p = new Person("Hongyi", 24);

// 构造函数也是函数,也能作为普通函数被调用
// 但不推荐这么写
Person("Jack", 12);

7.1.3 判断数据类型

方式①:typeof

typeof 操作符返回一个字符串,表示未经计算的操作数的类型。

  • 可以判断: undefined/ 数值 / 字符串 / 布尔值 / function
  • 不能判断: null与object object与array
  • 注意: 运行console.log(typeof undefined)时,得到的的也是一个字符串,同时为首字母小写—> 'undefined'
  • 代码示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// typeof返回数据类型的字符串表达
var a

//注意:typeof返回的是字符串
console.log(a, typeof a, typeof a === 'undefined',a === undefined ) // undefined 'undefined' true true
console.log(undefined === 'undefined') //false
a = 4
console.log(typeof a === 'number') //true
a = 'hongjilin'
console.log(typeof a === 'string') //true
console.log(typeof a === 'String') //false -->注意,返回的类型为小写
a = true
console.log(typeof a === 'boolean') //true
a = null
console.log(typeof a, a === null) // 'object' true
let b={}
console.log(typeof b,typeof null, '-------') // 'object' 'object' -->所以Typeof不能判断null与object

方式②:instanceof判断实例方法

  • 专门判断对象的具体类型
  • instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上
  • 代码示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var b1 = {
b2: [1, 'abc', console.log],
//可以简化成 b3:()=>()=> 'hongjilin' -->高阶函数相关知识
b3: function () {
return () =>{ return 'hongjilin'}
}
}

/**使用instanceof进行对象判断*/
console.log(b1 instanceof Object, b1 instanceof Array) // true false
console.log(b1.b2 instanceof Array, b1.b2 instanceof Object) // true true
console.log(b1.b3 instanceof Function, b1.b3 instanceof Object) // true true

/**使用typeof进行对象中某属性的判断*/
console.log(typeof b1.b2, typeof null) // 'object' 'object'
console.log(typeof b1.b3==='function') // true
console.log(typeof b1.b2[2]==='function') //true

/**调用对象与数组中某函数示例*/
b1.b2[2]('调用console.log打印hongjilin') //调用console.log打印hongjilin
console.log(b1.b3()()) // hongjilin

方式③:===

可以判断: undefined, null

简而言之,在比较两件事情时,双等号将执行类型转换;三等号将进行相同的比较,而不进行类型转换 (如果类型不同, 只是总会返回 false )

7.1.4 典型问题

undefinednull区别

  • undefined代表定义未赋值
  • nulll定义并赋值了,只是值为null
1
2
3
4
var a;
console.log(a); // undefined
a = null;
console.log(a); // null

②什么时候给变量赋值为null呢?

  • 初始赋值,表明将要赋值为对象,可以用做约定俗成的占位符
  • 结束前,让对象成为垃圾对象(被垃圾回收器回收)
1
2
3
4
5
6
//起始,可以用做约定俗成的占位符
var b = null // 初始赋值为null, 表明将要赋值为对象
//确定对象就赋值
b = ['atguigu', 12]
//最后在不使用的时候,将其引用置空,就可以释放b这个对象占用的内存---当没有引用指向它的对象称为垃圾对象
b = null // 让b指向的对象成为垃圾对象(被垃圾回收器回收)

7.2 函数

7.2.1 函数的概念

  • 什么是函数

    • 实现特定功能的n条语句的封装体
    • 只有函数是可以执行的, 其它类型的数据不能执行
  • 如何定义函数

    • 函数声明

      1
      2
      3
      function fn() {
      console.log("Hello World");
      }
    • 表达式

      1
      2
      3
      let fn = function() {
      console.log("Hello World");
      }

7.2.2 如何调用函数

例如有一个test函数,有四种方法来调用它:

  1. test(): 直接调用
  2. obj.test(): 通过对象调用,如果obj对象有test函数这个属性
  3. new test(): new调用
  4. test.call/apply(obj): 临时让test成为obj的方法进行调用
1
2
3
4
5
6
7
8
var obj = {}

function test() {
this.name = "Mark";
}
// obj.test() 不能直接调用, obj根本就没有test()这个属性
test.call(obj); // 可以让一个函数成为指定任意对象的方法进行调用
console.log(obj.name); // "Mark"

7.2.3 回调函数

什么函数才是回调函数

  • 你定义的
  • 你没有调
  • 但最终它执行了(在某个时刻或某个条件下)

常见的回调函数

  • dom事件回调函数 ==>发生事件的dom元素
  • 定时器回调函数 ===>window
  • ajax请求回调函数
  • 生命周期回调函数
1
2
3
4
5
6
7
8
9
// dom事件回调函数
document.getElementById('btn').onclick = function() {
alert(this.innerHTML);
}

// 定时器回调函数
setTimeout(function () {
alert('到点了'+this);
}, 2000);

7.2.4 IIFE函数

  1. 全称: Immediately-Invoked Function Expression 立即执行函数,或者称为匿名函数自调用
  2. 作用:
    • 隐藏实现
    • 不会污染外部(一般指全局)命名空间
    • 用它来编码js模块,向外暴露接口
  3. 代码示例
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
28
29
30
// 匿名函数自调用
(function () {
var a = 3;
console.log(a + 3);
})()

var a = 4;
console.log(a); // 4

//此处前方为何要一个`;`-->因为自调用函数外部有一个()包裹,可能与前方以()结尾的代码被一起认为是函数调用
//不加分号可能会被认为这样 console.log(a)(IIFE)

// 向外暴露接口示例
// 这里暴露了test函数
;(function () {
var a = 1;
function test() {
console.log(++a);
}
// 给window新增一个i属性,这个属性为一个函数
// 该函数返回一个对象
// 该对象有一个test属性,值为test函数
window.i = function() {
return {
test: test
}
}
})()
test(); // undefined 直接调用不行
i().test(); // 2 间接调用

7.2.5 函数中的this

this是什么

  • 任何函数本质上都是通过某个对象来调用的,作为方法被这个对象调用,如果没有直接指定就是window
  • 所有函数内部都有一个变量this
  • 它的值是调用函数的当前对象

如何确定this的指向

  • test(): window
  • p.test(): p
  • new test(): 新创建的对象
  • p.call(obj): obj

示例

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
28
29
30
31
function Person(color) {
console.log(this)
this.color = color;
this.getColor = function () {
console.log(this)
return this.color;
};
this.setColor = function (color) {
console.log(this)
this.color = color;
};
}

Person("red"); //this是谁? window

const p = new Person("yello"); //this是谁? p

p.getColor(); //this是谁? p

const obj = {};
//调用call会改变this指向-->让我的p函数成为`obj`的临时方法进行调用
p.setColor.call(obj, "black"); //this是谁? obj

const test = p.setColor;
test(); //this是谁? window -->因为直接调用了

function fun1() {
function fun2() { console.log(this); }
fun2(); //this是谁? window
}
fun1();//调用fun1

7.2.6 分号的使用

下面2种情况下不加分号会有问题,解决办法: 在行首加分号

  • 小括号开头的前一条语句
1
2
3
4
let a = 3
(function () {

})() // 如果都不加分号,则报错
1
2
3
4
5
// 修正1:
let a = 3
;(function () {

})()
1
2
3
4
5
// 修正2:
let a = 3;
(function () {

})()
  • 中方括号开头的前一条语句
1
2
3
4
let b = 4
[1, 3].forEach(function () {

}) // 如果都不加分号,则报错
1
2
3
4
5
// 修正方法1,2同上
let b = 4
;[1, 3].forEach(function () {

})

8 函数高级

8.1 原型与原型链

8.1.1 原型prototype

  • 函数的prototype属性

    • 每个函数都有一个prototype属性,它默认指向一个Object空对象(即称为: 原型对象),所谓空对象是指这个对象没有我们自己定义的属性

      1
      2
      3
      4
      5
      function func() {

      }
      console.log(typeof func.prototype); // object
      console.log(func.prototype);

      image-20220220132127033

    • 原型对象中有一个属性constructor,它指向函数对象

      image-20220220132930460

      image-20220220132811222

      1
      console.log(func.prototype.constructor === func); // true
  • 给原型对象添加属性(一般都是添加方法)

    1
    2
    3
    4
    5
    6
    7
    8
    function func() {

    }
    // 向原型对象添加属性,例如添加一个方法
    func.prototype.test = function() {
    console.log("Hello World");
    }
    console.log(func.prototype);

    image-20220220132415613

    • 作用:函数的所有实例对象自动拥有原型中的属性(方法)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      function Func() {

      }
      // 向原型对象添加属性
      Func.prototype.test = function() {
      console.log("Hello World");
      }
      // 创建一个Func的实例对象fn
      let fn = new Func();
      fn.test(); // "Hello World"
      console.log(fn.__proto__);

      image-20220220133336573

8.1.2 显式原型与隐式原型

  • 每个函数function都有一个prototype,即显式原型(属性),默认指向空的object对象
1
2
3
4
function Func() {

}
console.log(Func.prototype);
  • 每个实例对象都有一个[__proto__],可称为隐式原型(属性)
1
2
let fn = new Func();
console.log(fn.__proto__);
  • 对象的隐式原型的值为其对应构造函数的显式原型的值
1
console.log(Func.prototype === fn.__proto__); //true
  • 显式原型与隐式原型的内存结构图

image-20220220134616488

总结

  • 函数的[prototype]属性: 在定义函数时自动添加的,默认值是一个空Object对象
  • 对象的[__ proto __]属性: 创建对象时自动添加的,默认值为构造函数的prototype属性值
  • 程序员能直接操作显式原型,但不能直接操作隐式原型(ES6之前)

8.1.3 原型链

1) 基本说明

js加载页面时,会将一个内置的构造函数Object()加载进来,这个构造函数也是一个对象(函数),并由变量Object引用;

构造函数存在prototype属性,该prototype指向Object的原型对象。

1
2
console.log(Object);
console.log(Object.prototype);

image-20220220143139930

上面的即为Object原型对象的诸多属性。

2) 原型链图示

原型链——代码及图示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 定义一个函数对象
function Fn() {
this.test1 = function() {
console.log("test1()...");
}
}

Fn.prototype.test2 = function() {
console.log("test2()...");
}
// Fn的实例对象fn
let fn = new Fn();
fn.test1();
fn.test2();
console.log(fn.toString());
fn.test3();

图示:

左侧为栈,保存变量,右侧为内存空间;例如左侧的Fn为变量(引用型),它保存的是Fn函数对象的地址值(或者说,变量Fn指向了这个函数对象)。

image-20220220150510687

原型链的尽头是Object的原型对象。因为其属性__proto__指向了null

原型链的属性访问

访问一个对象的属性时:

  • 先在自身属性中查找,找到返回,例如test1()
  • 如果没有,再沿着[__proto__]这条链向上查找,找到返回,例如test2()toString()
  • 如果最终没找到,返回undefined,例如test3()
3)构造函数/原型/实例对象的关系(图解)

示例1

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

image-20220220151643532

示例2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Foo(){

}
// 本质为
let Foo = new Function();
// 所以有结论:
// 所有函数的__proto__都是一样的

function f1() {

}

function f2() {

}
console.log(f1.__proto__ === f2.__proto__); // true

image-20220220151733699

从上图可以看出:

  • 所有函数都有显式原型和隐式原型两个属性
  • 只有Function构造函数的显式原型和隐式原型是同一个对象。
  • 实例对象的隐式原型是对应构造函数的显式原型

所以有:

1
const Function = new Function();
4) 属性问题

分为读取和设置两种情况:

  • 读取对象的属性值时: 会自动到原型链中查找
  • 设置对象的属性值时: 不会查找原型链,如果当前对象中没有此属性,直接添加此属性并设置其值
  • 方法一般定义在原型中,属性一般通过构造函数定义在对象本身上
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Fn() {

}

Fn.prototype.a = "111";
var fn1 = new Fn();
console.log(fn1);
console.log(fn1.a); // 111

var fn2 = new Fn();
// 在fn2上没有a这个属性,在fn2上添加
fn2.a = "222";
console.log(fn2);
console.log(fn2.a); // 222

console.log(fn1.a); // 111
console.log(fn1.__proto__ === fn2.__proto__); // true

执行结果:

image-20220221142822418

8.1.4 instanceof

  • 表达式: A instanceof B
  • 判断方法:如果B函数的显式原型对象在A对象的原型链上,返回true,否则返回false

案例1

1
2
3
4
5
6
function Foo() {

}
var f1 = new Foo();
console.log(f1 instanceof Foo); // true
console.log(f1 instanceof Object); // true

image-20220221143254600

案例2

1
2
3
4
5
6
7
8
9
console.log(Object instanceof Function); // true
console.log(Object instanceof Object); // true
console.log(Function instanceof Function); // true
console.log(Function instanceof Object); // true

function Foo() {

}
console.log(Object instanceof Foo); // false

image-20220221143424115

8.1.5 面试题

面试题1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
测试题1
*/
var A = function() {

}
A.prototype.n = 1
var b = new A();
A.prototype = {
n: 2,
m: 3
}
var c = new A();
console.log(b.n, b.m, c.n, c.m); // 1 undefined 2 3

第9行代码将A的原型对象指向一个新的对象。

image-20220221144033763

面试题2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
测试题2
*/
var F = function(){

};

Object.prototype.a = function() {
console.log('a()...');
};

Function.prototype.b = function() {
console.log('b()...');
};

var f = new F();
f.a(); // 'a()...'
f.b(); // f.b is not a function
F.a(); // 'a()...'
F.b(); // 'b()...'

图解:

image-20220221145525563

8.2 执行上下文与执行上下文栈

8.2.1 前言

当代码在 JavaScript 中运行时,执行代码的环境非常重要,并将概括为以下几点:

  • 全局代码——第一次执行代码的默认环境。

  • 函数代码——当执行流进入函数体时。

  • (…)—— 我们当作 执行上下文 是当前代码执行的一个环境与范围。

换句话说,当我们启动程序时,我们从全局执行上下文中开始。一些变量是在全局执行上下文中声明的。我们称之为全局变量。当程序调用一个函数时,会发生什么?

以下几个步骤:

  • JavaScript 创建一个新的执行上下文,我们叫作本地执行上下文
  • 这个本地执行上下文将有它自己的一组变量,这些变量将是这个执行上下文的本地变量。
  • 新的执行上下文被推到到执行堆栈中。可以将执行堆栈看作是一种保存程序在其执行中的位置的容器。

函数什么时候结束?当它遇到一个 return 语句或一个结束括号}

当一个函数结束时,会发生以下情况:

  • 这个本地执行上下文从执行堆栈中弹出。
  • 函数将返回值返回调用上下文。调用上下文是调用这个本地的执行上下文,它可以是全局执行上下文,也可以是另外一个本地的执行上下文。这取决于调用执行上下文来处理此时的返回值,返回的值可以是一个对象、一个数组、一个函数、一个布尔值等等,如果函数没有 return 语句,则返回 undefined。
  • 这个本地执行上下文被销毁,销毁是很重要,这个本地执行上下文中声明的所有变量都将被删除,不在有变量,这个就是为什么 称为本地执行上下文中自有的变量。

8.2.2 变量提升与函数提升

  • 变量声明提升

    • 通过var定义(声明)的变量,在定义语句之前就可以访问到

    • 值: undefined

  • 函数声明提升
    • 通过function声明的函数,在之前就可以直接调用
    • 值: 函数定义(对象)
  • 两者出现的原因:浏览器的预处理(或预编译)导致的,参见下一小节

示例

1
2
3
4
5
6
var a = 3;
function fn() {
console.log(a); // undefined
var a = 4; // 变量提升
}
fn();

实际执行为:

1
2
3
4
5
6
7
var a = 3;
function fn() {
var a;
console.log(a); // undefined
a = 4; // 变量提升
}
fn();

所以输出为undefined

示例

1
2
3
4
5
6
7
8
9
10
console.log(b); // undefined
fn2(); // 函数声明提升 fn2()...
fn3(); // 报错 fn3 is not a function
var b = 3;
function fn2() {
console.log("fn2()...");
}
var fn3 = function() {
console.log("fn3()...");
}

注意:fn3是通过表达式声明的,不存在函数声明提升,因此报错。

8.2.3 执行上下文

  • 代码分类(位置)

    • 全局代码

    • 函数(局部)代码

全局执行上下文

  • 在执行全局代码window确定为全局执行上下文
  • 全局数据进行预处理
    • var定义的全局变量==>undefined,添加为window的属性
    • function声明的全局函数==>赋值(fun),添加为window的方法
    • this==>赋值(window)
  • 开始执行全局代码
1
2
3
4
5
6
7
8
9
// 全局执行上下文
console.log(a1, window.a1); // 均为undefined
a2(); // 打印
console.log(this); // window
var a1 = 3;
function a2() {
console.log("Hello World");
}
console.log(a1); // 3

函数执行上下文

  • 在调用函数,准备执行函数体之前(注意不是定义时),创建对应的函数执行上下文对象(虚拟的, 存在于栈中)
  • 局部数据进行预处理
    • 对形参变量\==>用实参进行赋值==>添加为执行上下文的属性
    • arguments==>用实参列表进行赋值,添加为执行上下文的属性
    • 对var定义的局部变量==>赋值为undefined,添加为执行上下文的属性
    • 对function声明的函数 ==>赋值(fun),添加为执行上下文的方法
    • 对this==>赋值(调用函数的对象)
  • 开始执行函数体代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 函数执行上下文
function fn(a1) {
console.log(a1); // 1
console.log(a2); // undefined
a3(); // 执行a3()
console.log(this); // window
console.log(arguments); // 伪数组:1, 2
var a2 = 3;
function a3() {
console.log("Hello World");
}
}

fn(1, 2);

8.2.4 执行上下文栈

  1. 在全局代码执行前,JS引擎就会创建一个栈来存储管理所有的执行上下文对象,包括全局和函数执行上下文对象
  2. 在全局执行上下文(window)确定后,将其添加到栈中(压栈)—>所以栈底百分百是[window]
  3. 在函数执行上下文创建后(创建时机:函数即将被调用前),将其添加到栈中(压栈)
  4. 在当前函数执行完后,将栈顶的对象移除(出栈)
  5. 当所有的代码执行完后,栈中只剩下window
  6. 上下文栈数==函数调用数+1

代码示例

1
2
3
4
5
6
7
8
9
10
11
// 1.进入全局执行上下文
var a = 10;
var bar = function (x) {
var b = 5;
foo(x + b); // 3.进入foo函数执行上下文
};
var foo = function (y) {
var c = 5;
console.log(a + c + y);
};
bar(10); // 2.进入bar函数执行上下文

image-20220221153337980

一般情况下的执行上下文栈

image-20220221153500499

8.2.5 测试题

函数提升优先级高于变量提升,且不会被变量声明覆盖,但是会被变量赋值覆盖

  • 测试题1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
测试题1:
*/
function a() {

}
var a;
console.log(typeof a); // function
//---------------------------------------
function b() {

}

var b = 1; // 函数提升被变量赋值覆盖
console.log(typeof b); // number
  • 测试题2
1
2
3
4
5
6
7
/*
测试题2:
*/
if (!(b in window)) {
var b = 1;
}
console.log(b); // undefined
  • 测试题3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/*
测试题3:
*/
var c = 1;
function c(c) {
console.log(c);
}
c(2); // 报错 c is not a function

//---------------------------------
// 实际执行顺序
// 先函数提升
function c(c) {
console.log(c);
}
// 后变量提升,但不会覆盖函数提升
var c;
console.log(typeof c); // function
// 变量赋值,并覆盖函数提升
c = 1;
console.log(typeof c); // number
c(2); // 报错 c is not a function

8.3 作用域与作用域链

8.3.1 作用域

  • 理解

    • 就是一块”地盘”,一个代码段所在的区域

    • 它是静态的(相对于上下文对象),在编写代码时就确定了

    • 结论:定义了n个函数,则有n+1个作用域
  • 分类

    • 全局作用域

    • 函数作用域

    • 没有块作用域(ES6有)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      if(true) {
      var c = 1;
      }
      console.log(c); // 1

      // ES6
      if(true) {
      let c = 1;
      }
      console.log(c); // c is not defined
  • 作用

    • 隔离变量,不同作用域下同名变量不会有冲突

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
// 以下共有3个作用域
var a = 10, b = 20;
function fn(x) {
var a = 100, c = 300;
console.log('fn()', a, b, c, x) //100 20 300 10
function bar(x) {
var a = 1000, d = 400;
console.log('bar()', a, b, c, d, x);
}
bar(100); //1000 20 300 400 100
bar(200); //1000 20 300 400 200
}
fn(10);

8.3.2 作用域与执行上下文的区别与联系

  • 区别1:

    • 全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了。而不是在函数调用时。

    • 全局执行上下文环境是在全局作用域确定之后,js代码马上执行之前创建

    • 函数执行上下文是在调用函数时,函数体代码执行之前创建

  • 区别2:

    • 作用域是静态的,只要函数定义好了就一直存在,且不会再变化

    • 执行上下文是动态的,调用函数时创建,函数调用结束时就会自动释放

  • 联系:

    • 执行上下文(对象)是从属于所在的作用域

    • 全局上下文环境==>全局作用域

    • 函数上下文环境==>对应的函数使用域

image-20220222103637692

8.3.3 作用域链

  • 理解

    • 多个上下级关系的作用域形成的链, 它的方向是从下向上的(从内到外)

    • 查找变量时就是沿着作用域链来查找的

  • 查找一个变量的查找规则

    • 在当前作用域下的执行上下文中查找对应的属性,如果有直接返回,否则进入2

    • 在上一级作用域的执行上下文中查找对应的属性,如果有直接返回,否则进入3

    • 再次执行2的相同操作,直到全局作用域,如果还找不到就抛出找不到的异常

image-20220222104217691

8.3.4 测试题

  • 测试题1
1
2
3
4
5
6
7
8
9
var x = 10;
function fn() {
console.log(x);
}
function show(f) {
var x = 20;
f();
}
show(fn); // 10

图示:共3个作用域,注意两个函数作用域并不嵌套

image-20220222104844162

  • 测试题2
1
2
3
4
5
6
7
8
9
10
var fn = function () {
console.log(fn);
}
fn(); // 输出函数本身
var obj = {
fn2: function () {
console.log(fn2);
}
}
obj.fn2(); // 报错 fn2 is not defined

8.4 闭包

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。

8.4.1 闭包概念的引出

需求:循环遍历加监听,点击某个按钮, 提示”点击的是第n个按钮”

错误场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button>测试1</button>
<button>测试2</button>
<button>测试3</button>

<script>
var btns = document.getElementsByTagName("button");
// 遍历加监听
for (var i = 0; i < btns.length; i++) {
var btn = btns[i];
btn.onclick = function() {
alert("第" + (i+1) + "个按钮");
}
}
</script>
</body>
</html>

image-20220222110855384

执行结果始终为4

解决

  • 方法1:保存下标,回调时再使用
1
2
3
4
5
6
7
8
9
10
var btns = document.getElementsByTagName("button");
// 遍历加监听
for (var i = 0; i < btns.length; i++) {
var btn = btns[i];
// 将btn所对应的下标保存在btn上
btn.index = i;
btn.onclick = function() {
alert("第" + (this.index+1) + "个按钮");
}
}
  • 方法2:利用IIFE,闭包
1
2
3
4
5
6
7
8
9
10
var btns = document.getElementsByTagName("button");
// 遍历加监听
for (var i = 0; i < btns.length; i++) {
(function(i) {
var btn = btns[i];
btn.onclick = function() {
alert("第" + (i+1) + "个按钮");
}
})(i)
}

8.4.2 理解闭包

  • 如何产生闭包?
    • 当一个嵌套的内部(子)函数引用了嵌套的外部(父)函数的变量(或函数)时,就产生了闭包
  • 闭包到底是什么?
    • 理解一:闭包是嵌套的内部函数(绝大部分人)
    • 理解二:包含被引用变量(函数)的对象(极少数人)。闭包是一个对象,属性就是被引用的变量
    • 注意:闭包存在于嵌套的内部函数中
  • 产生闭包的条件?
    • 函数嵌套
    • 内部函数引用了外部函数的数据(变量/函数),且外部函数执行
  • 闭包产生的个数?
    • 外部函数调用几次,就产生多少个闭包

示例

通过Chrome调试查看闭包:

1
2
3
4
5
6
7
8
9
10
11
function fn1() {
var a = 2;
var b = 3;
// 函数声明时,就会产生闭包
// 如果是表达式的话,则不行,因为没有函数声明提升
function fn2() {
console.log(a);
}
fn2();
}
fn1();

image-20220222113555758

点击“进入下一个断点”:

image-20220222113637153

8.4.3 常见的闭包

  • 将函数作为另一个函数的返回值
1
2
3
4
5
6
7
8
9
10
11
12
function fn1() {
var a = 2;
function fn2() {
a++;
console.log(a);
}
// 将函数作为另一个函数的返回值
return fn2;
}
var f = fn1(); // f = fn2
f(); // 3
f(); // 4

注意,fn1只被调用了一次var f = fn1();,因此只产生了一个闭包。

调试过程

var a = 2;a++;打上断点调试:

image-20220223111832638

当fn1函数返回时,该闭包对象引用的a已经赋值为2:

image-20220223112018898

第一次调用f时(f就是fn2),闭包仍然为刚才的闭包。a自增后打印输出3:

image-20220223112138950

第二次调用f时,闭包不变:a自增后打印输出为4

image-20220223112241293

  • 将函数作为实参传递给另一个函数调用
1
2
3
4
5
6
function showDelay(msg, time) {
setTimeout(function() {
alert(msg);
}, time);
}
showDelay("Hello World", 1000);

这里的外部函数是showDelay,内部函数是回调函数,且内部函数引用了外部函数变量msg(注意不是time),因此产生了闭包。

8.4.4 闭包的作用

  1. 使用函数内部的变量在函数执行完后,仍然存活在内存中(延长了局部变量的生命周期)
  2. 让函数外部可以操作(读写)到函数内部的数据(变量/函数)

典型问题:

  1. 函数执行完后,函数内部声明的局部变量是否还存在?
    • 一般是不存在,存在于闭包中的变量才可能存在
  2. 在函数外部能直接访问函数内部的局部变量吗?
    • 不能,但我们可以通过闭包让外部操作它

8.4.5 闭包的生命周期

  1. 产生:在嵌套内部函数定义时就产生了(不是在内部函数调用时,而是在外部函数调用时)
  2. 死亡:在嵌套的内部函数成为垃圾对象时
1
2
3
4
5
6
7
8
9
10
11
12
13
function fn1() {
//此时闭包就已经产生了(函数提升,实际上[fn2]提升到了第一行, 内部函数对象已经创建了)
var a = 2;
function fn2() {
a++;
console.log(a);
}
return fn2;
}
var f = fn1(); // f = fn2
f(); // 3
f(); // 4
f = null; // 闭包死亡(包含闭包的函数对象成为垃圾对象)

8.4.6 闭包的应用

闭包的应用 : 定义JS模块

  • 具有特定功能的js文件
  • 将所有的数据和功能都封装在一个函数内部(私有的)
  • 只向外暴露一个包含n个方法的对象或函数
  • 模块的使用者,只需要通过模块暴露的对象调用方法来实现对应的功能

代码示例

  • 新建js模块myModule.jsmyModule2.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function myModule() {
// 私有属性
var msg = "Hello World";
// 操作数据的函数,利用了闭包
function doSomething() {
console.log("doSomething " + msg.toUpperCase());
}

function doOtherthing() {
console.log("doOtherthing " + msg.toLowerCase());
}
// 向外暴露对象,给外部使用的方法
return {
doSomething: doSomething,
doOtherthing: doOtherthing
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(function(window) {
// 私有属性
var msg = "Hello World";
// 操作数据的函数,利用了闭包
function doSomething() {
console.log("doSomething " + msg.toUpperCase());
}

function doOtherthing() {
console.log("doOtherthing " + msg.toLowerCase());
}
// 向外暴露对象,给外部使用的方法
window.myModule2 = {
doSomething: doSomething,
doOtherthing: doOtherthing
}
})(window)
// 注:IIFE的两个window参数可不用写,但写上有利于代码压缩
  • 新建页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<!--引入外部js模块-->
<script type="text/javascript" src="./myModule.js"></script>
<script type="text/javascript" src="./myModule2.js"></script>
</head>
<body>
<script>
var module = myModule();
module.doSomething();
module.doOtherthing();

myModule2.doSomething();
myModule2.doOtherthing();
</script>
</body>
</html>

image-20220223134012352

8.4.7 闭包的缺点及解决

  • 缺点:

    • 函数执行完后,函数内的局部变量没有释放,占用内存时间会变长

    • 容易造成内存泄露

  • 解决:

    • 能不用闭包就不用

    • 及时释放

1
2
3
4
5
6
7
8
9
10
function fn1() {
var arr = new Array[100000];
function fn2() {
console.log(arr.length);
}
return fn2;
}
var f = fn1();
f();
f = null; //让内部函数成为垃圾对象-->回收闭包

8.4.8 内存溢出与内存泄漏

  • 内存溢出

    • 一种程序运行出现的错误

    • 当程序运行需要的内存超过了剩余的内存时,就出抛出内存溢出的错误

  • 内存泄露

    • 占用的内存没有及时释放

    • 内存泄露积累多了就容易导致内存溢出

    • 常见的内存泄露:

      • 意外的全局变量
      • 没有及时清理的计时器或回调函数
      • 闭包
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
28
29
30
31
32
33
// 1. 内存溢出
var obj = {};
for (var i = 0; i < 10000; i++) {
obj[i] = new Array(10000000);
console.log('-----');
}

// 2. 内存泄露
// 意外的全局变量
function fn() {
a = new Array(10000000); //不使用var let const去承接
console.log(a);
}
fn();

// 没有及时清理的计时器或回调函数
var intervalId = setInterval(function () { //启动循环定时器后不清理
console.log('----');
}, 1000)

// clearInterval(intervalId)

// 闭包
function fn1() {
var a = 4;
function fn2() {
console.log(++a);
}
return fn2;
}
var f = fn1();
f();
// f = null

不使用let const var等去声明,实际上是挂载到[window]上的,所以导致内存泄露

8.4.9 测试题

  • 测试题1
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 name = "The Window";
var object = {
name : "My Object",
getNameFunc : function() {
return function() {
return this.name;
};
}
};
console.log(object.getNameFunc()()); // The Window

//代码片段二
var name2 = "The Window";
var object2 = {
name2 : "My Object",
getNameFunc : function() {
var that = this;
return function() {
// 产生闭包 使用了外部函数的that
return that.name2;
};
}
};
console.log(object2.getNameFunc()()); // My Object
  • 测试题2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function fun(n,o) {
console.log(o)
return {
fun:function(m){
return fun(m,n);
}
};
}
var a = fun(0);
a.fun(1);
a.fun(2);
a.fun(3);//undefined,?,?,?
var b = fun(0).fun(1).fun(2).fun(3);//undefined,?,?,?
var c = fun(0).fun(1);
c.fun(2);
c.fun(3);//undefined,?,?,?

9 面向对象高级

9.1 对象创建模式

9.1.1 Object构造函数模式

  • 套路: 先创建空Object对象,再动态添加属性/方法
  • 适用场景: 起始时不确定对象内部数据
  • 问题: 语句太多
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 先创建空Object对象
var p = new Object()
p = {

} //此时内部数据是不确定的

// 再动态添加属性/方法
p.name = 'Tom'
p.age = 12
p.setName = function (name) {
this.name = name
}

//测试
console.log(p.name, p.age)
p.setName('Bob')
console.log(p.name, p.age)

9.1.2 对象字面量模式

  • 套路: 使用{}创建对象, 同时指定属性/方法
  • 适用场景: 起始时对象内部数据是确定的
  • 问题: 如果创建多个对象,有重复代码
1
2
3
4
5
6
7
8
9
10
11
var p = {
name: "Tom",
age: 12,
setName: function(name) {
this.name = name;
}
}

console.log(p.name, p.age);
p.setName("Hongyi");
console.log(p.name);

9.1.3 工厂模式

  • 套路: 通过工厂函数动态创建对象并返回
  • 适用场景: 需要创建多个对象
  • 问题: 对象没有一个具体的类型,都是Object类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 工厂函数,返回obj
function createPerson(name, age) {
var obj = {
name: name,
age: age,
setName: function(name) {
this.name = name;
}
}
return obj;
}

// 创建两个人
var p1 = createPerson("Hongyi", 23);
var p2 = createPerson("Mark", 24);

console.log(p1, p2);

9.1.4 自定义构造函数模式

  • 套路: 自定义构造函数,通过new创建对象
  • 适用场景: 需要创建多个类型确定的对象,与上方工厂模式有所对比
  • 问题: 每个对象都有相同的数据(例如相同的方法),浪费内存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 定义类型(或称构造函数)
function Person(name, age) {
this.name = name;
this.age = age;
this.setName = function(name) {
this.name = name;
}
}

var p1 = new Person("Tom", 12);
p1.setName("Mark");
console.log(p1 instanceof Person); // true

// 不同的类型
function Student (name, price) {
this.name = name
this.price = price
}
var s = new Student('Bob', 13000)
console.log(s instanceof Student)

var p2 = new Person('JACK', 23)
console.log(p1, p2)

9.1.5 构造函数+原型的组合模式

  • 套路: 自定义构造函数,属性在函数中初始化,方法添加到原型上
  • 适用场景: 需要创建多个类型确定的对象,推荐使用
  • 放在原型上可以节省空间(只需要加载一遍方法)
1
2
3
4
5
6
7
8
9
10
11
12
function Person(name, age) {
this.name = name;
this.age = age;
}

Person.prototype.setName = function(name) {
this.name = name;
}

var p1 = new Person("Tom", 12);
p1.setName("Mark");
console.log(p1 instanceof Person); // true

9.2 继承模式

9.2.1 原型链继承

  • 套路

    • 定义父类型构造函数

    • 给父类型的原型添加方法

    • 定义子类型的构造函数

    • 关键:创建父类型的对象赋值给子类型的原型

      1
      Son.prototype = new Father();
    • 关键:将子类型原型的构造属性设置为子类型

      1
      Son.prototype.constructor = Son;
    • 给子类型原型添加方法

    • 创建子类型的对象:可以调用父类型的方法

代码示例

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
// 父类型
function Supper() {
this.supProp = "Supper property";
}

Supper.prototype.showSupperProp = function() {
console.log(this.supProp);
}
// 子类型
function Sub() {
this.subProp = "Sub property";
}

// 关键:子类型的原型为父类型的一个实例对象
Sub.prototype = new Supper();
// 关键:将子类型原型的构造属性设置为子类型
Sub.prototype.constructor = Sub;

Sub.prototype.showSubProp = function() {
console.log(this.subProp);
}

var sub = new Sub();
sub.showSubProp();
sub.showSupperProp();

图示:

image-20220225110647049

注:构造函数的显式原型对象的constructor属性应该回指向构造函数本身。

9.2.2 借用构造函数继承

  • 套路:

    • 定义父类型构造函数

    • 定义子类型构造函数

    • 在子类型构造函数中调用父类型构造

  • 关键:

    • 在子类型构造函数中通用call()调用父类型构造函数
  • 作用:

    • 能借用父类中的构造方法,但是不灵活

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person(name, age) {
this.name = name;
this.age = age;
}
function Student(name, age, price) {
//此处利用call(),将 [Student]的this传递给Person构造函数
Person.call(this, name, age); // 相当于: this.Person(name, age)
/*this.name = name
this.age = age*/
this.price = price;
}

var s = new Student('Tom', 20, 14000);
console.log(s.name, s.age, s.price);

9.2.3 组合继承

  1. 利用原型链实现对父类型对象的方法继承
  2. 利用super()借用父类型构建函数初始化相同属性
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
function Person(name, age) {
this.name = name;
this.age = age;
}

Person.prototype.setName = function(name) {
this.name = name;
}

function Student(name, age, price) {
Person.call(this, name, age); // 为了得到属性
this.price = price;
}

Student.prototype = new Person(); // 为了能看到父类型的方法
Student.prototype.constructor = Student;

Student.prototype.setPrice = function(price) {
this.price = price;
}

var s = new Student('Tom', 24, 15000);
s.setName('Bob');
s.setPrice(16000);
console.log(s.name, s.age, s.price);

10 线程机制和事件机制

10.1 进程和线程

image-20220225125343543

  • JS是单线程还是多线程?

    • js是单线程运行的。但使用H5中的 Web Workers可以多线程运行
  • 浏览器运行是单线程还是多线程?

    • 有的是单进程:firefox、老版本的IE
    • 有的是多进程:chrome、新版本的IE

10.2 浏览器内核

支撑浏览器运行的最核心的程序。

10.2.1 不同浏览器的内核

  • Chrome,Safari : webkit
  • firefox : Gecko
  • IE : Trident
  • 360,搜狗等国内浏览器: Trident + webkit

10.2.2 内核的模块

  • 主线程

    • js引擎模块 : 负责js程序的编译与运行
    • html,css文档解析模块 : 负责页面文本的解析(拆解)
    • dom/css模块 : 负责dom/css在内存中的相关处理
    • 布局和渲染模块 : 负责页面的布局和效果的绘制(内存中的对象)
  • 分线程

    • 定时器模块 : 负责定时器的管理
    • 网络请求模块 : 负责服务器请求(常规/Ajax)
    • 事件响应模块 : 负责事件的管理

10.3 定时器

  • 定时器真是定时执行的吗?
    • 定时器并不能保证真正定时执行
    • 一般会延迟一丁点(可以接受),也有可能延迟很长时间(不能接受)
  • 定时器回调函数是在分线程执行的吗?

    • 在主线程执行的,因为js是单线程的
  • 定时器是如何实现的?

    • 事件循环模型(或称为事件轮询)
    • 事件轮询机制,使得js在单线程执行的前提下,能够实现执行异步任务的功能,或者说,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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button id="btn">启动定时器</button>
<script>
document.getElementById("btn").onclick = function(){
var start = Date.now();
console.log("启动定时器前...");
setTimeout(function (){
console.log("定时器执行了...用时: ", Date.now() - start);
}, 200);
console.log("启动定时器后...");
};

// 模拟做一个长时间的工作
for (var i = 0; i < 10000000000; i++) {

}
</script>
</body>
</html>

10.4 JS是单线程的

10.4.1 如何证明JS是单线程执行

  • setTimeout()的回调函数是在主线程执行的
  • 定时器回调函数只有在运行栈中的代码全部执行完后才有可能执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
setTimeout(function() {
console.log("timeout 2222");
alert("timeout 2222");
}, 200);

setTimeout(function() {
console.log("timeout 1111");
alert("timeout 1111");
}, 100);

function fn() {
console.log("fn...");
}

fn();
console.log("alert之前...");
// 暂停当前主线程的执行,同时暂停计时,点击确定后恢复程序执行和计时
alert("--------");
console.log("alert之后...");

程序运行顺序:

1
2
3
4
5
6
7
8
console.log("fn...");
console.log("alert之前...");
alert("--------");
console.log("alert之后...");
console.log("timeout 1111");
alert("timeout 1111");
console.log("timeout 2222");
alert("timeout 2222");

10.4.2 代码分类和JS引擎执行代码的基本流程

  • 代码分类

    • 初始化代码:是同步执行的
    • 回调代码:例如定时器里的回调函数,是异步执行的
  • JS引擎执行代码的基本流程

    • 先执行初始化代码: 包含一些特别的代码
      • 设置定时器(但不会立即执行里面的回调函数)
      • 绑定事件监听(但只有事件触发的时候才会执行回调函数)
      • 发送ajax请求
    • 后面在某个时刻才会执行回调代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
setTimeout(function() {
console.log("timeout 2222");
alert("timeout 2222");
}, 2000);

setTimeout(function() {
console.log("timeout 1111");
alert("timeout 1111");
}, 1000);

setTimeout(function() {
console.log("timeout 0000");
}, 0);

function fn() {
console.log("fn...");
}

fn();
console.log("alert之前...");
alert("--------");
console.log("alert之后...");

程序运行顺序:

1
2
3
4
5
6
7
8
9
console.log("fn...");
console.log("alert之前...");
alert("--------");
console.log("alert之后...");
console.log("timeout 0000");
console.log("timeout 1111");
alert("timeout 1111");
console.log("timeout 2222");
alert("timeout 2222");

为什么js要用单线程模式,而不用多线程模式?

  1. JavaScript的单线程,与它的用途有关。
  2. 作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。
  3. 这决定了它只能是单线程,否则会带来很复杂的同步问题
    • 举个栗子:如果我们要实现更新页面上一个dom节点然后删除,用单线程是没问题的
    • 但是如果多线程,当我删除线程先删除了dom节点,更新线程要去更新的时候就会出错

10.5 事件循环模型Event Loop

10.5.1 概述

我们都知道,javascript从诞生之日起就是一门单线程的非阻塞的脚本语言。这是由其最初的用途来决定的:与浏览器交互

  • 单线程:单线程意味着,javascript代码在执行的任何时候,都只有一个主线程来处理所有的任务。
    • 实现:JS引擎自身决定
  • 非阻塞:非阻塞则是当代码需要进行一项异步任务(无法立刻返回结果,需要花一定时间才能返回的任务,如I/O事件)的时候,主线程会挂起(pending)这个任务,然后在异步任务返回结果的时候再根据一定规则去执行相应的回调。
    • 实现:event loop(事件循环)

10.5.2 相关概念

事件循环模型原理图

image-20220226165740895

相关概念

  • 执行栈execution stack

    • 栈:当javascript代码执行的时候会将不同的变量存于内存中的不同位置:堆(heap)栈(stack)中来加以区分。其中,堆里存放着一些对象。而栈中则存放着一些基础类型变量以及对象的指针。 但是我们这里说的执行栈和上面这个栈的意义却有些不同
    • 执行栈:当我们调用一个方法的时候,js会生成一个与这个方法对应的执行环境(context),又叫执行上下文。这个执行环境中存在着这个方法的私有作用域、上层作用域的指向、方法的参数,这个作用域中定义的变量以及这个作用域的this对象。 而当一系列方法被依次调用的时候,因为js是单线程的,同一时间只能执行一个方法,于是这些方法被排队在一个单独的地方。这个地方被称为执行栈。如图左上角的stack
  • 回调队列callback queue

    • 广义上称为回调队列,细分为任务队列task queue,消息队列message queue和事件队列event queue
    • 事件队列:JS引擎遇到一个异步事件后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务,当一个异步事件返回结果后,js会将这个事件加入与当前执行栈不同的另一个队列,我们称之为事件队列
      • 被放入事件队列不会立刻执行其回调,而是等待当前执行栈中的所有任务都执行完毕,主线程处于闲置状态时,主线程会去查找事件队列是否有任务。如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码…,如此反复,这样就形成了一个无限的循环。这就是这个过程被称为“事件循环(Event Loop)”的原因。
      • 以上的事件循环过程是一个宏观的表述,实际上因为异步任务之间并不相同,因此他们的执行优先级也有区别。不同的异步任务被分为两类:微任务(micro task)和宏任务(macro task)
  • 宏任务与微任务

    • JS中用来存储待执行回调函数的队列包含2个不同特定的队列
      • 宏队列:用来保存待执行的宏任务(回调),比如:定时器回调/ajax回调/dom事件回调
      • 微队列:用来保存待执行的微任务(回调),比如:Promise的回调/muntation回调
    • JS执行时会区别这2个队列
      • JS执行引擎首先必须执行所有的初始化同步任务代码
      • 每次准备取出第一个宏任务执行前,都要将所有的微任务一个一个取出来执行,即微任务的优先级高于宏任务
  • Web APIs

    • 浏览器内核提供的管理模块,属于浏览器的分线程,不是JS的主线程。换句话说,这里执行的代码已经和JS没有任何的关系了。
    • 例如定时器代码

      1
      2
      3
      4
      setTimeout(function() {
      console.log("timeout");
      }, 2000);
      console.log("Hello World");

      JS将此段代码执行后(setTimeout),会将事件回调函数交给对应模块管理,之后继续向下执行打印代码(Hello World),而浏览器会新开启一个异步的定时器线程用于计时,“放入”Web APIs中。

      计时时间到达之后,定时器线程会将该回调函数放入回调队列中,等待主线程空闲时被调用。即,回调函数还是是被JS主线程执行的。

10.5.3 执行流程

  • 执行初始化代码,将事件回调函数交给对应模块管理
  • 当事件发生时,管理模块会将回调函数及其数据添加到回调列队中
  • 只有当初始化代码执行完后(可能要一定时间),才会遍历读取回调队列中的回调函数执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function fn1() {
console.log("fn1()...");
}
fn1();
document.getElementById("btn").onclick = function() {
console.log("点击了btn")
};
setTimeout(function() {
console.log("定时器执行了");
}, 2000);
function fn2() {
console.log("fn2()...");
}
fn2();