JS基础知识汇总

01 -《发展史》

浏览器发展史

  • 1995 年,网景公司研发的导航者浏览器。

  • 1995 年 8 月,微软推出 IE1.0 版本。

  • 1995 年 11 月,微软推出 IE2.0 版本。

  • 从 IE3 开始,IE 浏览器作为了 windows 内置浏览器。

  • 1998 年,网景公司被“美国在线”收购。

  • 2008 年,谷歌推出了第一款浏览器 Chrome。

JavaScript 发展史

1995 年,网景公司花了 10 天的时间,研发出了 JavaScript 语言。

2004 年,谷歌推出了 AJAX 技术,JS 逐渐开始受到程序员的重视。

2009 年,谷歌研发出了 Nodejs,JS 就可以在服务端进行开发。

JavaScript 版本

1997 年 6 月,JS 的第一版,取名叫做 ECMAScript 1.0, 简称 ES1.0。

2009 年 12 月, ECMAScript 5.0(简称 ES5)正式发布。

2015 年 6 月,ECMAScript 2015(简称 ES6)正式发布。

02 -《准备工作》

JavaScript 是一种轻量级的脚本语言,可以嵌入到 HTML 中,由浏览器来解析执行。

一、运行 JS 代码

每一款浏览器都内置了一个 JS 引擎,用来解析执行 JS 代码。因此,如果要运行 JS 代码,需要先创建一个 HTML 文件。

在 HTML 中运行 JS 代码的方式有两种:内部 JS 和外部 JS。

1、内部 JS

2、外部 JS

在 HTML 文件以外创建一个 .js 的文件,然后在 HTML 文件中引入。

二、格式

1、分号

在 JS 中,每一个代码语句完成后,都应该以英文的 ; 作为结束。

 var a = 1;

2、语句

从语法上来说,一行代码可以写多个语句,例如:

 var a = 1; var a = 1; var a = 1; var a = 1;

但是,实际开发中不建议这样书写代码。通常一个 ; 表示当前语句已经结束了,那么就换行再继续写下一行语句。

 var a = 1; 
 var a = 1; 
 var a = 1; 
 var a = 1;

3、语句块

JS 中可以通过 {} 来设置语句块,{} 中的代码要有一个 Tab 键的缩进。

 {
     var a = 1;
     {
         var a = 1;
     }
 }

三、注释

1、单行注释

2、多行注释

 /*
         范围内的都是注释
         范围内的都是注释
         范围内的都是注释
         范围内的都是注释
         范围内的都是注释
 */

四、命名

在 JS 中,也会遇到需要我们自己用英文取名字的情况。但是,在 JS 中命名时需要注意以下几点:

1、严格区分大小写

同一个字母,大小写分别表示不同的名字:

 username;
 userName;

上述代码是两个不同的名字。

2、不能以数字开头

通常,我们可以以字母、_ 或者 $ 符号作为名字的开头,但是不能以数字作为开头

 _useranme;
 1username; // 错误

3、常用的命名规则

  • 小驼峰命名法:如果名字是由多个单词组成,除了第一个单词外,后面所有的单词首字母都大写。

  • 大驼峰命名法:如果名字是由多个单词组成,所有的单词首字母都大写。

  • 蛇形命名法:如果名字是由多个单词组成,每一个单词之间用 _ 连接。

 userNumber;  // 小驼峰
 UserNumber;   // 大驼峰
 user_number;  //  蛇形命名法

03 -《变量》

JS 中的变量,可以理解为是保存数据的容器。

例如,我有一个密码:123456。

在 JS 中,需要将该密码数据保存下来,方便后续的使用。而保存的方式,就是通过变量。

一、变量的创建

变量的创建,官方说法叫做“变量的声明”。

基础语法:

var 自己取的变量名字 = 想要保存的数据;

案例代码:

var password = 123456;

在一个 JS 文件中,可以通过 var 创建多个变量,但是建议变量名不要重复,一旦重复,后面的变量名会将前面的变量名覆盖。

二、创建时不保存数据

创建时不保存数据,指的就是在创建变量时,不通过 = 给这个变量赋值。

var password;

三、输出变量

JS 中提供了 console.log() 方法来对数据进行输出:

var password = 123456;
console.log(password);

在浏览器的 F12 中的 console 中查看输出结果。

四、变量的赋值

赋值,指的就是给变量中保存数据。赋值可以分为两种情况:

  1. 创建变量的同时就进行赋值;

  2. 变量创建完成后,后续再赋值;

var a;
console.log(a);
a = 100;
console.log(a);

04 -《数据类型》

一、数据类型的分类

在 JS 中,根据数据的种类对数据的类型进行划分为两个大类:

  1. 基础(原始)类型数据

  2. 引用类型数据

其中,基础类型的数据又分为 7 个小类(截至到 ES10):

  1. 字符串 string

  2. 数字 number

  3. 布尔值 boolean

  4. 未定义 undefined

  5. null

  6. 唯一值 symbol(ES6 新增)

  7. 无穷大 bigInt(ES10 新增)

引用类型的数据只有 1 个小类:对象 object

一、字符串 string

字符串,指的是通过双引号或者单引号包裹起来的数据,都属于字符串类型。

var str1 = 'hello';
var str2 = "world";
var str3 = "123";
var str4 = "";
var str5 = "我的名字叫'张三'";

注意:在字符串中,单引号可以包双引号,反过来双引号中也可以包裹单引号,但是不能同类型的引号互相包裹。

字符串中的转义符

如果我们字符串中出现了同类型引号互相包裹的情况,可以使用转义符 \ 对字符串内部的引号进行转义。

var str6 = "我的名字叫\"张三\"";

二、数字 number

数字类型,指的是所有的整数和小数(浮点数)。

var num1 = 100;
var num2 = 0.1;
var num3 = .1;

NaN

NaN 是 JS 中的一种特殊值,表示非数字(Not a Number),但是属于 number 类型。

注意:

  1. NaN 与任何数据都不相等,包括自己;

  2. 任何涉及到 NaN 的算术运算,得到的结果都是 NaN;

三、布尔值 boolean

布尔类型下面,只有两个值,就是 truefalse

var boo1 = true;
var boo2 = false;

四、未定义 undefined

undefined 类型中只有一个值,就是 undefined 本身,表示“未赋值”。

五、空 null

null 类型中只有一个值,就是 null 本身,表示“空”。

null 有一个比较特殊的地方在于,当我们使用 typeof 检测 null 的数据类型时,得到的是 object

六、唯一值 symbol

var sym1 = Symbol();
var sym2 = Symbol();
console.log(sym1, sym2);

05 -《数据类型转换》

数据类型转换,分为两种方式:

  • 强制转换(显式转换)

  • 自动转换(隐式转换)

一、强制转换(显式转换)

强制转换,指的就是我们手动的去调用一些方法,来实现数据类型的转换。JS 中提供了以下几种强制转换的方法:

  1. 转换为数字:Number()parseInt()parseFloat()

  2. 转换为字符串:String()toString()

  3. 转换为布尔值:Boolean()

1、转换为数字

基本用法如下:

var str = '100';
var num1 = Number(str);
var num2 = parseInt(str);
var num3 = parseFloat(str);
console.log(num1, num2, num3);

转换规则:

  1. Number() 是对数据进行整体的转换。如果数据整体是一个合法的数字,就直接转换为对应的数字,如果不是一个合法的数字,直接得到 NaN;

  2. parseInt() 是数据从左往右一个一个进行转换,如果是一个合法的数字,就留下来,继续转换下一个数字,直到遇到第一个不合法的数字时,直接结束转换过程。

  3. parseFloat()数据从左往右一个一个进行转换,如果是一个合法的数字或小数点(只有第一个小数点合法),就留下来,继续转换下一个数字,直到遇到第一个不合法的数字时,直接结束转换过程。

2、转换为字符串

基本语法如下:

var num = 200;
console.log(String(num));
console.log(num.toString());

String() 可以将任意数据类型都转换为字符串,toString() 可以将除了 undefinednull 以外的其他类型数据,都转换为字符串。

3、转换为布尔值

基础语法如下:

var a = 1;
var boo = Boolean(a);
console.log(boo);

转换规则:

除了 0、空字符串、NaN、undefined、null、false 这个六个数据以外,其他的数据转换为布尔值都是 true。

拓展:toLocalString()的妙用

  1. 可以直接得到钱的分割效果

let num = 1000000
num.toLocaleString()  // '1,000,000'

2.上面方法或许你还要自己再拼接 ¥ 来达到 ¥1,000,000.00 的效果 用以下方法可以直接得到 ¥1,000,000.00 的效果

let num = 1000000
num.toLocaleString('zh',{ style: 'currency', currency: 'cny' })  // '¥1,000,000.00'
  1. 如果你想要达到 CNY 1,000,000.00 的效果可以这样

let num = 1000000
num.toLocaleString('zh',{ style: 'currency', currency: 'cny', currencyDisplay: 'code' })  // 'CNY 1,000,000.00'

  1. 也可以达到这种效果 '1,000,000.00人民币'

let num = 1000000
num.toLocaleString('zh',{ style: 'currency', currency: 'cny', currencyDisplay: 'name' })  // '1,000,000.00人民币'
  1. 可以转换日期

new Date().toLocaleString() // '2022/1/7 下午2:00:05'

6.可以转百分比

 num.toLocaleString("zh", { style: "percent" }); // 100000000%

二、自动转换(隐式转换)

1、转换为数字

当数据参与算术运算时,会隐式的转换为数字类型,然后再进行计算。

因此,当我们获取到一个值,不是数字时,我们可以直接 -0 来将其转换为对应的数字。

还可以~~数据

 var b = '2';
 console.log(b - 0);
 console.log(~~b);

2、转换为字符串

当数据进行 + 运算时,一旦参与运算的数据中有一个字符串,那么这些数据都自动拼接成一个整体的字符串,不再做加法运算。

 var a = 1;
 console.log(a + '');

3、转换为布尔值

 var a = 1;
 console.log(!!a);

06 -《运算符》

JS 中提供了一组用来操作数据的运算符。

一、字符串拼接符

+ 加号可以用来实现字符串之间的拼接。

var str1 = 'hello';
var str2 = 'world';
console.log(str1 + " " + str2);

二、算术运算符

算术运算符说明
+求和
-求差
*求积
/求商
%求余
++自增 1
--自减 1

案例代码:

var num1 = 10;
var num2 = 4;
console.log(num1 + num2);
console.log(num1 - num2);
console.log(num1 * num2);
console.log(num1 / num2);
console.log(num1 % num2);

自增(减)运算符

自增(减)运算符分为了前置型和后置型。

var num = 10;
++num;
num++;

如果在一行代码中,只有 ++-- 操作,前置和后置的运行结果没有区别。

如果在一行代码中,有两件事情要做,前置型则先运算再执行其他操作,后置型先执行其他操作再运算。

三、比较运算符

比较运算符说明
>大于
<小于
>=大于等于
<=小于等于
==等于
!=不等于
===(数据值和类型)全等
!==全不等

案例代码:

var num1 = 10;
var num2 = '10';
console.log(num1 > num2);
console.log(num1 < num2);
console.log(num1 >= num2);
console.log(num1 <= num2);
console.log(num1 == num2);
console.log(num1 === num2);
console.log(num1 != num2);
console.log(num1 !== num2);

说明:

  • 好理解的版本:== 只比较值,不比较类型;=== 既要比较值也要比较类型;

  • 真正原理的版本:== 在做比较前,会将类型不一致的数据隐式转换为同一个类型,然后再进行比较。===== 在比较时都要比较值和类型。

四、逻辑运算符

逻辑运算符说明
&&查找类型为 false 的值(如果不是布尔值,会隐式转换),找到后就留下这个值本身,如果找不到,就留下最后一个值;
两根竖线查找类型为 true 的值(如果不是布尔值,会隐式转换),找到后就留下这个值本身,如果找不到,就留下最后一个值;
!取反(如果不是布尔值,会隐式转换后再取反)

案例代码:

var a = 0;
var b = 1;
var c = 2;
console.log(a && b && c);
console.log(a || b || c);
console.log(!a);

五、赋值运算符

赋值运算符说明
=将 = 右边的内容赋值给 = 左边
+=“变量名 = 变量名 + 值” 形式可以简写为:“变量名 += 值”
-=“变量名 = 变量名 - 值” 形式可以简写为:“变量名 -= 值”
*=“变量名 = 变量名 值” 形式可以简写为:“变量名 = 值”
/=“变量名 = 变量名 / 值” 形式可以简写为:“变量名 /= 值”
%=“变量名 = 变量名 % 值” 形式可以简写为:“变量名 %= 值”

案例代码:

var a = 1;
a += 2;
console.log(a);   // 3

六、三目(元)运算符

三目(元)运算符的基础语法如下:

判断条件 ? 条件成立时执行 : 条件不成立时执行

七、特殊运算符 typeof

typeof 可以用来检测数据的类型。(typeof 一个数组和null得到object)

var a = '123';
console.log(typeof a);

07 -《 输入输出》

一、输入

JS 中提供了三个弹窗:

  • alert():弹出一个提示窗口;

  • prompt():弹出一个输入框;

  • comfirm():弹出一个确认框,让用户选择确认或取消;

08 -《流程控制语句》

JS 中代码的执行顺序可以分为三种:

  1. 顺序执行:代码从上往下依次执行;

  2. 选择执行:通过一些判断条件来控制某一段代码是否执行;

  3. 循环执行:控制某一段代码反复执行;

一、条件语句

我们可以通过条件语句来实现代码的选择执行。

JS 中提供了以下几种条件语句:

  • if

  • else

  • else if

  • switch case

1、if

var a = 0;
// 如果 a > 0 时,就执行以下输出语句
if (a > 0) {
    // 当 if 小括号中的结果为 true 时,执行 if 大括号中代码
    console.log('hello');
}
console.log('world');

2、else

else 必须搭配 if 一起使用。基础语法:

if(判断条件) {
    判断条件为 true 时执行
} else {
    判断条件为 false 时执行
}

案例代码:

var a = 0;
// 如果 a > 0 时,就执行以下输出语句
if (a >= 0) {
    // 当 if 小括号中的结果为 true 时,执行 if 大括号中代码
    console.log('hello');
} else {
    console.log('world');
}

3、else if

else if 必须搭配 if 一起使用,基础语法如下:

if (判断条件一) {
    判断条件一为 true 时执行
} else if (判断条件二) {
    判断条件一为 false 且判断条件二为 true 时执行
} else if (判断条件三) {
    判断条件一二为 false 且判断条件三为 true 时执行
}

案例代码:

var a = 0;
if (a > 0) {
    console.log('进入 if');
} else if (a == 0) {
    console.log('进入 else if');
}

4、三者搭配使用

var a = 0;
if (a > 0) {
    console.log('进入 if');
} else if (a < 0) {
    console.log('进入 else if');
} else {
    console.log('进入 else');
}

练习:

  1. 版本一:提示用户“输入 1 - 7 之间的任意数字”,如果输入的是 1-5,就提示工作日,否则就提示休息日;

  2. 版本二:提示用户“输入 1 - 7 之间的任意数字”,如果不在 1-7 的范围内,提示用户“输入有误”。如果输入的是 1-5,就提示工作日,否则就提示休息日;

5、switch case

基础语法:

switch (变量名) {
    case 第一个值:
        当变量名和第一个值全等时执行的代码;
        break;
    case 第二个值:
        当变量名和第二个值全等时执行的代码
    default:
        当变量名和所有 case 的值都不匹配时执行的代码
}

案例代码:

var a = 3;
switch (a) {
    case 1:
        console.log('进入到第一个 case');
    case 2:
        console.log('进入到第二个 case');
        break;
    case 3:
        console.log('进入到第三个 case');
    case 4:
        console.log('进入到第四个 case');
    default:
        console.log('进入到 default');
}

二、循环语句

循环语句,就是可以让某一段代码反复执行多次。

JS 中提供了以下三种循环语句:

  1. while

  2. do...while

  3. for

1、while

基础语法:

while (循环判断条件) {
    循环判断条件为 true 时执行的代码
}

2、do while

基础语法:

do {
    第一次执行不需要经过条件判断
    从第二次开始,每一次的循环都需要判断条件为 true 时执行
} while (循环判断条件)

3、for

基础语法:

for (创建变量并赋值; 条件判断; 修改变量的值) {
    当条件判断为 true 时执行的代码
}

执行顺序:

for (1; 2; 3) {
    4
}

执行顺序:1 - 2(true) - 4 - 3 - 2(true)- 4 - 3 - 243 … - 2(false)

案例代码:

for (var i = 0; i < 5; i++) {
    console.log(i);
}

练习一:

  1. 提示用户输入两个数字

  2. 输出用户输入的两个数字之间所有能够被 7 整除的数字,并且统计总共的数量,一个都没有的话也提示用户没有能整除的数。

  3. 扩展(考虑用户输入的数字大小先后)

三、终止循环

JS 中提供了两种终止循环的方式:

  • break:立即结束离它最近的整个循环

  • continue:立即结束它最近的本次循环,立即进入下一次循环

09 -《数组基础 》

数组 Array,可以用来将多条数据通过 [ ] 包裹起来组成一个整体。

例如:

var arr = [100, 'hello', true, 200];

说明:

  • 元素(数组项):数组中的每一条数据,都可以叫做数组的“元素”,也可以叫做“数组项”;

  • 下标(索引):数组中的每一条数据在数组中都有一个对应的位置,我们通过数字来表示每一条数据位置。但是要注意,数字从 0 开始取值。这个用来表示位置的数字,我们可以叫做数组的“下标”或者数组的“索引”。

一、数组的特点

  • 无类型限制:数组中可以保存任意类型的数据;

  • 无数量限制:数组中可以保存任意数量的数据;

  • 动态数组:数组创建完成后,后续还可以继续对数组中的元素进行增加、修改、删除等操作;

二、数组的创建

数组的创建分为两种形式:字面量方式和构造函数方式。

1、字面量方式

var 变量名 = [];

2、构造函数方式

var 变量名 = new Array();

三、数组创建并保存数据

在创建数组时,就保存数据到数组中:

var 变量名 = [数据一, 数据二...];
var 变量名 = new Array(数据一, 数据二...);

四、数组的下标(索引)

我们可以通过数组的下标,找到数组中对应位置的数据,然后对数据进行增加、删除、修改、查看等操作。

1、查看

var arr = ['a', 'b', 'c'];
console.log(arr[1]);
console.log(arr[2]);
console.log(arr[5]);

2、增加

var arr = ['a', 'b', 'c'];
arr[3] = 'd';
arr[5] = 'e';
console.log(arr);

3、修改

var arr = ['a', 'b', 'c'];
arr[0] = 'd';
console.log(arr);

4、删除(了解)

通过 delete 删除的元素,会保留原位置:

var arr = ['a', 'b', 'c'];
delete arr[0];
console.log(arr);

五、数组的长度

每一个数组身上,都有一个 length 的属性,用来表示数组的长度(数组中元素的个数)。

// 获取数组的长度
var len = arr.length;
console.log(len);
// 修改数组的长度
arr.length = 0;

六、数组的遍历

遍历,指的是依次访问数组中的每一个元素。

for(var i = 0; i < 数组名.length; i++) {
    console.log(数组名[i]);
}

七、多维(二维)数组(了解)

数组中的元素也可以是数组,这样形式的数组,我们叫做多维数组。

var arr = [
    ['a', 'b'],
    ['c', 'd', 'e']
];

多维数组的遍历,根据数组嵌套的层数,来嵌套 for 循环实现每一个元素的遍历:

for(var i = 0; i < 数组名.length; i++) {
    for(var j = 0; j < 数组名[i].length; j++) {
        console.log(数组名[i][j]);
    }
}

练习:

  1. 求出给定数组中所有数字的和、以及所有奇数和、偶数和。(简单)

  2. 在不排序的前提下,找出数组中的最大值和最小值。(中等)

  3. 找出某一个值在数组中第一次出现的下标。(简单)

  4. 找出数组中重复的值(假设只有一组重复的值)。(偏难)

  5. 将数组中的元素按照从小到大的顺序进行排序。(面试题 - 难)

  6. 实现一个多用户版的3次循环登录。例如用户数据如下:

    var users = [
        ['zhangsan', '123'],
        ['lisi', '000']
    ]
    

    要求:两个账号任意一组都能登录成功。

10 -《数组的常见方法(API)》

一、操作方法

方法参数(小括号中的值)说明是否改变原数组返回值
push()要添加的数据,可以有多个,每个数据之间用 , 隔开向数组末尾添加元素数组长度
pop()删除数组中最后一个元素删除的元素
unshift()要添加的数据,可以有多个,每个数据之间用 , 隔开向数组开头添加元素数组长度
shift()删除数组中第一个元素删除的元素
splice()1. 只有一个参数:修改后的长度 2. 两个参数:下标和删除的元素个数 3. 三个及以上:下标、删除元素的个数、新增的元素1. 修改数组长度 2. 删除任意位置元素 3. 在任意位置新增元素 4. 修改任意位置元素删除的元素

代码案例:

var arr = ['a', 'b', 'c', 'd', 'e'];
console.log('原数组:', arr);
var result = arr.push('a', 'b');
var result = arr.pop();
var result = arr.unshift('hello', 'world', 'e');
var result = arr.shift();
var result = arr.splice(0, 1, 'hello');
console.log('操作后的数组:', arr);
console.log('返回值:', result);

练习:去掉数组中重复的多余元素。

二、查询方法

数组的查询方法,可以用来查询指定元素在数组中是否存在,以及元素的位置。

方法参数(小括号中的值)说明是否改变原数组返回值
indexOf()需要查询的元素查询元素在数中第一次出现的下标下标 没找到则返回 -1
lastIndexOf()需要查询的元素查询元素在数组中最后一次出现的下标下标 没找到则返回 -1
includes()需要查询的元素查询元素在数组中是否存在布尔值
var arr = ['a', 'b', 'c', 'd', 'e', 'b'];
console.log('原数组:', arr);
var result = arr.indexOf('f');
var result = arr.lastIndexOf('b');
var result = arr.includes('b');
console.log('操作后的数组:', arr);
console.log('结果:', result);

三、排序方法

方法参数(小括号中的值)说明是否改变原数组返回值
reverse()反转数组元素的顺序倒序后的新数组
sort()function(a, b) { return a - b } function(a, b) { return b - a }1. a - b 从小到大排序 2. b - a 从大到小排序排序后的新数组

代码案例:

var arr = [10, 12, 5, 8, 15, 4, 9, 0];
console.log('原数组:', arr);
var result = arr.reverse();
var result = arr.sort(function(a, b) {
    return b - a;
})
console.log('操作后的数组:', arr);
console.log('结果:', result);

数组包对象的排序的方法

升序

let data = [
                {value: 335, name: '家电'},
                {value: 310, name: '服装'},
                {value: 274, name: '食品'},
                {value: 235, name: '数码'},
                {value: 400, name: '家纺'}
            ]
data.sort(function (a, b) { return a.value - b.value; })      

降序

let data = [
                {value: 335, name: '家电'},
                {value: 310, name: '服装'},
                {value: 274, name: '食品'},
                {value: 235, name: '数码'},
                {value: 400, name: '家纺'}
            ]
data.sort(function (a, b) { return b.value - a.value; })      

结论 可用于数组排查,数组对象排序,echarts柱状图排序等等

四、数组和字符串的转换方法

方法参数(小括号中的值)说明是否改变原本的值返回值
join()任意字符串将数组元素按照指定字符拼接成一个大字符串(数组转字符串)拼接后的字符串
split()任意字符串将字符串按照指定的字符拆分成一个数组拆分后的新数组

说明:split() 不是数组的方法,而是字符串的方法,split()可以接正则。比如:"xiaowang0ok1235" .split(/\d+/)

// 数字转字符串
var arr = [1, 2, 3, 4];
console.log('原数组:', arr);
var result = arr.join('');
console.log('操作后的数组:', arr);
console.log('结果:', result);
// 字符串转数组
var str = 'hello';
console.log('原字符串:', str);
var result = str.split('e');
console.log('操作后的字符串:', str);
console.log('结果:', result);

五、数组的合并和截取

方法参数(小括号中的值)说明是否改变原本的数组返回值
concat()需要合并的数组或值,多个数据之间用 , 隔开可以将多个数组或值合并成一个新的数组合并后的新数组
slice()1. 只有一个参数:开始截取的下标(包含) 2. 两个参数:开始截取的下标(包含)、结束截取的下标(不包含) 说明:可以为负数截取数组中的某一个部分截取后的新数组

案例代码:

var arr1 = ['a', 'b', 'c'];
var arr2 = [4, 5, 6];
var arr3 = [7, 8, 9];
var result = arr1.concat(arr2, arr3, 10);
var sliceResult = result.slice(2, -1);
console.log('原数组:', result);
console.log('结果:', sliceResult);

11 -《数组的遍历方法》

一、for 循环

var arr = ['a', 'b', 'c', 'd'];
for(var i = 0; i < arr.length; i++) {
    console.log(arr[i]);
}

优化版的 for 循环

将数组的长度先计算出来,避免每一次循环的时候重复计算数组长度:

var arr = ['a', 'b', 'c', 'd'];
for (var i = 0, len = arr.length; i < len; i++) {
    console.log(arr[i]);
}

二、for of

ES6 中新增了 for of 用来遍历数组。

其中 item 是任意变量名,arr 是要遍历的数组名。

var arr = ['a', 'b', 'c', 'd'];
for (var item of arr) {
    console.log(item);
}

item 可以接收到数组中的每一条数据。

三、数组的循环方法

1、forEach()

var arr = ['a', 'b', 'c', 'd'];
arr.forEach(function (item, index) {
    console.log(`当前元素${item},对应的下标${index}`);
})

注意:forEach() 循环中不能使用 breakcontinue

2、map()

var arr = [10, 20, 30, 40];
var result = arr.map(function (item, index) {
    console.log(`当前元素${item},对应的下标${index}`);
    return item * 2;
});
var result = arr.map(function (item, index) {
    console.log(`当前元素${item},对应的下标${index}`);
    if (item > 20) {
        return item * 2;
    } else {
        return item;
    }
});
console.log(result);

说明:

  1. map() 可以正常的遍历数组,获取到数组的元素和下标;

  2. 在 map 的方法中设置 return 新数据,最终这些“新数据”会组成一个新数组。

注意:

  1. 新数组和原数组的长度一定是一致的;

  2. 新数组中的数据和原数组中的数据可以不一致;

3、filter()

var arr = [10, 20, 30, 40];
var result = arr.filter(function (item, index) {
    console.log(`当前元素${item},对应的下标${index}`);
    return item > 20;
})
console.log(result);

说明:

  1. filter() 可以正常的遍历数组,获取到数组的元素和下标;

  2. 在 filter 方法中设置 return 筛选条件,最终满足“筛选条件”的 item 会组成一个新数组。

注意:

  1. 新数组和原数组的长度可以不一致;

  2. 新数组中的数据,一定是来源于原数组且不会发生改变;

练习:

1、用 filter 方法实现数组的去重;

2、给出以下数组(数组中的第一个元素是班级名称,第二个元素是班级中的学生姓名):

var classesData = [
    ['Web01', ['张三', '李四']],
    ['Web02', ['张三']],
    ['Java01', ['张三', '李四', '王五']],
    ['Java02']
]

要求:

  • 找到所有的班级名称,组成一个新数组 ['Web01', 'Web02', 'Java01', 'Java02']

  • 找到所有班级名称以及班级学生人数,组成新数组 [['Web01', 2], ['Web02', 1], ['Java01', 3], ['Java02', 0]]

3、数组扁平化。将以下二维数组变成一维数组:

var arr = ['a', 'b', ['c', 'd'], 'e', ['f']];
// 变成 ['a', 'b', 'c', 'd', 'e', 'f']

4、every() 和 some()

every() 和 some() 都可以遍历数组,同时,这两个方法执行完成后,会得到一个布尔值。

var arr = [10, 20, 30, 40];
var everyResult = arr.every(function (item, index) {
    return item >= 10;
});
var someResult = arr.some(function (item, index) {
    return item > 40;
});

结论:

  1. every():一假得假;

  2. some():一真得真;

5、reduce()

var arr = [[10, 20], 30, 40];
var result = arr.reduce(function (item, next) {
    console.log(item, next);
    return item + next;
}, 0)
var result = arr.reduce(function (item, next) {
    // []  10 ---- [10]
    // [10] 20 ---- [10, 20]
    // [10, 20] [30, 40] ---- [10, 20, 30, 40]
    return item.concat(next)
}, [])
console.log(result);

四、总结

  1. forEach() 可以遍历得到元素和下标;不能使用 breakcontinue

  2. map() 可以遍历得到元素和下标;可以得到新数组,且与原数组长度一致,内容可以不一致;

  3. filter() 可以遍历得到元素和下标;可以得到新数组,且与原数组内容一致,长度可以不一致;

  4. some() 可以遍历得到元素和下标;可以得到布尔值,一真得真;

  5. every() 可以遍历得到元素和下标;可以得到布尔值,一假得假;

  6. reduce() 可以遍历得到元素;

  7. find():自学作业!自己去学习这个方法的使用!

12 -《函数》

函数的作用,可以将一段代码包裹成一个整体。而我们选择将一段代码包裹成一个整体的目的有两个:

  1. 当代码量庞大且复杂时,我们可以通过函数来将代码拆分成独立的小功能;

  2. 当一段代码需要在不同时间反复使用时,我们通过函数将代码包裹起来,需要使用时直接使用函数即可。

一、创建函数

JS 中创建函数的方式有两种:声明式函数、函数表达式。

1、声明式函数

基础语法:

function 函数名 () {
    // 函数内的代码
}

2、函数表达式

基础语法:

var 变量(函数)名 = function () {
    // 函数内的代码
}

二、调用函数

函数创建好后默认不会执行函数内部的代码。需要我们调用函数,才能执行函数内部的代码。

基础语法:

函数名();

同一个函数,可以反复调用多次,每次调用,函数内的代码都会重新全部执行一次。

案例代码:

function foo() {
    console.log('hello');
}
foo();
foo();
foo();

三、函数的参数

当我们创建的一个函数中,在使用时我们希望函数内部的代码有一部分内容是可以发生变化的。那么,这一部分变化的内容,就可以作为函数的参数来进行处理。

基础语法:

function 函数名 (形式参数名1, 形式参数名2) {
    // 函数内容
    // 使用形式参数
}
函数名(实际参数1, 实际参数2);

说明:

  1. 在创建函数时,定义的参数,叫做形式参数,简称“形参”。形参可以看作就是函数内部的变量名;

  2. 在调用函数时,传递的参数,叫做实际参数,简称“实参”。实参可以看作传递给函数内部的数据;

  3. 从语法上来说,形参和实参的个数可以不一致;

案例代码:

function foo(username) {
    console.log('hello ' + username);
}
foo('张三');
foo('李四');

练习:

  1. 封装一个求和的函数。用户任意传入两个数字,求出这两个数之间所有数之和,如果用户只传入一个数,就求出 0 到这个数之间所有整数的和;

  2. 封装一个日期格式化的函数。

    • 用户传入一个日期(例如:20220325),我们在函数中将日期处理成 2022年03月25日 的格式。

    • 用户传入一个日期和一个符号(例如:-),我们在函数中将日期处理成用指定符号拼接的格式,例如 2022-03-25

四、作用域

作用域,指的就是作用范围。

当一个函数创建成功时,该函数就自动将函数内部和函数外部划分成了两个区域。函数外部的区域我们称之为“全局作用域”,函数内部的区域我们称之为“局部(函数)作用域”。

1、全局作用域

全局作用域,我们可以看作是一个公共区域。意思就是说我们设置在全局作用域中的数据,任何地方都可以使用。

var a = 1;
function foo() {
    console.log('函数内部', a);
}
foo();
console.log('函数外部', a);

2、局部作用域

局部作用域,我们可以看作是一个私有区域。意思就是说我们设置在局部作用域中的数据,只能在当前函数中使用。

function foo() {
    var a = 1;
    console.log('函数内部', a);
}
foo();
console.log('函数外部', a);  // 报错:a is not defined

说明:

  1. 全局作用域和局部作用域中的变量名可以重名;

  2. 使用重名的变量时,优先使用当前作用域中的变量;

五、函数的返回值

函数中,除了可以自己去定义一些函数需要执行的业务代码外,我们还可以通过 return 来将函数内部的数据返回到函数外部。

函数内部设置返回值的基础语法:

function 函数名() {
    // 内部代码
    return 数据值;
}

函数外部接收返回值得基础语法:

var 变量名 = 函数名();  // 变量名用来接收返回值

案例代码:

function foo() {
    var a = 1;
    return a;
}
var result = foo();

注意事项:

  1. return 执行完成后,会立即结束当前函数;

  2. return 最多只能返回一个值,也可以不返回值;

  3. 如果没有 return 语句或者 return 后面没有值,外部接收到的都是 undefined

13 -《ES6 中的函数》

一、形参的默认值

形参的默认值,是当没有传递对应的实参时,才会使用默认值。如果传递了实参,默认值就不会生效。

基础语法:

function 函数名(形参名 = 默认值) {
}
函数名()

案例代码:

function foo(x = 0, y = 0) {
    console.log(x + y);
}
foo(1);

二、rest 参数(不定参数)

基础语法:

function 函数名(...不定参数名) {
}
函数名()

说明:不定参数可以以数组的形式,接收到所有没有对应形参的实参。

案例代码:

function foo(a, ...b) {
    console.log(b);  // [ 2, 3, 4]
}
foo(1, 2, 3, 4)

注意:

  1. 在使用时,普通形参和不定参数可以同时存在;

  2. 不定参数只能是最后一个参数;

三、箭头函数

箭头函数,就是用 => 取代了 function 关键字。

普通函数的基础语法:

var 变量(函数)名 = function () {
    // 函数内的代码
}

更换为箭头函数,去掉 function 关键字,在 (){} 中间加一个 =>

var 变量(函数)名 = () => {
    // 函数内的代码
}

箭头函数的简写

箭头函数的简写,主要分为两种情况:

1)只有一个形参,可以去掉小括号 ()

var 函数名 = (形参名) => {
}
// 简写
var 函数名 = 形参名 => {
}

2)函数内只有一个 return 语句,可以去掉 return 和函数的大括号 {}

var 函数名 = () => {
    return 值;
}
// 简写
var 函数名 = () => 值;

案例代码:

// 普通函数
function foo(x, y) {
    return x + y
}
// 箭头函数
var foo = (x, y) => x + y;
foo(1, 2);
var bar = x => x * x;     
console.log(bar(3));

14 -《对象 object》

对象,和数组一样,也可以用来将一组数据包裹成一个整体。但是对象的符号是 {}

对象和数组的区别在于,数组中每一条数据都有固定的位置,我们需要记住每一条数据所在的位置。 在对象中,数据是无序的,但是对象中每一条数据都是有一个自己的“名字”,称为“键”,而数据本身,称为“值”。对象中,保存的数据,都是以“键值对”的形式进行保存。

一、创建对象

对象的创建分为两种形式:字面量方式和构造函数方式。

1、字面量方式

var obj = {};

2、构造函数方式

var obj = new Object();

二、对象保存数据

基础语法:

var obj = {
    键一: 值一,
    键二: 值二,
    // ....
}

注意:

  1. 对象的值可以是任意类型的数据;

  2. 对象的键是字符串类型(引号可以省略不写)

案例代码:

var obj = {
    account: '123',
    password: '456',
    money: 10000
}
console.log(obj);
var users = [
    { account: '1', password: '1', money: 1000 },
    { account: '2', password: '2', money: 2000 },
    { account: '3', password: '3', money: 3000 },
];

三、对象的属性和方法

因为对象中可以保存任意类型的数据,因此,我们根据数据值的类型是不是函数,来将对象中的数据又划分成了两类:

  1. 如果数据值是函数,那么这条数据也可以称为“对象的方法”;

  2. 如果数据值不是函数,那么这条数据也可以称为“对象的属性”;

例如,我们想要创建一个学生对象,用来保存学生的相关信息。包括:学生的姓名、年龄、性别以及学生的能力学习、吃饭、睡觉等。

var student = {
    // 对象的属性
    name: '张三',
    age: 20,
    gender: '男',
    // 对象的方法
    study: function() {
        // ...
    },
    eat: function() {
    },
    sleep: function() {
    }
}

四、操作对象属性

在 JS 中,不管我们要对对象的属性做增删查改中的任意一个操作,都是通过 . 点符号来实现的。

1、新增对象属性

基础语法:

对象名.属性名(键)= 属性值(值);

案例代码:

var student = {};
student.name = "张三";
student.age = 20;

2、修改对象属性

基础语法:

对象名.属性名(键)= 属性值(值);

案例代码:

var student = {
    gender: '男'
};
student.gender = "女";

说明:一个对象中,不可能出现重复的“键”,如果出现重名的情况,后面的值会覆盖掉前面的值。

3、查看对象属性

var student = {
    gender: '男'
};
student.name = "张三";
student.age = 20;
student.gender = "女";
console.log(student.name);

4、删除对象属性

基础语法:

delete 对象名.属性名;

案例代码:

var student = {};
student.name = "张三";
student.age = 20;
student.gender = "女";
delete student.age;
console.log(student);

5、中括号访问对象的属性

除了用 . 去访问对象的属性外,也可以使用 [] 去访问。

基础语法:

对象名["属性名(键)"]

通常,只有当属性名(键)是一个变量时,我们才使用 []

基础语法:

对象名[变量名]

案例代码:

var key = "age";
var student = {
    name: '张三',
    [key]: 20
}
student[key] = 30;
console.log(student);

练习:

1)以下列用户数据实现一个登录。

var users = [
    { username: '123', password: '123' },
    { username: '000', password: '000' }
]

2)计算出数组中每一个元素在数组中出现的次数:

var arr = ["a", "a", "b", "c", "a", "b"];

五、操作对象的方法

var student = {
    sayHello: function () {
        console.log('hello');
    }
}
student.sayHello();
student.sayHi = function () {
    console.log('hi');
}
student.sayHi();

六、对象的简写(ES6)

ES6 中针对对象,提供了两种简写形式:

1)对象的值是一个变量,且变量的单词和对象的键是同一个单词。那么就可以将 : 值 省略。

var name = '张三';
var age = 20;
var student = {
    // name: name,
    // age: age
    name,
    age
}
console.log(student);

2)对象的值是一个函数,可以将 : function 省略。

var student = {
    // sayHello: function () {
    //     console.log('hello');
    // },
    sayHello() {
        console.log('hello');
    }
}

七、对象的遍历

1、for in

var student = {
    name: '张三',
    age: 20
}
for (var key in student) {
    console.log(student[key]);
}

2、Object.keys()

var student = {
    name: '张三',
    age: 20
}
Object.keys(student).forEach(key => {
    console.log(student[key]);
})

01 -《BOM 和 DOM》

JavaScript 的核心是分为三个部分:

  1. ECMAScript:JavaScript 的语法规范

  2. BOM

  3. DOM

BOM 和 DOM

BOM,是 Browser Object Model 的缩写,翻译成“浏览器对象模型”,BOM 中提供了很多操作浏览器的方法。

DOM,是 Document Object Model 的缩写,翻译成“文档对象模型”,DOM 中提供了很多操作 HTML 文档的方法。

BOM 和 DOM 的关系

在 BOM 中,包含了很多的对象:

  1. window 对象:浏览器窗口对象(用window .open = "../影院1.html"可实现点击事件页面跳转)。

  2. location 对象:浏览器 URL 地址对象(用location.href = "../影院1.html"可实现点击事件页面跳转);reload()刷新当前页面。

  3. screen 对象:屏幕对象

  4. history 对象:浏览记录对象back();返回上一个页面;forward()前进下一个页面;go(数字)跳转到指定的页面。

  5. navigator 对象:浏览器对象

  6. document 对象:文档对象

由于 document 对象中包含的内容太多,因此,W3C 组织就将 document 对象单独提取出来,做成了一套规范,取名 DOM。

02 -《BOM》

window 对象

1、常用属性

属性名说明
innerHeight/innerWidth视口(文档显示区)宽高
outerHeight/outerWidth浏览器窗口宽高
screenLeft/screenTop浏览器左上角到屏幕左上角的距离
console.log(window);
console.log('innerHeight', window.innerHeight);
console.log('innerWidth', window.innerWidth);
console.log('outerHeight', window.outerHeight);
console.log('outerWidth', window.outerWidth);
console.log('screenLeft', window.screenLeft);
console.log('screenTop', window.screenTop);

2、常用方法

方法名参数说明
open()页面路径打开一个新窗口
setInterval()1. 函数 2. 时间(单位:毫秒)间隔指定时间循环执行函数(最后执行)
setTimeout()1. 函数 2. 时间(单位:毫秒)延迟指定时间执行函数一次
clearInterval()需要暂停的 setInterval() 名字暂停 setInterval()
clearTimeout()需要暂停的 setTimeout() 名字暂停 setTimeout()
var timer = setTimeout(() => {
    console.log(1);
}, 1000);
clearTimeout(timer);
var i = 0;
var timer = setInterval(() => {
    i++;
    if (i === 5) {
        clearInterval(timer);
    }
    console.log(i);
}, 1000);

说明:

setIntervalsetTimeout 都不会阻塞后面代码的执行。原因在于,这两个时间函数不管写在代码中的任何位置,它们都是最后执行。

03 -《元素节点操作》

一、获取元素节点

获取元素节点,指的就是通过 JS 获取到页面中的 HTML 标签。

方法名参数(字符串)说明返回值
getElementById()id 属性值通过 id 获取元素元素节点
getElementsByClassName()class 属性值通过 class 获取元素数组(包含匹配的元素节点)
getElementsByTagName()标签名通过标签名获取元素数组(包含匹配的元素节点)
querySelector()CSS 选择器通过 CSS 选择器获取到第一个匹配的元素元素节点
querySelectorAll()CSS 选择器通过 CSS 选择器获取到所有匹配的元素数组(包含匹配的元素节点)
<h1 class="hi" id="hello">你好</h1>
<h1 class="hi">你好</h1>
<h1 class="hi">你好</h1>
<script>
    // 1. 通过 id 获取元素
    var node = document.getElementById("hello");
    // 2. 通过 class 获取元素
    var nodes = document.getElementsByClassName("hi");
    // 3. 通过标签名获取元素
    var nodes = document.getElementsByTagName("h1");
    // console.log(nodes[1]);
    // 4. 通过 CSS 选择器获取第一个匹配的元素
    var node = document.querySelector('.hi');
    // console.log(node);
    // 5. 通过 CSS 选择器获取匹配的所有元素
    var nodes = document.querySelectorAll('.hi');
</script>

二、创建元素节点

方法名参数(字符串)说明返回值
createElement()标签名创建元素节点元素节点
var node = document.createElement("h2");
console.log(node);

注意:创建好的节点是保存在内存中的,默认情况下不会在页面中渲染出来。

三、添加元素节点

方法名参数(元素节点)说明
父节点.appendChild()要添加的子元素节点往指定父节点的末尾添加一个子节点
父节点.insertBefore()1. 新的子节点 2. 旧的子节点在指定的旧子节点之前添加一个新的子节点

四、删除元素节点

方法名参数(元素节点)说明
父节点.removeChild()要删除的子元素节点删除指定的子节点

五、元素节点操作的新方法

方法名参数说明
父节点.append()要添加的子节点往指定父节点的末尾添加一个子节点
父节点.prepend()要添加的子节点往指定父节点的开头添加一个子节点
节点.before()要添加的兄弟节点往指定节点的前面添加一个兄弟节点
节点.after()要添加的兄弟节点往指定节点的后面添加一个兄弟节点
节点.remove()删除指定节点
旧节点.replaceWith()要替换的新节点用新节点替换指定的旧节点
节点.cloneNode()true/false克隆节点【var newBox = box.cloneNode(true)】

六、获取关系节点

属性名说明
子节点.parentElement获取父节点
父节点.firstElementChild获取第一个子节点
父节点.lastElementChild获取最后一个子节点
父节点.children获取所有的子节点
兄弟节点.previousElementSibling获取前一个兄弟节点
兄弟节点.nextElementSibling获取后一个兄弟节点

04 -《节点的其他操作》

一、操作元素内容

属性名说明
元素节点.innerText获取、新增、修改(删除)指定元素的文本内容
元素节点.innerHTML获取、新增、修改(删除)指定元素的文本内容

说明:innerTextinnerHTML 区别在于,innerHTML 可以解析字符串中的 HTML 标签。

二、操作元素(标签)属性

元素的属性,指的就是标签身上,除了标签名以外的其他内容,都可以看作是标签的属性。

通常,标签上的属性以 属性名="属性值" 格式设置的。

方法名参数(字符串)说明返回值
元素节点.getAttribute()属性名获取指定属性名的值属性值(字符串)
元素节点.setAttribute()1. 属性名 2. 属性值新增/修改指定的属性
元素节点.removeAttribute()属性名删除指定属性
元素节点.hasAttribute()属性名判断是否存在指定属性布尔值

点运算符操作属性

注意:在使用 . 去操作元素的 class 时,需要使用 className 属性。

<a href="#" id="link">连接</a>
<img src="./logo.png" id="image" alt="">
<input type="text" id="input">
<script>
    var link = document.getElementById('link');
    var image = document.getElementById('image');
    var input = document.getElementById('input');
    // console.log(image.getAttribute("src"));
    link.href = "http://www.baidu.com"
    input.type = "button"
    link.className = "active";
</script>

三、操作元素样式

DOM 中,如果使用 JS 操作样式,一定是在操作内联样式。基础语法如下:

元素节点.style.CSS样式名 = "样式值";

案例代码:

box.style.width = "100px";
box.style.height = "100px";
box.style.backgroundColor = "pink";

四、获取元素实际尺寸

DOM 中给元素身上提供了两组属性,用来获取元素的实际尺寸:

1、获取宽(高)度 + 内边距

console.log(元素节点.clientWidth, 元素节点.clientHeight);

2、获取宽(高)度 + 内边距 + 边框宽度

console.log(元素节点.offsetWidth, 元素节点.offsetHeight);

05 -《事件基础》

DOM 中的事件,实际上指的是给元素节点身上设置事件侦听器。

例如,我们获取到页面中的一个按钮,然后给按钮身上添加一个单击事件的侦听器 function。后续,一旦当用户单击了这个按钮后,就会执行我们的侦听器 function 的代码。

参考代码:

buttonNode.onclick = function () {
}

上述代码中的 function 不需要我们手动调用,而是当浏览器检测到用户对按钮进行了单击操作,就会自动执行该函数。

一、事件三要素

  • 事件源:触发事件的元素

  • 事件类型:例如

    • 鼠标单击

    • 鼠标双击

    • 键盘按下

  • 事件处理函数:当事件发生时,需要执行的一些额外的操作

二、添加事件

DOM 中提供了三种方式给元素身上添加事件:

  1. HTML 标签的事件属性

  2. DOM0 级标准事件

  3. DOM2 级事件

1、HTML 标签的事件属性

<!-- 1. 给按钮添加一个单击事件 -->
<button οnclick="buttonClick()">按钮</button>
<script>
    function buttonClick() {
        console.log("按钮被点击了");
    }
</script>

缺点:

  1. 将 HTML 代码和 JS 代码混在一起编写了,不利于项目的后期维护;

  2. 事件的处理函数必须是一个全局的函数;

2、DOM0 级标准事件

<!-- 2. DOM0 级标准事件: 给按钮添加一个单击事件 -->
<button>按钮</button>
<script>
    var btn = document.querySelector('button');
    btn.onclick = function () {
        console.log("按钮被点击了");
    }
</script>

优点:

  1. 将 HTML 代码和 JS 代码分离,有利于项目的后期维护;

  2. 兼容 IE 低版本的浏览器;

缺点:

  • 不能在同一个元素身上添加多个同类型事件,例如不能在同一个按钮身上添加多个单击事件;

3、DOM2 事件

<button>按钮</button>
<script>
    var btn = document.querySelector('button');
    btn.addEventListener('click', function () {
        console.log("1. 按钮被点击了");
    });
    btn.addEventListener('click', function () {
        console.log("2. 按钮被点击了");
    });
</script>

优点:

  1. 将 HTML 代码和 JS 代码分离,有利于项目的后期维护;

  2. 可以在同一个元素身上添加多个同类型事件;

缺点:

  • 不支持 IE8 及以下版本的浏览器。

06 -《事件类型》

一、鼠标事件

事件类型说明
click鼠标单击时
dblclick鼠标双击时
mousedown鼠标按下时
mouseup鼠标松开时
mouseover鼠标指针进入元素时
mouseout鼠标指针离开元素时
mouseenter鼠标指针进入元素时*
mouseleave鼠标指针离开元素时*
mousemove鼠标移动时

mouseovermouseout 用来触发鼠标的移入和移出事件,当我们将事件添加到父节点身上时,除了父节点以外,子节点也会触发该事件。

mouseentermouseleave 用来触发鼠标的移入和移出事件,当我们将事件添加到父节点身上时,就只有父节点能触发该事件。

二、键盘事件

事件类型说明
keydown按键按下时
keyup按键松开时
keypress按住按键时
  document.onkeydown = function (e) {
        // 回车提交表单
        // 兼容FF和IE和Opera
        var theEvent = window.event || e;
        var code = theEvent.keyCode || theEvent.which || theEvent.charCode;
        if (code == 13) {
        //   queryInfo();
            console.log("按了回车键");
        }
      };

三、表单事件

事件类型说明
focus输入框获得焦点(点击了输入框,可以开始输入内容了)
blur输入框失去焦点(失焦)
change改变表单元素的内容或状态
input输入框内容发生改变

说明:

  1. input 事件在输入框身上,是只要输入框内容发生改变,就会执行 input 事件;

  2. change 事件在输入框身上,需要输入框内容发生改变,同时输入框要是去焦点,才会执行 change 事件;

    事件流

    事件流,指的是事件在页面的嵌套节点之间进行传播。

    一、事件流的分类

    我们把事件流分为三类:

    • 冒泡型事件流

    • 捕获型事件流

    • DOM 事件流:同时包含冒泡和捕获

    DOM0 和 DOM2 添加的事件,默认都是冒泡型事件。其中 DOM2 级事件,可以通过 addEventListener 的第三个参数,设置 true,来将事件流切换为捕获型事件。

    案例代码:

    parent.addEventListener('click', function () {
        console.log('点击了最外层的红色');
    }, true)
    child.addEventListener('click', function () {
        console.log('点击了中间的蓝色');
    }, true)
    grandson.addEventListener('click', function () {
        console.log('点击了最里层的黑色');
    }, true)
    

07 -《事件对象》

所有的事件处理函数,都会自动接收到一个对象,通常用形参 evente 来表示。

基础语法:

元素节点.addEventListener("事件类型", function(e) {
})

一、事件对象的属性

属性名说明
target获取用户操作的节点(即触发事件的节点)
pageX/pageY获取鼠标相当于整个页面的坐标(包括被卷去的body部分的长度)
clientX/clientY获取鼠标相当于视口的坐标(当前body可视区域的坐标)
offsetX/offsetY获取鼠标相当于事件源的坐标
screenX/screenY获取鼠标相当于屏幕的坐标
keyCode获取键盘事件中当前按键的按键码

案例代码:

child.addEventListener('click', function (event) {
    console.log(event.target);
})
parent.addEventListener('click', function (event) {
    // 1. 获取鼠标相对于页面的坐标
    console.log('相对于页面', event.pageX, event.pageY);
    // 2. 获取鼠标相对于视口的坐标
    console.log('相对于视口', event.clientX, event.clientY);
    // 3. 获取鼠标相对于屏幕的坐标
    console.log(event.screenX, event.screenY);
    // 4. 获取鼠标相当于触发事件的元素(event.target)的坐标
    console.log(event.offsetX, event.offsetY);
})
<input type="text" id="myInput">
<script>
    myInput.addEventListener('keyup', function (event) {
        if (event.keyCode === 13) {
            console.log('用户按下了回车键');
        }
    })
</script> 

在这里插入图片描述

二、事件对象的方法

方法名说明
stopPropagation()阻止事件流的传播
preventDefault()阻止元素的默认行为(标签自带的一些功能,例如 a 标签的跳转,button 按钮的表单提交),锚点
parent.addEventListener('click', function () {
    console.log('点击了最外层的红色');
})
child.addEventListener('click', function () {
    console.log('点击了中间的蓝色');
})
grandson.addEventListener('click', function (event) {
    console.log('点击了最里层的黑色');
    event.stopPropagation();
})
<a id="link" href="http://www.baidu.com">连接</a>
<form action="">
    <button id="btn">按钮</button>
</form>
<script>
    btn.addEventListener('click', function (event) {
        event.preventDefault();
    })
    link.addEventListener('click', function (event) {
        console.log('点击了a标签');
        event.preventDefault();
    })
</script>

08 -《事件委托》

事件委托,指的是将子节点的事件,添加到(离子节点最近的静态的祖先节点)的身上。底层利用了事件冒泡的原理。

基础语法:

父节点.onclick = function(event) {
    if(通过 event.target 的属性判断是否是指定子节点) {
        真正点击子节点要执行的操作
    }
}

案例代码:

<ul id="buttons">
    <li><button>按钮1</button></li>
    <li><button>按钮2</button></li>
    <li><button>按钮3</button></li>
</ul>
<script>
    var buttons = document.getElementById('buttons');
    buttons.onclick = function (event) {
        if (event.target.nodeName === 'BUTTON') {
            console.log('点击成功');
        }
    }
</script>

应用场景

  1. 当我们想要给动态生成的元素身上添加事件时,就可以使用事件委托,将事件添加到离它最近的静态父节点身上;

  2. 当我们想要给多个子元素添加同样的事件时,就可以使用事件委托,将事件添加到离它最近的静态父节点身上;

JS 常用知识汇总

一、保存数据

实际开发中,所有需要显示到页面中的去的数据,数据格式基本上都是数组包对象。

参考学生数据:

var studentsData = [
    { id: 1, name: '张三', age: 20 },
    { id: 2, name: '李四', age: 18 },
    { id: 3, name: '王五', age: 22 }
]

二、操作数据

在开发中,对数据的操作,基本上就是新增、修改、删除和渲染这四个操作。

1、数据渲染

数据渲染,指的就是根据 JS 中的数据,创建相应的 HTML 标签,然后将数据和标签一起添加到 HTML 页面中。

常用的数据渲染的方式是 map() 搭配 join(),具体步骤如下:

  1. map() 循环遍历数组,通过 item 去接收到数组中的每一条数据;

  2. map() 中通过 returnitem 数据和 HTML 标签组合在一起,最终得到一个新数组;

  3. 通过 join() 方法将新数组中的每一条数据拼接成一个大字符串。

参考将学生数据渲染成 table(完整写法):

// 获取到 tbody 标签
var studentsTbody = document.getElementById("studentsTbody");
render();
function render() {
    var studentsTrArray = studentsData.map(function(item, index) {
        return (
        	`<tr>
        		<td>${item.name}</td>
        		<td>${item.age}</td>
        	</tr>`
        )
    });
    var studentsHTML = studentsTrArray.join("");
    studentsTbody.innerHTML = studentsHTML;
}

箭头函数(不简写)语法:

// 获取到 tbody 标签
var studentsTbody = document.getElementById("studentsTbody");
render();
function render() {
    var studentsTrArray = studentsData.map((item, index) => {
        return (
        	`<tr>
        		<td>${item.name}</td>
        		<td>${item.age}</td>
        	</tr>`
        )
    });
    var studentsHTML = studentsTrArray.join("");
    studentsTbody.innerHTML = studentsHTML;
}

箭头函数(简写)语法:

// 获取到 tbody 标签
var studentsTbody = document.getElementById("studentsTbody");
render();
function render() {
    var studentsTrArray = studentsData.map(item => (
        	`<tr>
        		<td>${item.name}</td>
        		<td>${item.age}</td>
        	</tr>`
        ));
    var studentsHTML = studentsTrArray.join("");
    studentsTbody.innerHTML = studentsHTML;
}

2、新增数据

新增数据,通常就是往数组中新增一个对象,需要注意的是,新对象的键必须和数组中的旧对象的键完全保持一致

常用的新增数据的方法就是 push(),具体步骤:

  1. 调用数组的 push() 方法, 然后将新对象传递到小括号中;

  2. 数据修改成功后,需要再次调用 render() 方法用最新的数据来重新渲染页面;

参考学生数据的新增:

var newId = 4;
studentsData.push({
    id: newId++,
    name: '新学生的姓名',
    age: '新学生的年龄'
});
render();

3、删除数据

删除数据,是通过数据的 id,找到对应的数据,然后将数据从数组中删除。

常用的删除数组中的数据的方法是 filter(),具体步骤:

  1. filter() 循环遍历数组,通过 item 去接收到数组中的每一条数据;

  2. filter() 中通过 return 来设置筛选条件,最后所有满足条件的 item 会组成一个新数组;

  3. filter() 得到的新数组将原本的旧数组覆盖。

参考学生数据的删除(完整写法):(应该还要用到event.target.dataset.id)

var studentsArray = studentsData.filter(function(item, index) {
    var id = 要删除的学生的 id;
    return item.id != id;
});
studentsData = studentsArray;

箭头函数(不简写)语法:

var studentsArray = studentsData.filter((item, index) => {
    return item.id != 要删除的学生的 id;
});
studentsData = studentsArray;

箭头函数(简写)语法:

var studentsArray = studentsData.filter(item => item.id != 要删除的学生的 id);
studentsData = studentsArray;

4、修改数据

修改数据,实际上即使通过 id 找到要修改的数据对象,然后对该对象的属性进行修改。

修改数据常用的方法是 find(),具体步骤:

  1. 通过 find() 循环遍历数组,通过 item 接收到数组中的每一条数据;

  2. find() 中通过 return 设置查找条件,最终得到的是第一个满足查找条件的 item

  3. 找到对应的对象后,直接修改对象的属性即可。

参考学生数据的修改:

var studentItem = studentsData.find(function(item, index) {
    return item.id === 要修改的学生的 id;
})
studentItem.name = "马冬梅";

箭头函数(不简写)写法:

var studentItem = studentsData.find((item, index) => {
    return item.id === 要修改的学生的 id;
})
studentItem.name = "马冬梅";

箭头函数(简写)写法:

var studentItem = studentsData.find(item => item.id === 要修改的学生的 id);
studentItem.name = "马冬梅";

购物车案例:

// 给 tbody 添加一个点击事件
        list.addEventListener("click", function (event) {
            // 如果用户点击的是删除按钮
            console.log(event.target);

            if (event.target.className === 'deleteBtn') {
                // 通过当前点击的删除按钮,找到对应的数据,删除数组中该条数据
                goodsData = goodsData.filter(item => item.id != event.target.dataset.id);
                render();
            }
            // 如果用户点击的是数量增加的按钮
            if (event.target.className === 'plusBtn') {
                var goodItem = goodsData.find(item => item.id == event.target.dataset.id);
                goodItem.count++;
                render();
            }
            // 如果用户点击的是数量减少的按钮
            if (event.target.className === 'minusBtn') {
                var goodItem = goodsData.find(item => item.id == event.target.dataset.id);
                goodItem.count--;
                if (goodItem.count <= 0) {
                    goodsData = goodsData.filter(item => item.id != event.target.dataset.id);
                }
                render();
            }
        })

  function render() {
            list.innerHTML = goodsData.map(item => (
                `
            <tr>
                <td>${item.id}</td>
                <td>${item.name}</td>
                <td>${item.price}</td>
                <td>
                    <button class="minusBtn" data-id="${item.id}">-</button>
                    ${item.count}
                    <button class="plusBtn" data-id="${item.id}">+</button>
                </td>
                <td>${item.count * item.price}</td>
                <td><button class="deleteBtn" data-id="${item.id}">删除</button></td>
            </tr>
            `
            )).join("");
        }

三、事件处理

1、添加事件

基础语法:

元素节点.onclick = function (event) {
    
}

其中,onclick 根据实际情况替换成其他数据类型。

2、事件委托

事件委托,想要给子节点添加事件,但是子节点是通过 JS 动态生成的,这种情况下,就需要找到子节点外层最近的写在 HTML 中的静态父节点。将儿子的事件添加到静态父节点身上。

基础语法:

父节点.onclick = function(event) {
    if(event.target.nodeName === "大写的标签名") {
        // 点击事件真正要执行的代码
    }
}

说明:其中 .nodeName 可以替换成标签身上的其他属性,例如 classNametype 等。

四、自定义属性

HTML 标签身上,除了自带的原生属性以外,我们还可以自己给标签添加任意属性。通常来说,为了区分与原生属性与自定义属性,我们会以 data- 来定义自定义属性的名称。

添加自定义属性的基础语法:

<标签名 data-属性名="属性值"></标签名>

在 JS 中获取自定义属性值的基础语法:

var 属性值 = 元素节点.dataset.属性名;

通常,当我们需要对数组中的某一条数据进行修改或删除时,我们会将自定义属性添加在执行(触发)事件的元素节点身上。

例如,要删除学生,我们可以将学生的 id 添加到删除按钮身上,或者要增加商品的数量,可以将商品的 id 添加到增加的按钮身上....

五、时间函数

JS 中提供了两个时间函数:

  • setInterval():每隔指定的时间都会执行(执行多次);

  • setTimeout():按照指定时间推迟执行(执行一次);

基础语法:

setInterval(function() {
    
}, 1000)

setInterval(() => {
    
}, 1000)

我们也可以将第一个参数(函数)单独提取出来:

function tiemr() {
    
}

setInterval(tiemr, 1000);

通常,当我们的功能需要在时间函数暂停后又重启,建议将时间函数的第一个参数单独提取出去。

function tiemr() {
    console.log("每隔1秒钟输出一次")
}
var intervalId = setInterval(tiemr, 1000);

当用户点击暂停按钮,需要暂停计时器:

clearInterval(intervalId);

当用户点击继续按钮,需要重启计时器:

intervalId = setInterval(tiemr, 1000);

注意:重启计时器时,不需要再通过 var 去创建一个变量,而是直接对原来的计数器变量进行赋值。

六、函数

针对我们自己手动创建(封装)的函数,根据写法不同,可以将函数的创建分为声明式和表达式:

// 声明式
function 函数名() {
    
}
// 表达式
var 变量名 = function() {
    
}

区别:

  1. 声明式的函数没有创建的顺序要求,可以在创建函数的代码之前,去调用该函数;

  2. 表达式的函数有创建的顺序要求,必须在函数创建完成后,再调用;

声明式参考代码:

render();

function render() {
    
}

表达式参考代码:

var render = function() {
    
}

render();

10 -《对象的分类》

我们将 JavaScript 中的对象分为三类:

  • 宿主对象

  • 内置对象

  • 本地对象

宿主对象

因为 JS 是需要在浏览器中运行,所以浏览器就是 JS 的宿主。因此宿主对象,指的就是浏览器提供的对象。BOM 和 DOM 中所有的对象都是宿主对象。

内置对象和本地对象

内置对象和本地对象,指的都是 JS 自带的对象,不是宿主提供的,也不需要我们手动去创建。区别在于:

  • 本地对象在使用时,需要通过 new 来得到对象。例如:ArrayObjectStringDateRegExp…;

  • 内置对象在使用时,可以直接使用,不需要再通过 new 来得到对象。例如:MathJSON…;

11 -《字符串的属性和方法》

字符串的属性

属性名说明
length获取字符串的长度

字符串的方法

方法名参数说明返回值
includes()是否包含的字符判断字符串中是否包含指定的字符布尔值
split()分割字符串的字符用指定的字符将字符串拆分成一个数组数组
indexOf()查找的字符查找指定字符在字符串中第一次出现的位置下标下标或 -1
lastIndexOf()查找的字符查找指定字符在字符串中最后一次出现的位置下标下标或 -1
substring()开始和截止的索引截取父字符串的某一部分(不包括截止位置的字符)截取后的字符串

参考代码:

var str = 'HEllo';
console.log(str.includes("h"));
console.log(str.split('e'));
方法名参数说明返回值
trim()去掉字符串的首尾空格去掉首尾空格后的字符串
toLowerCase()将字符串中的字母转换为小写字母转换后的字符串
toUpperCase()将字符串中的字母转换为大写字母转换后的字符串

参考代码:

var str = 'HEllo';
console.log(str.trim());
console.log(str.toLowerCase());
console.log(str.toUpperCase());

字符串的遍历(了解)

var str = 'HEllo';
for (var i = 0; i < str.length; i++) {
    console.log(str[i]);
}
for (var item of str) {
    console.log(item);
}

12 -《数学对象 Math》

Math 的属性

属性名说明
PI获取数学圆周率值

Math 的方法

方法名参数说明
min()多个数字求多个数字中的最小值
max()多个数字求多个数字中的最大值
ceil()数字对当前数进行上舍入(向上取整)
floor()数字对当前数进行下舍入(向下取整)
round()数字对当前数进行四舍五入
random()返回 0 ~ 1 之间的随机数
abs()数字返回绝对值

代码案例:

console.log(Math.max(10, 1, 2, 20, 3, 4, 5));
console.log(Math.min(10, 1, 2, 20, 3, 4, 5));
var num = 10.8;
console.log('ceil', Math.ceil(num));    // 11   11
console.log('floor', Math.floor(num));  // 10   10
console.log('round', Math.round(num));  // 10   11
function getRandom(x, y = 0) {
    var min = Math.min(x, y);
    var max = Math.max(x, y);
    return parseInt(Math.random() * (max - min + 1) + min);
}
console.log(getRandom(10));
// 产生一个车牌号,如川A3D4Q5(三个随机数字,两个字母,排序也是随机的)
getOneCarNumber(){
            // 产生两个字母。三个数字,随机打乱
            let array = []
            // 产生数字
            for (let index = 0; index < 3; index++) {
                let num = Math.floor(Math.random()*10)
                array.push(num)
            }
            // 产生两个字母
            // ASCII
            for (let index = 0; index < 2; index++) {
                const num = Math.floor(Math.random()*26 + 65)
                // 将这个数字转化位字符串(将65-90的数字转成A-Z的字母)
                const chart = String.fromCharCode(num)
                array.push(chart)
            }
            // 打乱顺序
            array.sort((a,b)=>{
                // sort方法自定义排序,传递一个函数。这个函数 正数 负数 0
                return 0.5-Math.random()  //一半的几率反转顺序
                // return a - b
            })
            return "川A "+array.join("")
        }

13 -《日期对象 Date》

创建 Date 对象

要创建一个日期对象,使用 new 操作符和 Date 构造函数即可。如下所示:

var nowDate = new Date();

在调用 Date 构造函数而不传递参数的情况下,新创建的对象自动获得当前本地日期和时间。

也可以通过传参的方式来指定日期:

new Date(2006, 0, 12);

上面的创建形式表示 2006 年 1 月 12 日这一天。

获取日期的方法

通过日期对象,调用以下方法,获取日期和时间:

方法说明
getFullYear()返回 Date 对象“年份”部分的实际数值。
getMonth()返回 Date 对象“月份”部分的数值(0 ~ 11)。
getDay()返回 Date 对象“星期”部分的数值(0 ~ 6)。
getDate()返回 Date 对象“日期”部分的数值(1 ~ 31)。
getHours()返回 Date 对象“小时”部分的数值(0 ~ 23)。
getMinutes()返回 Date 对象“分钟”部分的数值(0 ~ 59)。
getSeconds()返回 Date 对象“秒”部分的数值(0 ~ 59)。
getMilliSeconds()返回 Date 对象“毫秒”部分的数值(0 ~ 999)。
getTime()返回 Date 对象与 UTC 时间 1970 年 1 月 1 日午夜之间相差的毫秒数。
var dt = new Date();
var year = dt.getFullYear();
var month = dt.getMonth() + 1;
var date = dt.getDate();
var day = dt.getDay();
var hours = dt.getHours();
var minutes = dt.getMinutes();
var seconds = dt.getSeconds();

得到一个 2022-7-13 17:02:45 格式的时间

let yy = new Date().getFullYear();
let mm = new Date().getMonth()+1;
let dd = new Date().getDate();
let hh = new Date().getHours();
let mf = new Date().getMinutes()<10 ? '0'+new Date().getMinutes() : new Date().getMinutes();
let ss = new Date().getSeconds()<10 ? '0'+new Date().getSeconds() : new Date().getSeconds();
let time = yy+'-'+mm+'-'+dd+' '+hh+':'+mf+':'+ss;

14 -《JSON 对象》

JSON,全称是 JavaScript Object Notation。

JSON 对象和 JS 对象

JSON 对象和 JS 对象很类似,但是有区别。

JSON 对象的代码参考:

{
    "name": "张三",
    "age": 20
}

JS 对象的代码参考:

var student = {
    name: '张三',
    age: 20,
}

我们将 JSON 对象和 JS 对象的区别做一个汇总:

区别JSON 对象JS 对象
必须加双引号可以加引号,也可以不加
字符串只能使用双引号单引号、双引号、反单引号(模板字符串)都可以
逗号最后一条数据末尾不能加逗号最后一条数据末尾可以加逗号,也可以不加
值的类型值不能是 undefined 或 function值可以是任意类型的数据

JSON 对象的方法

JSON 对象提供了两个方法:

  1. JSON.stringify():将 JS 对象转换为 JSON 格式的字符串;也可以把number转为字符串

  2. JSON.parse():将 JSON 格式的字符串转换为 JS 对象;

示例代码:

var student = {
    name: '张三',
    age: 20,
    gender: "男",
}
// JS 对象转换为 JSON 格式的字符串
var jsonString = JSON.stringify(student);
var jsonString = '{"name":"张三","age":20,"gender":"男"}';
// JSON 格式的字符串转换为 JS 对象
var student = JSON.parse(jsonString);

15 -《正则表达式》

正则对象,我们通常叫做“正则表达式”。英文全称是 Regular Expression,也可以翻译为“规则表达式”。

我们可以使用正则表达式来定义一个规则,可以用来对字符串进行验证、查找、替换等操作。

一、创建正则

正则表达式的创建方式有两种:字面量和构造函数。

1、字面量方式

 var regExp = /规则/修饰符;

2、构造函数方式

 var regExp = new RegExp('规则', '修饰符');

说明:在创建正则时,“规则”是必填的,“修饰符”是可选的。

案例代码:

 var regExp = /a/;

上述例子中的正则,表示的意思就是:字符串中需要包含字母 a

二、正则的方法

方法名参数说明返回值
test()字符串判断字符串是否匹配正则表达式布尔值
exec()字符串根据正则在字符串中查找对应的值数组(包含找到的值)或 null
compile()字符串修改正则的内容

示例代码:

var regExp = /eo/;
var str = 'hello eo';
console.log(regExp.test(str));
console.log(regExp.exec(str));
regExp.compile('b');
console.log(regExp.test(str));

三、正则规则

1、方括号表达式

表达式说明
/[abc]/包含 [] 中的任意一个字符
/[^abc]/包含除了 [] 中以外的其他任意字符
/[0-9]/包含任意数字
/[a-z]/包含小写 a - z 之间的任意字母
/[A-Z]/包含大写 A - Z 之间的任意字母

示例代码:

// var regExp = /[eo]/;
// var regExp = /[^a]/;
// var regExp = /[0-9]/;
// var regExp = /[a-z]/;
// var regExp = /[A-Z]/;
var regExp = /[A-Za-z0-9]/;
console.log(regExp.test('100px'));

2、元字符

元字符说明
/\w/包含数字、字母、下划线中的任意字符。等同于 /[0-9a-zA-Z_]/
/\W/包含除了数字、字母、下划线以外的其他任意字符。等同于 /[^0-9a-zA-Z_]/
/\d/包含任意数字。等同于/[0-9]/
/\D/包含除了数字以外的其他任意字符。等同于 /[^0-9]/
/\s/包含空白字符(空格)。
/\S/包含除了空白字符以外的其他字符。

3、量词

量词说明
/n+/包含至少一个指定字符,可以理解为 n 连续出现的次数 >= 1。
/n*/指定字符可有可无,可以理解为 n 连续出现的次数 >= 0。
/n?/指定字符出现 0 或 1 次,可以理解为 n 出现的次数 = 0 或者 = 1。
/n{x}/指定字符连续出现 x 次(x 是任意数字)
/n{x,}/指定字符至少连续出现 x 次(x 是任意数字),可以理解为 n 出现的次数 >= x。
/n{x,y}/指定字符出现最少 x 次,最多 y 次。可以理解为 n 出现的次数 >= x 并且 <= y。

4、开始和结束

符号说明
^n表示以指定字符为开头
n$表示以指定字符为结尾

通常来说,只要是对字符串的内容进行验证,都会在正则中加入“开始 ^”和“结束 $”。

示例代码:

// 密码中包含数字、字母、下划线,以大写字母开头,6-20位。
var passwordRe = /^[A-Z]\w{5,19}$/;
console.log(passwordRe.test('A123123'));

5、其他符号

符号说明
.大体上可以将 . 看作是通配符(实际上不能匹配一些特殊符号,例如换行符 \n、回车符、制表符等)。
竖线表示“或”
()对规则进行分组
\转义符

示例代码:

var regExp = /^(a|b)$/;
console.log(regExp.test('a'))

四、修饰符

修饰符说明
i不区分大小写。
g对所有字符串进行匹配(而非在找到第一个匹配后停止)。
m多行匹配(即在到达一行文本末位时还会继续查找下一行)。

其中,gm 仅作了解。

示例代码:

var regExp = /^[a-z]{6,10}$/i;
console.log(regExp.test('ABCDEFG'));  // true

01 -《本地存储》

本地存储,简单来说就是将用户的相关数据保存在浏览器中。

早期的本地存储采用的方式是 cookie,但是由于 cookie 存储大小有限,所以在 HTML5 中新增了两种本地存储的方式:

  • localStorage:永久性的本地存储,只要不手动删除数据,数据会一直保存在浏览器中;

  • sessionStorage:会话级的本地存储,只在当前窗口有效,窗口关闭后数据会自动销毁。

本地存储的方法

localStorage 和 sessionStorage 的方法都是一样的:储存类型为字符串

方法名参数说明
setItem()1. 数据的键 2. 数据的值保存、修改数据
getItem()需要获取的数据的键获取数据
removeItem()需要删除的数据的键删除数据
clear()清空数据

案例代码:

// 保存数据
localStorage.setItem('name', '张三');
localStorage.setItem('age', 20);
// // 修改数据
localStorage.setItem('name', '李四');
// 获取数据
var name = localStorage.getItem('name');
// console.log(name);
// 删除数据
localStorage.removeItem('age');
// 清空数据
localStorage.clear();

点运算符操作数据

由于 localStorage 和 sessionStorage 也是对象,因此我们也可以直接使用 . 点运算符来操作本地存储的数据。

 // 保存、修改数据
 localStorage.name = "张三";
 sessionStorage.name = "张三";
 // 获取数据
 console.log(localStorage.name);
 console.log(sessionStorage.name);

将数据存入本地储存

 var users = [
            { account: '123', password: '123' },
            { account: '000', password: '000' },
        ];
        console.log(users);
        localStorage.users = JSON.stringify(users);
        console.log(JSON.parse(localStorage.users));

02 -《canvas 画布》

一、准备工作

1、创建画布

<canvas width="600" height="600"></canvas>

注意:

  1. canvas 的大小不要通过 CSS 来调整,直接在 canvas 元素身上通过属性来设置;

  2. canvas 的背景颜色或边框可以在 CSS 中自行设置。

2、创建画笔(创建上下文)

var myCanvas = document.getElementById('myCanvas');
var pen = myCanvas.getContext('2d');

画布和画笔创建完成后,就可以开始绘画了。

二、绘制矩形

canvas 中提供了四种绘制矩形的方法:

方法名参数说明
fillRect()横坐标、纵坐标、矩形宽度、矩形高度绘制有背景颜色的矩形
strokeRect()横坐标、纵坐标、矩形宽度、矩形高度绘制有边框的矩形
rect()横坐标、纵坐标、矩形宽度、矩形高度绘制既没有背景颜色也没有边框的矩形
clearRect()横坐标、纵坐标、矩形宽度、矩形高度擦除矩形区域(类似橡皮擦功能)

案例代码:

// 1. 绘制实心矩形
pen.fillRect(0, 0, 100, 100);
// 2. 绘制空心矩形
pen.strokeRect(50, 50, 100, 100);
// 3. 绘制一个即没有背景颜色也没有边框的矩形
pen.rect(150, 150, 100, 100);
// 4. 清除矩形区域(类似橡皮擦)
// pen.clearRect(0, 0, 600, 600); 

三、填充和描边

canvas 中还提供了两个方法,来为图形进行填充和描边。填充指的就是给图形添加背景颜色,描边指的就是给图形添加边框。

方法名参数说明
fill()给前面所有可以添加背景的图形的添加背景颜色
stroke()给前面所有可以添加边框的图形的添加边框

案例代码:

pen.rect(150, 150, 100, 100);
// 填充和描边(添加背景和添加边框)
pen.fill();
pen.stroke();

四、绘制路径(画线条)

canvas 中绘制路径的方式,是通过确定两个点的坐标,然后通过 stroke() 方法将两个连接起来,实现线条的绘制。

方法名参数说明
moveTo()横坐标、纵坐标设置线条的起点坐标
lineTo()横坐标、纵坐标设置线条下一个点的坐标

案例代码:

// 设置线条的起点
pen.moveTo(400, 400);
// 设置线条下一个点的坐标
pen.lineTo(400, 500);
pen.lineTo(500, 500);
// 连接以上所有的点
pen.stroke();

五、闭合图形

当我们使用线条绘制多边形时,最后需要将图形完整的闭合(不建议手动将终点和起点设置在一起实现闭合),canvas 提供了专门的方法来实现多边形的闭合。

方法名参数说明
closePath()闭合多边形

案例代码:

pen.moveTo(300, 300);
pen.lineTo(300, 400);
pen.lineTo(400, 400);
// 闭合图形
pen.closePath();
pen.stroke();

六、路径初始化

由于 fill()stroke() 两个方法都会作用于它们前面所有的图形,这样就导致我们没办法去分别绘制实心和空心的图形。因此我们需要在每次绘制一个新的图形之前,都使用 beginPath() 方法对路径做一个初始化处理,相当于将前一个图形的路径与当前图形的路径做一个隔离,从而让 fill()stroke() 只作用于当前图形。

方法名参数说明
beginPath()对路径进行初始化处理,将前一个图形路径与当前图形路径隔离

案例代码:

// 第一个三角形
pen.moveTo(100, 100);
pen.lineTo(200, 200);
pen.lineTo(200, 100); 
pen.closePath();
// 绘制图形轮廓
pen.stroke();
// 第二个三角形
// 绘制新图形前,清空掉上一个图形中的路径(必须)。
pen.beginPath();
pen.moveTo(200, 200);
pen.lineTo(300, 300);
pen.lineTo(300, 200);
pen.closePath();
// 填充整个图形内部
pen.fill();

七、绘制圆形

canvas 中提供了一个 arc() 方法用来绘制圆形。

方法名参数说明
arc()圆心横坐标、圆心纵坐标、半径、起始角度、结束角度、是否逆时针(默认 false)绘制圆形

注意:第四、五个参数用来设置开始和结束的角度,需要将实际的角度经过以下计算后传入到 arc 方法中:

(Math.PI / 180) * 实际角度

案例代码:

// 清空路径容器
pen.beginPath();
// 画圆
pen.arc(100, 100, 50, 0, (Math.PI / 180) * 360);
// 绘制圆的边框
pen.stroke();

八、设置样式

canvas 中提供了以下几种方式来设置样式:

属性名可选属性值说明
fillStyle颜色字符串设置图形的填充(背景)颜色。
strokeStyle颜色字符串设置图形的轮廓(边框)颜色。
lineWidth数字设置线条的粗细(属性值必须是正数)。
lineJoinround(圆角)、bevel(斜角)设置线条与线条间交接处的样式。
lineCapround(圆角)设置每一条线段两端的样式。

注意:所有的样式都是作用于它们后面的图形。

案例代码:

pen.beginPath();
pen.lineWidth = 20;
pen.fillStyle = 'pink';
pen.strokeStyle = 'green';
pen.lineJoin = "round";
pen.lineCap = "round";
// 绘制一个三角形
pen.moveTo(200, 200);
pen.lineTo(300, 300);
pen.lineTo(300, 200);
pen.closePath();
pen.stroke();
pen.fill();

九、样式初始化

由于 canvas 中的所有样式都会作用于后面的所有图形,这样也会导致我们很多图形应用了相同的样式。因此,我们也需要对样式进行处理,来实现每一个图形都使用自己的样式。

方法名参数说明
save()将当前样式保存到内存中
restore()从内容取出上一个保存到样式,并作用于当前样式

案例代码:

pen.beginPath();
pen.save();
    pen.fillStyle = "tomato";
    pen.strokeStyle = "orange";
    pen.lineWidth = 20;
    pen.moveTo(400, 400);
    pen.lineTo(400, 500);
    pen.lineTo(500, 500);
    pen.closePath();
    pen.stroke();
    pen.fill();
pen.restore();

十、转换

转换方法参数说明
translate()1. 左右移动的距离 2. 上下移动的距离移动画布的 (0, 0) 点
scale()1. 横向缩放比例 2. 纵向缩放比例对画布内容进行缩放
rotate()(Math.PI / 180) * 实际旋转角度将画布相对于 (0, 0) 点进行旋转

说明:

  1. canvas 的转换方法都是作用于后面的图形;

  2. canvas 的转换方法连续多次调用时,效果会叠加;

案例代码:

// 转换
// translate 位移
pen.translate(100, 100);
// pen.translate(100, 100);
// pen.translate(100, 100);
// scale 缩放
// pen.scale(0.5, 0.5);
// rotate 旋转
pen.rotate(Math.PI / 180 * 45);
pen.rotate(Math.PI / 180 * 45);
pen.fillRect(0, 0, 100, 100);

总结

当我们在绘制图形时,既有样式,又有转换,还有路径以及图形,那么我们建议,大家的代码顺序格式参考如下:

pen.beginPath();
pen.save();
    // 转换
    // 设置样式
    // 绘制图形
    // pen.stroke();
    // pen.fill();
pen.restore();

03 -《拖放》

HTML5 中新增了一个拖放功能。默认情况下,只有 img 标签是可拖放元素,其他标签,如果需要变成可拖放元素的话,需要在标签身上添加一个属性 draggable="true"

<div id="box" draggable="true"></div>

拖放事件

拖放事件分为两类:

  1. 关于被拖拽元素的事件

  2. 关于投放区元素的事件

关于被拖拽元素的事件

事件类型说明
dragstart拖拽开始时触发的事件
drag拖拽过程中触发的事件
dragend拖拽结束时触发的事件

案例代码:

var box = document.getElementById('box');
box.ondragstart = function () {
    console.log('被拖拽元素:拖拽开始');
}
box.ondrag = function () {
    console.log('被拖拽元素:拖拽进过程中');
}
box.ondragend = function() {
    console.log('被拖拽元素:拖拽结束');
}

关于投放区元素的事件

事件类型说明
dragenter当被拖拽元素进入投放区域时触发的事件(鼠标指针进入投放区)
dragover拖拽元素在投放区域内移动时触发的事件
dragleave当被拖拽元素离开投放区域时触发的事件
drop当被拖拽元素在投放区域投放时触发的事件(当拖拽元素在投放区域中,鼠标松开时触发的事件)

案例代码:

drop.ondragenter = function () {
    console.log('投放区元素:拖拽元素进入投放区');
}
drop.ondragover = function (event) {
    event.preventDefault();
    console.log('投放区元素:拖拽元素在投放区移动');
}
drop.ondragleave = function () {
    console.log('投放区元素:拖拽元素离开投放区');
}
drop.ondrop = function () {
    console.log('投放区元素:在投放区松开拖拽元素');
}

说明:dragover 事件的默认行为会阻止 drop 事件的执行,因此,如果需要使用 drop 事件时,需要在 dragover 事件中组件事件的默认行为。

04 -《百度地图》

HTML5 中提供了 Geolocation 方法用来获取用户位置。在实际开发中,通常都会引入第三方的地图插件,例如:百度地图、腾讯地图、高德地图等。

一、准备工作

1、登录账号

访问百度地图 API 的官网 https://lbsyun.baidu.com/,登录百度账号。

2、创建应用

从首页的【控制台】进入到【应用管理】中的【我的应用】,创建当前项目应用,最后自动生成对应的 AK 密钥:

二、基本使用

浏览器端使用百度地图的开发文档参考下图:

我们可以先将文档中的 Hello Word 案例复制下来运行查看效果:

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>Hello, World</title>
    <style type="text/css">
        html {
            height: 100%
        }
        body {
            height: 100%;
            margin: 0px;
            padding: 0px
        }
        #container {
            height: 100%
        }
    </style>
    <script src="https://api.map.baidu.com/api?v=1.0&type=webgl&ak=gYFqshTZIiqLFdMByY2KRPQtFhKy7lSz">
        </script>
</head>
<body>
    <div id="container"></div>
    <script type="text/javascript">
        var map = new BMapGL.Map("container");  // 创建地图实例     
        var point = new BMapGL.Point(116.404, 39.915); // 创建点坐标      
        map.centerAndZoom(point, 15);  // 初始化地图,设置中心点坐标和地图级别
    </script>
</body>
</html>

1、定位

通过浏览器地位到用户所在城市:

var map = new BMapGL.Map("container");  // 创建地图实例 
var point = new BMapGL.Point(116.404, 39.915); // 创建点坐标
map.centerAndZoom(point, 15);  // 初始化地图,设置中心点坐标和地图级别
var geolocation = new BMapGL.Geolocation();  // 创建定位实例对象
geolocation.getCurrentPosition(function (r) {   // 获取当前位置
    if (this.getStatus() == BMAP_STATUS_SUCCESS) {
        var mk = new BMapGL.Marker(r.point);  // 创建标记点
        map.addOverlay(mk);     // 将标记点添加到地图上
        map.panTo(r.point);     // 将地图的中心位置移动到指定坐标点
    }
    else {
        alert('failed' + this.getStatus());
    }
});

2、(正)地址解析

(正)地址解析,指的是可以通过用户给定的中文地址,获取到该地址对应经纬度:

var map = new BMapGL.Map("container");  // 创建地图实例 
var point = new BMapGL.Point(116.404, 39.915); // 创建点坐标
map.centerAndZoom(point, 15);  // 初始化地图,设置中心点坐标和地图级别
//创建地址解析器实例
var myGeo = new BMapGL.Geocoder();
// 将地址解析结果显示在地图上,并调整地图视野
myGeo.getPoint('成都市高新区孵化园5号楼', function (point) {
    console.log(point);  // 获取到当前地址对应的经纬度
    if (point) {
        map.centerAndZoom(point, 16);  // 将地图的中心点设置为当前位置
        map.addOverlay(new BMapGL.Marker(point, { title: '成都市高新区孵化园5号楼' }));  // 添加标记点
    } else {
        alert('您选择的地址没有解析到结果!');
    }
}, '北京市')

3、逆地址解析

逆地址解析,根据给定的经纬度,解析出当前经纬度对应的中文地址。

我们通过定位方法获取到用户当前的经纬度,然后解析出用户所在位置的中文地址:

var map = new BMapGL.Map("container");  // 创建地图实例 
var point = new BMapGL.Point(116.404, 39.915); // 创建点坐标
map.centerAndZoom(point, 15);  // 初始化地图,设置中心点坐标和地图级别
var geolocation = new BMapGL.Geolocation();  // 创建定位实例对象
geolocation.getCurrentPosition(function (r) {   // 获取当前位置
    if (this.getStatus() == BMAP_STATUS_SUCCESS) {
        var mk = new BMapGL.Marker(r.point);  // 创建标记点
        map.addOverlay(mk);     // 将标记点添加到地图上
        map.panTo(r.point);     // 将地图的中心位置移动到指定坐标点
        // 逆地址解析
        // 创建地理编码实例      
        var myGeo = new BMapGL.Geocoder();
        // 根据坐标得到地址描述    
        myGeo.getLocation(r.point, function (result) {
            if (result) {
                alert(result.address);
            }
        });
    }
    else {
        alert('failed' + this.getStatus());
    }
});

05 -《移动端网页开发》

一、移动端开发

  1. 移动端网页开发:用户不需要下载其他软件,只要有浏览器就能访问页面;

  2. 原生 APP 开发:用户需要下载安装包,安装成功后才能使用;

  3. 小程序开发:用户需要下载微信,在微信中使用各种小程序;

二、视口 viewport

浏览器网页的开发,从早期的布局视口、视觉视口,然后发展到现在的理想视口。

使用理想视口来开发移动端网页,需要在 .html 文件的 `` 标签中添加以下代码:

<meta name="viewport" content="width=device-width, initial-scale=1.0">

三、尺寸单位

移动端开发中,可以选择的尺寸单位有以下几种:

单位说明
em1em 等于元素自身的 font-size 大小(如果自己没有设置 font-size,是会继承父级的 font-size)
rem1rem 等于 html 的 font-size 大小(默认 16px)
vh1vh 等于视口高度的 1%,100vh 等于视口高度
vw1vw 等于视口宽度的 1%,100vw 等于视口宽度

在开发移动端网页时,布局建议优先考虑弹性布局,尺寸单位优先考虑 rem,如果需要全屏时可以使用 100vh100vw

06 -《jQuery》

jQuery,实际上就是一系列节点操作的方法组成的一个 JS 文件,我们把这个 JS 文件称为“库”。

一、准备工作

1、下载

由于 jQuery 是一个别人封装好的 JS 库,因此我们如果需要在项目中使用的话,需要先将其源码下载下来,然后在 .html 中引入后使用。

jQuery 官网:https://jquery.com/

如果点击下载连接是直接打开的源码,可以将源码全部复制后,在本地自己新建一个 .js 文件将 jQuery 的源码粘贴进去。

2、引入

下载成功后,将 jQuery 的文件添加到项目目录中,然后在 .html 中通过 `` 引入后使用:

<body>
    <!-- 页面标签代码 -->
    <!-- 引入 jQuery -->
    <script src="./js/jquery-3.6.0.min.js"></script>
    <script>
        // 自己的 JS 代码
    </script>
</body>

二、jQuery 常用方法

jQuery 的方法可以查看 jQuery API 网站:https://jquery.cuishifeng.cn/

1、通过选择器获取元素节点

基础语法如下:

$("选择器");

示例代码:

<body>
    <div id="box"></div>
    <div class="box"></div>
    <input type="checkbox">
    <input type="checkbox" checked>
    <input type="checkbox">
    <script src="./js/jquery-3.6.0.min.js"></script>
    <script>
        var boxId = $("#box");
        var boxClass = $(".box");
        var boxes = $("div");
        var checkboxes = $('[type=checkbox]:checked')
    </script>
</body>

2、操作元素的属性

HTML 元素身上的属性可以分为三类:

  1. 普通原生属性:必须有属性名和属性值。例如:hrefsrc 等;

  2. 特殊原生属性:只要有属性名就能使用。例如:checkedselected 等;

  3. 自定义属性:开发者自己在标签身上添加以 data- 为前缀来命名的属性。例如:data-iddata-index 等。

在 jQuery 中,针对以上三类属性,提供了三种不同的属性操作方法:

  1. 原生普通属性:attr()removeAttr()

  2. 特殊原生属性:prop()removeProp()

  3. 自定义属性:data()

    使用.prop()方法来调整元素的属性,把按钮设置成禁用以后,按钮会变灰并且不能点击,代码为$("button").prop("disabled", true);

示例代码:

var srcValue = $('img').attr('src');  // 获取 img 的 src 属性值
$('img').attr('src', './logo.png');   // 修改或新增 img 的 src 属性值
$('img').attr({                       // 修改或新增 img 的 src 和 title 属性
    src: './logo.png',
    title: '蜗牛学院'
}); 
var checkedValue = $('[type=checkbox]').attr('checked');  // 获取复选框是否被选中
$('[type=checkbox]').attr('checked', true);               // 设置复选框被选中
$('[type=checkbox]').attr({                               // 设置复选框被选中且被禁用
    checked: true,
    disabled: true
});
var dataIdValue = $('button').data('id');   // 获取按钮的 data-id 的属性值
$('button').data('id', 1);                  // 新增或修改按钮的 data-id 的属性
$('button').data({                          // 新增或修改按钮的 data-id 和 data-name 属性
    id: 2,
    name: '张三'
});

说明:使用 jQuery 的 data() 方法添加的自定义属性,在元素身上不会显示出来。

3、操作元素的 class

jQuery 中提供了三种方法来操作元素的 class:

  • 新增一个或多个 class:addClass()

  • 删除一个或多个 class:removeClass()

  • 切换一个或多个 class:toggleClass()

示例代码:

$('div').addClass('box container');    // 给 div 添加 box 和 container 两个 class
$('div').removeClass('box container'); // 删除 div 的 box 和 container 两个 class
$('div').removeClass();                // 删除 div 的所有 class 
$('div').toggle('box container');      // 切换 div 的 box 和 container 两个 class

4、操作元素的内容

jQuery 中提供了三种方法来操作元素的内容:

  • 操作元素的内容(包括 HTML 标签):html()

  • 操作元素的文本内容(不解析 HTML 标签):text()

  • 操作表单元素的值:val()

示例qing'kong 代码:

var divHTML = $('div').html();     // 获取 div 中的内容
$('div').html('<p>Hello</p>');     // 将 div 中的内容修改为一个 p 标签
var divHTML = $('div').text();     // 获取 div 中的文本内容
$('div').text('Hello');            // 将 div 中的内容修改为一个文本 Hello
var inputValue = $('input[type=text]').val();     // 获取输入框的 value 值
$('input[type=text]').val('123');                 // 将输入框的值设置为 123

5.操作CSS样式

$("p").css({ "color": "#ff0011", "background": "blue" });

项目准备工作

HTML(页面布局)

<body>
    <!-- 头部 -->
    <header>
        <!-- 顶部登录注册按钮 -->
        <div class="header-top">
            <div class="center-container">
                <a href="#">下载 APP</a>
                <div>
                    <a href="#">登录</a>
                    <a href="#">注册</a>
                </div>
            </div>
        </div>
        <!-- 导航栏 -->
        <div class="header-nav">
            <div class="center-container">
                <!-- logo -->
            </div>
        </div>
        <!-- 轮播图 -->
        <div class="header-banner">
            <div class="center-container">

            </div>
        </div>
    </header>

    <!-- 主体内容 -->
    <main>
        <div class="center-container">
            <!-- 下拉列表 -->
            <div></div>
            <!-- 电影相关信息 -->
            <div>
                <!-- 电影列表 -->
                <div class="movies-list">
                    <!-- 正在热映 -->
                    <div>
                        <!-- 标题 -->
                        <div></div>
                        <!-- 正在热映的电影列表 -->
                        <div class="moviesList" id="hotMoviesList">
                            <!-- 每一个电影项 -->
                            <!-- <div class="movie-item">
                                <img src="./images/img-10.jpg" alt="">
                                <div class="movie-name">
                                    <span>怒火重案</span>
                                    <span>9.0</span>
                                </div>
                            </div> -->
                        </div>
                    </div>
                    <!-- 即将上映 -->
                    <div>
                        <div></div>
                        <div class="moviesList" id="comingMoviesList"></div>
                    </div>
                </div>
                <!-- 电影活动 -->
                <div></div>
            </div>
        </div>
    </main>
    <!-- 底部 -->
    <footer>
        <div class="center-container">
            1212124124124
        </div>
    </footer>

    <script src="./utils/jquery-3.6.0.min.js"></script>
    <script src="./utils/utils.js"></script>
    <script src="./js/getData.js"></script>
    <script src="./js/home.js"></script>
</body>

CSS

* {
    padding: 0;
    margin: 0;
}

.header-top {
    height: 40px;
    background-color: #f0f0f0;
}

.center-container {
    width: 1200px;
    margin: 0 auto;
    /* outline: 1px solid red; */
}

.header-top .center-container {
    display: flex;
    justify-content: space-between;
    line-height: 40px;
    /* align-items: center; */
}

.header-banner {
    height: 360px;
    background-image: url('../images/banner02.png');
    background-position: center;
}

.movies-list {
    width: 730px;
}

.moviesList {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-around;
}

.movie-item {
    width: 160px;
    border: 1px solid #ddd;
    margin: 20px 0;
}

.movie-item > img {
    width: 160px;
    height: 220px;
}

.movie-item > .movie-name {
    display: flex;
    justify-content: space-between;
    line-height: 40px;
    padding: 0 10px;
}


footer {
    height: 290px;
    background-color: #262426;
}

JS获取数据库数据

// 数据文件的路径,应该相对于 HTML 文件的位置去进行查找

// 获取所有电影数据
// $.get('./data/movies.json', function (data) {
//     setStorage('movies', data.movies);
// });

// 获取即将上映的数据
// $.get('./data/upcoming.json', function (data) {
//     setStorage('comingMovies', data.slice(0, 8))
// });

// 获取正在热映的数据
$.get('./data/nowplaying-电影.json', function (data) {
    console.log(data); // [] 35
    setStorage('hotMovies', data.slice(0, 8))
})

JS数据渲染

// 获取正在热映的电影数据
var hotMovies = getStorage('hotMovies');
// 获取即将上映的电影数据
var comingMovies = getStorage('comingMovies');


var hotMoviesHTML = moviesRender(hotMovies);
$('#hotMoviesList').html(hotMoviesHTML);

var comingMoviesHTML = moviesRender(comingMovies);
$('#comingMoviesList').html(comingMoviesHTML);


function moviesRender(movies) {
    var moviesHTML = movies.map(item => (
        `<div class="movie-item">
            <img src="./images/img-10.jpg" alt="">
            <div class="movie-name">
                <span>${item.title}</span>
                <span>${item.score || ''}</span>
            </div>
        </div>`
    )).join('');
    return moviesHTML;
}

JS登录和注册

登录

<body>
    <h2>用户登录</h2>
    <div>
        <input type="text" id="account">
        <input type="text" id="password">
        <button id="loginBtn">登录</button>
    </div>

    <script src="../utils/utils.js"></script>
    <script>
        var accountNode = document.getElementById('account');
        var passwordNode = document.getElementById('password');
        var loginBtn = document.getElementById('loginBtn');

        loginBtn.onclick = function () {
            var account = accountNode.value;
            var password = passwordNode.value;
            // 获取本地存储中已经注册好的用户数据
            var users = getStorage('users');
            // var users = JSON.parse(localStorage.users || '[]');

            var isLogin = users.some(item => item.account == account && item.password == password);
            if (isLogin) {
                alert("登录成功");
            } else {
                alert("登录失败");
            }

        }
    </script>
</body>

html密码框不显示,密码框中密码的显示与隐藏切换(JS)

目标:

点击小眼睛后,

1、密码框变成文本框

2、小眼睛图片由闭眼变成睁眼;

3、再次点击后,又变成闭眼+密码框

要想实现3,方法1:

则需要一个变量,来辅助判断当前input的属性,如果flag为0,则在点击后,将input的type属性改为text框,同时,再将flag值重新赋为1.此时,再次点击,又会进行判断,而这次的flag=1,所以,就直接将input的type属性改为了password

要想实现3,方法2:

直接对input的type属性进行判断,若为text,则改为password,若为password,则改为text。

注册

<body>
    <h2>用户注册</h2>
    <div>
        <input type="text" id="account">
        <input type="text" id="password">
        <button id="registerBtn">注册</button>
    </div>
    
    <script src="../utils/utils.js"></script>
    <script>
        var accountNode = document.getElementById('account');
        var passwordNode = document.getElementById('password');
        var registerBtn = document.getElementById('registerBtn');

        var users = getStorage('users');

        registerBtn.onclick = function () {
            var account = accountNode.value;
            var password = passwordNode.value;
            users.push({
                account, password
            });
            // localStorage.users = JSON.stringify(users);
            setStorage('users', users);
        }
    </script>
</body>

01 -《let 和 const》

ES6 中新增了两种创建变量的方式:let 和 const。

一、基础语法

let、const 创建变量的基础语法和 var 一致:

var name = '张三';
let age = "20";
const gender = "男";

二、区别

虽然 var、let、const 都可以用来创建变量,但是三者也有不一样的地方。

1、作用域

关键字作用域说明
var函数作用域通过函数来划分作用区域,函数内的是局部作用域,函数外的是全局作用域。
let、const块级作用域通过大括号来划分作用区域,大括号内的是局部作用域,大括号外的是全局作用域。

示例代码:

var a = 10;        // 全局变量 a
let b = 10;        // 全局变量 b
const c = 10;      // 全局变量 c
if (true) {
    var d = 20;    // 全局变量 d
    let e = 20;    // 局部变量 e
    const f = 20;  // 局部变量 f
}
function foo() {
    var g = 20;    // 局部变量 g
    var h = 20;    // 局部变量 h
    var i = 20;    // 局部变量 i
}

2、重复声明

关键字重复声明说明
var允许可以在同一作用域中创建多个同名的变量,但是后面的会覆盖前面的。
let、const不允许不可以在同一作用域中创建多个同名的变量

示例代码:

var a;            // 可以不给初始值
let b;             // 可以不给初始值
const c = 20;     // 必须给初始值

3、初始赋值

关键字初始赋值说明
var、let可以不赋值在创建变量时可以不给变量设置初始值
const必须赋值在创建变量时必须给变量设置初始值

示例代码:

var a;            // 可以不给初始值
let b;             // 可以不给初始值
const c = 20;     // 必须给初始值

4、修改赋值

关键字修改赋值说明
var、let可以修改变量的值变量创建成功后,后续可以重新修改变量的值
const不可以修改变量的值变量创建成功后,后续不可以修改变量的值(只修改值,不修改地址,const检测不到,不会报错,比如数组.push()。)

附:由于 const 创建的变量,其实是不可变的量,因此 const 创建的变量也叫做“常量”。

示例代码:

var a = 1;
a = 2;
let b = 1;
b = 2;
cnst c = 1;
c = 2;       // 报错

5、变量提升

关键字变量提升说明
var存在在代码执行前会将变量的 var 声明提升到当前作用域顶部。
let、const不存在不存在变量提升的特性

示例代码:

console.log(a);   // undefined
var a = 1; 
console.log(b);   // 报错
let b = 2;
console.log(c);   // 报错
const c = 3;

6、总结

我们将 var、let、const 三者之间的区别汇总到下表中:

区别varletconst
作用域函数作用域块级作用域块级作用域
重复声明可以不可以不可以
初始赋值可以不赋值可以不赋值必须赋值
修改赋值可以修改可以修改不可以修改
变量提升存在不存在不存在

引申:

在js中未声明的变量,在非严格模式下,会被隐式视为创建全局变量在严格模式下,这是一个错误.

 function show() {
            a = 10
            console.log(a); //10
        }
        show()
        console.log(a); //10

02 -《变量提升和函数提升》

一、JS 的预解析

浏览器之所以能够运行 JS 代码,是因为每一款浏览器中都有一个 JS 引擎,专门负责处理 JS 代码。而 JS 引擎在处理 JS 代码时,分为两个步骤:

  1. 预解析:在代码真正执行之前会先对代码做一些额外的处理;

  2. 执行代码:对处理后的代码,开始从上往下依次执行;

变量提升和函数提升,就是在预解析阶段对代码做的额外处理。

二、变量提升

变量提升,在代码预解析阶段,JS 引擎会找到当前作用域中所有通过 var 创建的变量,然后将这些变量的创建提升到当前作用域的顶部,并且将变量的初始值设置为 undefined。

例如以下代码:

console.log(a);//undefined
var a = 1;

经过预解析处理后,变成了以下形式:

var a = undefined;
console.log(a);
a = 1;

三、函数提升

函数提升,在代码预解析阶段,JS 引擎会找到当前作用域中所有的声明式函数,然后将这些函数整体全部提升到当前作用域的顶部。

例如以下代码:

foo();
function foo() {
    console.log(1)
}

经过预解析处理后,变成了以下形式:

function foo() {
    console.log(1)
}
foo();

四、总结

简单来说,我们只需要记住,var 创建的变量和声明式函数,都存在提升。var 提升的是创建的变量以及初始赋值 undefined,声明式函数提升的是函数整体。

五、暂时性死区(扩展)

严格意义上来说,let、const 创建的变量不应该说“不提升”,而是有一个新的特性:暂时性死区。

在变量的处理过程中,分为三个阶段:

  1. 创建变量:通过关键创建变量;

  2. 变量初始化:第一次给变量设置值;

  3. 变量赋值:从第二次开始修改变量的值;

暂时性死区,指的就是在预解析阶段,变量只有创建过程被提升了,初始化和赋值都没有提升,导致我们在使用变量时出现暂时性死区,报错提示“初始化之前不能访问变量”。

我们分别从下面三个例子中来理解这个概念。

1、var 声明的变量

我们来看看通过 var 声明的变量,整个“创建、初始化和赋值”过程。

假设有如下代码:

 console.log(a);
 var a = 1;

JS 引擎会有以下过程:

  1. 找到所有用 var 声明的变量,在环境中“创建”这些变量(即变量 a);

  2. 将这些变量“初始化”为 undefined;

  3. 开始执行代码,输出变量 a,得到 undefined;

  4. a = 1 将 a 变量“赋值”为 1;

也就是说,var 声明的变量会在代码执行之前就“创建变量,并将其初始化为 undefined”。这就解释了为什么在 var a = 1 之前 console.log(a) 会得到 undefined。

2、function 声明的函数

接下来看看 function 声明的函数,整个“创建、初始化和赋值”过程。

假设有如下代码:

 foo();
 function foo() {
     console.log(2);
 }

JS 引擎会有以下过程:

  1. 找到所有用 function 声明的变量,在环境中“创建”这些变量(即 foo)。

  2. 将这些变量“初始化”并“赋值”为 function () { console.log(2) }

  3. 开始执行代码 foo()

也就是说 function 声明会在代码执行之前就“创建、初始化并赋值”。

3、let 的提升

最后我们来看看 let 声明的变量,整个“创建、初始化和赋值”过程。

假设代码如下:

 let a = 1;
     a = 2;

JS 引擎会有以下过程:

  1. 找到所有用 let 声明的变量,在环境中“创建”这些变量;

  2. 开始执行代码(注意现在没有初始化);

  3. 执行 x = 1,将 x “初始化”为 1(这并不是一次赋值,如果代码是 let x,就将 x 初始化为 undefined);

  4. 执行 x = 2,对 x 进行“赋值”;

因此,如果在 let a 之前访问 a 会出现报错:

console.log(a); // Uncaught ReferenceError: a is not defined
 let a = 1;

这是因为,在执行 log 时 a 虽然已经创建了,但还没“初始化”,所以不能使用(也就是所谓的暂时性死区)。

同样的道理,我们再看以下代码:

 let a = "global";
 {
     console.log(a); // Uncaught ReferenceError: a is not defined
     let a = 1;
 }

我们发现还是会报同样的错。

原因有两个:

  1. 全局虽然创建了 a,且在输出前也完成了初始化;但是由于块级作用域中也创建了 a,虽然在输出前没有初始化,但是只要当前作用域已经创建了该变量,使用时就不会再访问其他作用域中的同名变量。

  2. 但是由于执行 log 时 a 虽然“创建”了但是没有“初始化”,所以不能使用(暂时性死区);

4、总结

通过以上三个例子,我们总结一下变量提升和函数提升:

  1. var 声明的变量,“创建”和“初始化”都会被提升;

  2. function 创建的函数,“创建”、“初始化”和“赋值”都会被提升;

  3. let 声明的变量,只有“创建”的过程会被提升,”初始化“和”赋值“都没有提升;

而对于我们的 JS 引擎来说,执行代码会遵循以下原则:

  1. 如果当前作用域中已经创建了对应的变量,在使用时就不会去其他作用域中查找;

  2. 变量如果只是创建完成但没有初始化的话,是无法使用的(使用时会报错);

03 -《内存空间》

在 JS 中,不管是基础数据还是引用数据,只要是数据,都会保存在浏览器的内存空间中。而不同类型的数据,在内存中保存到方式也是不一样的。

堆和栈

浏览器的内存空间,划分了两块区域,分别叫做“堆”和“栈”。

所有基础类型的数据,都是保存在“栈”中;所有引用类型的数据,都是保存在“堆”中。

例如:

var a = 1;
var b = 'hello';
var arr = [1, 2, 3];
var obj = { x: 1, y: 2 };
var newArr = arr;
var newObj = obj;

图例解析:

结论

  1. 所有基础类型的数据,数据值都是保存在栈里面的;

  2. 所有引用类型的数据,数据值都是保存在堆里面的;

  3. 所有引用类型的数据,都有一个“引用地址”,指向了数据值在堆里面的位置;

  4. 所有的引用地址,都是保存在栈里面的;

04 -《解构赋值》

解构赋值,通常都是用在数组或对象中。当我们需要从一组数据中获取到其中某几个数据时,就会考虑使用解构赋值。

一、基础语法

1、数组的解构赋值

例如我们有数组中有一组数字:

const arr = [10, 20, 30, 40];

现在,我们需要单独的拿到数组中的每一个值,并赋值给不同的变量。传统的处理方式如下:

const a = arr[0];
const b = arr[1];
const c = arr[2];
const d = arr[3];

而 ES6 中的解构赋值可以让这个过程变得更加简单:

const [a, b, c, d] = arr;

= 赋值符右边数组中的数据,会按照下标对应的位置,依次赋值给 = 赋值符左边的数组中的变量。

如果我们不需要解构全部的值,只需要拿到其中的某几个值,也可以:

const [a] = arr;
console.log(a);    // 1
const [, , , d] = arr;
console.log(d);    // 4

2、对象的解构赋值

例如我们有对象中有一组学生信息数据:

const student = { name: '张三', age: 20, gender: '男' };

现在,我们需要单独的拿到对象中的每一个值,并赋值给不同的变量。传统的处理方式如下:

const a = student.name;
const b = student.age;
const c = student.gender;

而 ES6 中的解构赋值可以让这个过程变得更加简单:

const { name: a, age: b, gender: c }  = student;

= 赋值符右边对象中的数据,会按照对应的键名,依次赋值给 = 赋值符左边的对象值中的变量。

如果我们不需要解构全部的值,只需要拿到其中的某几个值,也可以:

const { name: a }  = student;
console.log(a);    // 张三

上面的代码我们还可以继续优化,变量是可以我们自己任意命名的,因此,上面的代码我们也可以改写成:

const { name: name, age: age, gender: gender }  = student;

又因为 ES6 中,针对对象的键和值如果是同一个单词时,可以再简写一下,最终变成以下格式:

const { name, age, gender } = student;

二、解构赋值的默认值

解构赋值允许通过赋值符 = 给每一个变量设置默认值。这样,当某一个变量没有被赋值时,就可以使用自己的默认值。

1、数组解构赋值的默认值

示例代码:

 // 没有默认值
 var [a, b] = [];
 console.log( a );  // undefined
 console.log( b );  // undefined
 // 有默认值
 var [a = 1, b = 2] = [];
 console.log( a );  // 1
 console.log( b );  // 2
 var [a = 1, b = 2] = [3, 4];
 console.log( a );  // 3
 console.log( b );  // 4

说明:并不是每一个变量都必须给默认值,我们可以根据自己的需求来设置。

2、对象解构赋值的默认值

示例代码:

 // 没有默认值(不简写)
 var { name: name } = {};
 console.log( name ); // undefined
 // 没有默认值(简写)
 var { name } = {};
 console.log( name ); // undefined
 // 有默认值(不简写)
 var { name: name = "张三" } = {};
 console.log( name ); // 张三
 // 有默认值(简写)
 var { name = "张三" } = {};
 console.log( name ); // 张三
 var { name = "张三", age } = { name: "李四", age: 30 };
 console.log( name, age ); // 李四 30

三、函数参数中的解构赋值

因为函数的参数也可以是数组或对象,因此,数组对象的解构赋值也可以用在函数的参数中。

示例代码:

 function add([x, y]){
     console.log(x + y);  // 3
 }
 add([1, 2]);

上面代码中,在调用函数 add 时传递了一个数组作为实参,在接收时本来应该用一个形参来接收。但是,我们直接用数组将形参解构了,然后根据解构赋值的规则,用变量 xy 接收到数组中对应的数字 12

参数为对象时也是同样的道理:

 function add({ x, y }){
     console.log(x + y); // 3
 }
 add({ x: 1, y: 2 });

1、参数解构赋值的默认值

解构赋值的默认值在函数形参中同样适用。

参数为数组时:

 // x、y 没有接收到实际的值,就会使用默认的值
 function add([x = 1, y = 2]){
     console.log(x + y);  // 3
 }
 add([]);  
 // x、y 接收到了实际的值,就不会使用默认的值
 function add([x = 1, y = 2]){
     console.log(x + y);  // 30
 }
 add([10, 20]);

参数为对象时:

 // x、y 没有接收到实际的值,就会使用默认的值
 function add({ x = 1, y = 2 }){
     console.log(x + y); // 3
 }
 add({});   
 // x、y 接收到了实际的值,就不会使用默认的值
 function add({ x = 1, y = 2 }){
     console.log(x + y); // 30
 }
 add({x: 10, y: 20});

2、实参为空的解构赋值

如果我们对在形参中对一个数组或对象进行解构赋值,但是,在调用时,并没有传递任何参数。例如:

 function add([x = 1, y = 2]) {
     console.log(x + y);
 }
 add();
 // 或
 function add({x = 1, y = 2}) {
     console.log(x + y);
 }
 add();

这个时候,程序会报错。

原因在于我们调用函数时没有传实参,那么形参接收到的其实就是 undefined。而从语法上来说,undefined 是不能解构成一个数组或对象的,因此程序会报错。

所以,这里我们要考虑两种情况的默认值:

  1. 函数参数的默认值:当没有传递任何实参时,我们默认实参是一个 []{}

  2. 解构赋值的默认值:当数组或对象中没有值时,我们根据需求设置变量以及变量默认值;

示例代码如下:

 // 函数参数的默认值:设置形参 arr 的默认值是一个 []
 function add(arr = []) {
 }
 add();
 // 对形参 arr 进行解构
 function add([x, y] = []) {
 }
 add();
 // 最终方案:参数的默认值以及解构赋值的默认值
 function add([x = 1, y = 2] = []) {
     console.log(x + y);   // 3
 }
 add();

这样,即使我们调用函数时没有传参,函数也会默认使用空数组 [],然后再对 [] 进行解构赋值。解构赋值后 x 和 y 也没有接收到值,然后都使用自己的默认值 1 和 2。

同理,参数为对象时也可以这样处理:

 function add({ x = 1, y = 2 } = {}) {
     console.log(x + y);
 }
 add();

05 - 《扩展运算符》

扩展运算符,符号是 ...

扩展运算符的作用,就是通过 ...数据 的形式将一组数据全部展开。

一、数组的扩展

基础语法:

const arr = [10, 20, 30];
console.log(...arr);

应用场景

1、数组的复制

当我们希望从原数组身上复制一个新的数组出来,要求数据一模一样,但是地址不一样:

const arr = [10, 20, 30];
const newArr = [...arr];

2、数组的合并

当我们需要将多个数组合并成一个新数组,除了数组自带的 concat() 方法外,也可以使用扩展运算符:

const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const newArr = [...arr1, ...arr2];

3、数组项的新增

当我们需要在一个原数组的基础之上,往数组中新增一个或多个元素,同时还要得到的是一个新数组的地址:

const arr = [10, 20, 30];
const newArr = [...arr, 40, 50];

二、对象的扩展

应用场景

1、对象的复制

当我们希望从原对象身上复制一个新的对象出来,要求数据一模一样,但是地址不一样:

const obj = { a: 1, b: 2 };
const newObj = { ...obj };

2、对象的合并

当我们需要将多个对象合并成一个新对象,也可以使用扩展运算符:

const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };
const newObj = { ...obj1, ...obj2 };

3、对象属性的新增

当我们需要在一个原对象的基础之上,往对象中新增一个或多个属性,同时还要得到的是一个新对象的地址:

const obj = { a: 1, b: 2 };
const newObj = { ...obj, c: 3, d: 4, e: 5 };

06 -《Set 和 Map》

ES6 中新增了两种新的数据结构 —— Set 和 Map。

一、Set

ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值

1、基本用法

创建 Set

Set 本身是一个构造函数,调用构造函数用来生成 Set 数据结构。

 var s = new Set();
 console.log(s);

初始化

Set 函数可以接受一个数组(或类似数组的对象)作为参数,用来进行初始化。

 let s = new Set([1, 2, 3, 4, 4]);
 console.log(s);

通过上面代码运行结果我们可以看到,Set 成员当中如果存在重复的值,会被自动删除掉。

2、Set 实例的属性和方法

Set 结构的实例有以下属性和操作方法。

属性

  • size:返回 Set 实例的成员总数。

 let s = new Set([1, 2, 3])
 console.log(s.size);   // 3

方法

Set 实例的方法分为两大类:操作方法(用于数据操作)和遍历方法(用于遍历数据)。

操作方法

  • add(value):添加数据,并返回新的 Set 结构。

  • delete(value):删除数据,返回一个布尔值,表示是否删除成功。

  • has(value):查看是否存在某个数据,返回一个布尔值。

  • clear():清除所有数据,没有返回值。

 let set = new Set([1, 2, 3, 4, 4]); 
 // 添加数据 5
 let addSet = set.add(5);
 console.log(addSet);   // Set(5) {1, 2, 3, 4, 5}
 // 删除数据 4
 let delSet = set.delete(4);
 console.log(delSet);   // true
 // 查看是否存在数据 4
 let hasSet = set.has(4);
 console.log(hasSet);   // false
 // 清除所有数据
 set.clear();
 console.log(set);      // Set(0) {}

遍历方法

Set 提供了三个遍历器生成函数和一个遍历方法。

  • keys():返回一个键名的遍历器。

  • values():返回一个键值的遍历器。

  • entries():返回一个键值对的遍历器。

  • forEach():使用回调函数遍历每个成员。

let color = new Set(["red", "green", "blue"]);
 for(let item of color.keys()){
     console.log(item);
 }
 // red
 // green
 // blue
 for(let item of color.values()){
     console.log(item);
 }
 // red
 // green
 // blue
 for(let item of color.entries()){
     console.log(item);
 }
 // ["red", "red"]
 // ["green", "green"]
 // ["blue", "blue"]
 color.forEach((item) => {
     console.log(item)
 })
 // red
 // green
 // blue

说明:

上例代码中,entries()方法返回的遍历器,同时包括了键名和键值,所以每次输出一个数组,它的两个成员完全相等。

3、转换为数组

扩展运算符

由于扩展运算符...内部的原理也是使用的for-of循环,所以也可以用于操作 Set 结构。

例如将 Set 结构转换为数组结构:

 let color = new Set(["red", "green", "blue"]);
 let colorArr = [...color];

数组剔重

*扩展运算符和 Set 结构相结合,就可以去除数组的重复成员。

 let arr = [1, 1, 2, 2, 3, 3];
 let unique = [...new Set(arr)];     // [1, 2, 3]

二、Map

JavaScript 中的对象(Object),本质上是键值对的集合,但是只能用字符串来做键名。这给它的使用带来了很大的限制。

为了解决这个问题,ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。

也就是说,Object 结构提供了“字符串 - 值”的对应,Map 结构提供了“值 - 值”的对应,是一种更完善的 JSON 数据结构的实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。

1、基本用法

创建 Map

Map 本身是一个构造函数,调用构造函数用来生成 Map 数据结构。

 var m = new Map();
 console.log(m);

初始化

Map 函数也可以接受一个数组(或类似数组的对象)作为参数,用来进行初始化。但是跟 Set 不同的是,Map 中该数组中的成员是一对对表示键值对的数组。

 let m = new Map([["name", "zhangsan"], ["age", 20]]);
 console.log(m);

2、Map 实例的属性和方法

Map 结构的实例有以下属性和方法。

属性

  • size:返回 Map 结构的成员总数。

 let m = new Map([["name", "zhangsan"], ["age", 20]]);
 console.log( m.size );

方法

Map 实例的方法也分为了两大类:操作方法(用于数据操作)和遍历方法(用于遍历数据)。

操作方法

  • set(key, value):添加或修改数据。设置key所对应的键值,并返d回 Map 结构本身。

  • get(key):获取数据。读取key对应的键值,如果找不到key,返回undefined

  • has(key):查看是否存在某个数据,返回一个布尔值。

  • delete(key):删除数据。删除成功返回true

  • clear():清除所有数据,没有返回值。

 let map = new Map([["name", "zhangsan"], ["age", 20]]);
 // 设置 name 的值为 lisi
 map.set("name", "lisi");
 console.log( map );       // Map(2) {"name" => "lisi", "age" => 20}
 // 获取 name 对应的值
 let getMap = map.get("name");
 console.log( getMap );    // lisi
 // 查看是否存在 age
 let hasMap = map.has("age");
 console.log( hasMap );    // true
 // 删除 age 键值对
 let delMap = map.delete("age");
 console.log( delMap );    // true
 // 清空所有数据
 map.clear();
 console.log(map);         // Map(0) {}

遍历方法

Map 提供了三个遍历器生成函数和一个遍历方法。

  • keys():返回一个键名的遍历器。

  • values():返回一个键值的遍历器。

  • entries():返回一个键值对的遍历器。

  • forEach():使用回调函数遍历每个成员。

 let num = new Map([["one", 1], ["two", 2], ["three", 3]]);
 for(let key of num.keys()){
     console.log(key);
 }
 // one
 // two
 // three
 for(let value of num.values()){
     console.log(value);
 }
 // 1
 // 2
 // 3
 for(let item of num.entries()){
     console.log(item[0], item[1]);
 }
 // one 1
 // two 2
 // three 3
 // 将上面代码通过解构改成如下
 for(let [key, value] of num.entries()){
     console.log(key, value);
 }
 // one 1
 // two 2
 // three 3
 num.forEach((value, key) => {
     console.log(value, key)
 })
 // 1 one
 // 2 two
 // 3 three

三、与其他数据结构互换

1、Map 转为数组

Map 转为数组最方便的方法,就是使用扩展运算符...

 let myMap = new Map();
 myMap
     .set(true, "真")
     .set(false, "假");
 console.log(myMap);   // {true => "真", false => "假"}
 let newMap = [...myMap];
 console.log(newMap);  // [[true, "真"], [false, "假"]]

2、数组转为 Map

将数组传入 Map 构造函数中,就可以转为 Map。

 let arr = [[true, "真"], [false, "假"]];
 let map = new Map(arr);
 console.log(map);   // {true => "真", false => "假"}

3、Map 转为对象

如果 Map 所有的键都是字符串,它就可以转为对象。

 function strMapToObj(strMap){
     let obj = {};
     for(let [k, v] of strMap){
         obj[k] = v;
     }
     return obj;
 }
 let myMap = new Map().set("green","绿").set("red","红");
 console.log(myMap);                  // {"green" => "绿", "red" => "红"}
 console.log( strMapToObj(myMap) );   // { green: "绿", red: "红" }

4、对象转为 Map

 function objToStrMap(obj){
     let strMap = new Map();
     for(let item in obj){
         strMap.set( item, obj[item] )
     }
     return strMap;
 }
 let obj = { name: "zhangsan", age: 20 };
 console.log( objToStrMap(obj) );    
 // {"name" => "zhangsan", "age" => 20}

07 -《函数的其他形式》

一、匿名函数

匿名函数,就是指的没有函数名的函数。

const foo = function () {
}

二、立即执行函数

立即执行函数,简称为“IIFE”。指的就是函数在创建的同时立即就执行了。

(function () {
    console.log(1)
})();

三、回调函数

回调函数,指的是将一个函数 A 作为参数,传递给另一个函数 B,那么其中的函数 A,就称之为“回调函数”。

setTimeout(function () { }, 1000);

四、递归函数

当一个内部在调用自己,那么该函数就是一个“递归函数”。

function foo() {
    console.log(1);
    foo();
}
foo();

注意:在使用递归函数时,需要一些判断条件,来避免出现无限递归。

练习:将一个多维数组扁平化。

const arr = [1, 2, [3, 4], 5, [6, [7, 8]]];

08 -《执行上下文》

执行上下文,指的就是代码的运行环境。

一、执行上下文的分类

执行上下文分为三类:

  1. 全局上下文:在代码运行之前,会自动产生一个全局上下文;

  2. 局部上下文:在函数被调用时,会产生一个局部上下文;

  3. eval() 上下文:在 eval() 方法被调用时,会产生一个 eval() 上下文(仅作了解);

二、执行上下文栈

在代码运行过程中,每一次产生的执行上下文,都会保存到内存的栈空间中,这个保存的过程,我们叫做“压栈”。

当一个函数调用完成时,该函数对应的执行上下文,会从栈空间中销毁,这个销毁的过程,我们叫做“出栈”。

执行上下文栈,采用的压栈和出栈的顺序是:先进后出,后进先出。

对于执行上下文栈来说,只最上面的上下文是处于活动状态的,而只有活动状态的上下文,才能运行其内部的代码。

三、总结

  1. 任何一个程序,都会产生唯一的一个全局上下文;

  2. 任何函数,只要被调用一次,就会产生一个对应的局部上下文,调用多次就产生多个;

  3. 当一个函数调用完成时,对应的上下文会从栈内存中销毁;

  4. 只有处于栈内存顶部的上下文,是活动状态的,其内部的代码才能运行;

09 -《变量对象》

变量对象,就是用来保存当前上下文中所有的数据。

一、上下文和变量对象

执行上下文从创建到销毁可以分为三个阶段:

  1. 执行上下文创建:在执行上下文内部创建变量对象、建立作用域链、确定 this 指向;

  2. 代码运行阶段:当执行上下文创建完成后,开始运行内部代码;

  3. 执行上下文销毁:当代码运行完成后,执行上下文从栈中销毁;

从以上三个步骤中,可以分析出上下文和变量对象之间的关系:

  • 每一个执行上下文中都会有一个自己的变量对象;

  • 上下文在创建的同时,它的变量对象也就产生了;

二、变量对象的作用

变量对象在创建的时候,会依次完成以下三件事情:

  1. 找到当前上下文中所有的参数,将形参和实参以“键值对”的形式保存在变量对象中;

  2. 找到当前上下文中所有的声明式函数,将函数名和函数地址以“键值对”的形式保存在变量对象中;

  3. 找到当前上下文中所有通过 var 声明的变量,将变量名和 undefined 以“键值对”的形式保存在变量对象中;

以上三件事情完成后,变量对象就创建完成了。

说明:如果出现形参名、函数名、变量名重名的情况,正常情况下是变量对象中后面的会覆盖前面的。但是由于 JavaScript 中的函数是一等公民,所以如果变量没有的值的时候,是无法将同名函数给覆盖掉的。

10 -《作用域链》

作用域链

我们知道,当函数被调用激活时,会开始创建对应的执行上下文。在执行上下文生成的过程中,变量对象作用域链、以及this的值会分别被确定。在前面我们已经详细讲解了关于变量对象的分析,所以这里,我们接着来分析作用域链。

作用域链,实际上是由一系列的变量对象组成的。

每一个作用域链,它的前端始终是当前代码的执行行上下文的变量量对象,而它的末端也始终是全局上下文的变量对象。

案例分析

但看文字估计还是不太能理解到底什么是作用链,所以我们通过一个例子,结合代码来具体进行分析说明。

var food = "巧克力味的屎";
var eat = function(){
    console.log( "吃" + food );//函数在创建的时候,找父级
};
(function(){
    var food = "屎味的巧克力";
    eat();
})();

全局上下文在浏览器器打开的同时产生,eat 上下文和立即执行函数上下文都是在函数调用时才产生。上下文创建好的同时,在上下文的内部创建出了了变量量对象和作用域链。

作用域链中包含的每一个变量量对象,都应该是前一个变量量对象所在函数声明时所在的上下文内的变量量对象。

也就是说, eat 函数声明时所在的上下文是全局上下文,所以, eat 作用域链中的第二个变量量对象应该是全局上下文中的变量量对象。

这样,我们 eat 函数在查找 food 变量量时会沿着作用域链最终查找到全局变量对象中去,得到 food="巧克力味的屎" 的结果。

11 -《闭包》

闭包,实际上指的就是当一个函数执行完成后,由于其他地方需要继续使用该函数内部的数据,导致这个函数的变量对象在该销毁的时候没有销毁,因此,这个函数就变成了一个闭包函数。

一个函数如果要形成闭包,需要满足以下几个条件:

  1. 函数之间形成嵌套;

  2. 内部函数中在访问外部函数的变量对象;

  3. 内部函数在创建以外的其他区域被调用;

闭包的优点:

  1. 延长了变量的作用时间;

  2. 扩大了变量的作用范围;

  3. 避免全局变量名的污染(命名冲突);

闭包的缺点:

  1. 滥用闭包会造成内存的浪费;

例子:

 function foo() {
            let a = 1;
            function bar() {
                console.log(a);
            }
            return bar;
        }

        const bar = foo();
        // bar();

        function outer() {
            bar();
        }

12 -《面向对象的概念》

面向对象,实际上就是以对象为核心的一种编程方式。

一、面向过程

面向过程的编程方式,重点会放在项目的过程上。

例如:ATM 取款机的练习。我们分析它的步骤:

  1. 注册

  2. 登录

  3. 主菜单

  4. 存款

  5. 取款

当我们把过程分析好后,代码层面,我们会根据每一个步骤都封装成一个函数:

function register() { };
function login() {  };
function menu() { };
// ...

二、面向对象

面向对象的编程方式,重点会放在项目的对象上。

1、对象

我们可以将任何事物都看作是一个对象,每一个对象都有自己的属性和方法。例如:一个学生对象,他的姓名、年龄、性别等信息,就可以看作是该对象的属性。学生可以学习、走路、吃饭等能力,这些能录就可以看作是学生对象的方法。

例如:ATM 取款机的练习。我们分析它里面包含的对象:

  • ATM 取款机对象:提供了登录、注册、存款、取款、转账等方法;

  • 用户对象:提供了账号、密码、账户余额等属性;

模拟面向对象的代码:

const user = {
    account: '1',
    password: '1',
    money: 1000
}
const atm = {
    login: function () {
    },
    register() {
    },
    saveMoney() {
    }
}
atm.register();

面向过程和面向对象的对比

面向过程

●优点: 性能比面向对象高,适合跟硬件联系很紧密的东西,例如单片机就采用的面向过程编程。 ●缺点:没有面向对象易维护、易复用、易扩展

面向对象

●优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的面向对象系统,使系统更加灵活、更加易于维护 ●缺点:性能比面向过程低

用面向过程的方法写出来的程序是-份蛋炒饭,而用面向对象写出来的程序是-份盖浇饭。

13 -《创建类和对象》

一、类

类,我们可以理解为“种类”、“分类”。

类,就是用来管理所有同类型的对象。换句话说,我们可以通过一个类,产生很多个一样的对象。

所以,类和对象之间的关系:通过一个类可以产生多个相同的对象。

引申:

抽象了对象的公共部分,它泛指某一大类( class ) 对象特指某一个,通过类实例化-个具体的对象

面向对象的思维特点: 1.抽取(抽象)对象共用的属性和行为组织(封装)成一 个(模板) 2.对类进行实例化,获取类的对象

1、创建类

在 ES6 之前,JavaScript 中并没有类的概念,因此也没有创建类的代码。所以,在 ES6 之前,都是用函数来模拟类。

// 模拟一个“学生类”
function Student() {
}

说明:虽然首字母大写不是必须的,但是所有程序员之间,有一个约定俗成的习惯,就是将这种用来创建对象的函数叫做“构造函数”,实际上就是用来模拟“类”。

2、产生对象

直接通过 new 的方式去调用构造函数,就会得到一个对象。

例如,我们想要通过 Student 类得到学生对象:

const s1 = new Student();
const s2 = new Student();

通过 new 的方式去调用构造函数,得到的对象,通常叫做“实例对象”。

二、实例对象的属性和方法

1、实例对象的属性

在每一个构造函数中,都有一个 this 关键字,指向了 new 出来的实例对象。

function Student(name, age, gender) {
    this.name = name;
    this.age = age;
    this.gender = gender;
    this.major = "WEB前端";
}
const s1 = new Student('张三', 20, '男');
const s2 = new Student('李四', 18, '女');
console.log('s1', s1);
console.log('s2', s2)
;

2、实例对象的方法

所有实例对象的方法,都设置在构造函数的 prototype 属性上:

function Student(name, age, gender) {
    this.name = name;
    this.age = age;
    this.gender = gender;
    this.major = "WEB前端";
}
// 原型
Student.prototype.sayHello = function () {
    console.log('hello');
}
Student.prototype.introduce = function () {
    this.sayHello();
    console.log(`我叫${this.name},性别${this.gender},今年${this.age}岁。`);
}
const s1 = new Student('张三', 20, '男');
const s2 = new Student('李四', 18, '女');

三、ES6 中的类

ES6 中,新增了 class 用来创建类。

类constructor构造函数

constructor()方法是类的构造函数(默认方法) ,用于传递参数返回实例对象,通过new命令生成对象实例时,自动调用该方法。如果没有显示定义类内部会自动给我们创建一 个constructor()

class Student {
    constructor(name, age, gender) {
        // 属性
        this.name = name;
        this.age = age;
        this.gender = gender;
        this.major = "WEB前端";
    }
    // 方法
    sayHello() {
        console.log('hello');
    }
    introduce() {
        this.sayHello();
        console.log(`我叫${this.name},性别${this.gender},今年${this.age}岁。`);
    }
}
const s1 = new Student('张三', 20, '男');
const s2 = new Student('李四', 18, '女');
s1.introduce() ;
s2.introduce() ;

注意点:

1.在ES6中类没有变量提升,所以必须先定义类,才能通过类实例化对象 2.类里面的共有属性和方法一定要加this使用. 3.类里面的this指向问题.

  1. constructor 里面的this指向实例对象,方法里面的this指向这个方法的调用者

14 -《原型和原型链》

一、原型

1、prototype

JavaScript 中每一个函数身上都有一个 prototype 的属性,称为“原型”。

function Person() { }
console.log(Person.prototype);

2、proto

JavaScript 中每一个对象身上都有一个 __proto__ 的属性,称为“隐式原型”。

function Person() {
    
}
const p = new Person();
console.log(p.__proto__);

3、原型和隐式原型

每一个对象的隐式原型,都指向创建该对象的函数的原型。

二、原型链

由对象的 __proto__ 和创建该对象的函数的 prototype 组成的链条,称为“原型链”。

原型链的作用,当我们访问一个的实例对象身上不存在的属性或方法时,解析器就会沿着原型链,到对应的函数的原型身上去进行查找。

示例代码:

function Person() {
    this.name = "张三";
}
Person.prototype.sayHello = function() {
    console.log('hello');
}
const p = new Person();
console.log(p.name);
p.sayHello();

图示:

15 -《this 指向汇总》

  • 全局的 this:window 对象

  • 普通函数中 this:window 对象,因为时window调用的函数

  • 回调函数中的 this:window 对象

  • 对象方法中的 this:调用该方法的对象

  • 事件处理函数中的 this:绑定事件的元素节点

  • 面向对象中的 this:new 出来的实例对象

  • 箭头函数中的 this:所在上下文中(父级)的 this

1、指向 window 对象

// 全局
console.log(this);
// 普通函数
function foo() {
    console.log(this);
}
foo();
// 回调函数
setTimeout(function () {
    console.log(this);
}, 1000)

2、指向调用方法的对象

// 对象的方法
const student = {
    type: '学生',
    show: function () {
        console.log(this);
    }
}
const teacher = {
    type: '老师'
}
teacher.show = student.show;
student.show();    // student
teacher.show();    // teacher

3、指向绑定事件的元素

const box = document.getElementById('box');
box.onclick = function (event) {
    // console.log(event.target);
    console.log(this);  // box 节点
}

4、指向 new 出来的实例对象

function Person() {
    console.log(this);
}
Person.prototype.sayHello = function () {
    console.log(this);
}
new Person();
class Animal {
    constructor() {
        console.log(this);
    }
    sayHello() {
        console.log(this);
    }
}
new Animal();

5、箭头函数的 this

箭头函数中没有 this,因此,如果我们要在箭头函数中使用 this,实际上使用是箭头函数所在上下文(父级)中的 this。

上下文的划分,是根据函数来的。函数内的就是局部上下文,函数外的就是全局上下文。

const student = {
    type: '学生',
    show: () => {
        console.log(this);
        var foo = () => {
            console.log(this);
        }
        foo();
    }
}
student.show();

二、改变 this 指向

JS 中提供了三种方法来改变 this 的指向:

  • call:改变 this 指向的同时立即执行函数;call 的参数依次传递即可;

  • apply:改变 this 指向的同时立即执行函数;apply 的参数需要以数组的形式传递;

  • bind:改变 this 指向后返回一个新的函数(不会立即执行该函数);bind 的参数后续调用时依次传递即可;

const student = {
    type: '学生',
    show: function (a, b) {
        console.log(this);
        console.log(a, b);
    }
}
const teacher = {
    type: '老师'
}
// 正常调用
student.show(1, 2);
// call()
student.show.call(teacher, 1, 2);
// apply()
student.show.apply(teacher, [1, 2]);
// bind()
// const show = student.show.bind(teacher);
// show();
student.show.bind(teacher)(1, 2);

在实际开发中,大部分时候,如果this不是我们想要的指向,通常我们来说我们都希望它可以指向父级。这种情况下,我们优先考虑使用箭头函数来改变this的指向。

16 -《继承》

面向对象有大三特性:

  1. 封装:将属性和方法添加到类中,这个过程就可以叫做面向对象的封装;

  2. 继承:子类可以拥有父类身上所有的属性和方法,同时子类还可以拥有自己的属性和方法;

  3. 多态:一个实例对象在不同情形下可以有多种状态;

一、ES5 的继承

// 父类
function Person(name, age) {
    this.name = name;
    this.age = age;
}
Person.prototype.sayHello = function () {
    console.log('hello');
}
// 子类
function Student(name, age) {
    // 父类属性的继承
    Person.call(this, name, age);
}
// 父类方法的继承
Student.prototype = new Person();
const child = new Student("张三", 20);
console.log(child);
child.sayHello();

二、ES6 的继承

// 父类
class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    sayHello() {
        console.log('hello');
    }
}
// 子类
class Student extends Person {
    constructor(name, age) {
        super(name, age);
    }
}
const child = new Student('张三', 20);
console.log(child);
child.sayHello();

扩展:super

// 利用super调用父级的consructor
class F{
            constructor(x,y){
                this.x = x
                this.y =y
            }
            sum (){
                console.log(this.x +this.y);
            }
        }
        class S extends F{
            constructor(x,y){
                super(x,y)
            }
        }
        var son = new S(1,2)
        var son1 = new S(11,21)
        son.sum() //3
        son1.sum() //32

// 利用super调用父级sum方法
class Father{
            constructor(x,y){
                this.x = x
                this.y = y
            }
            sum (){
                console.log(this.x + this.y);
            }
        }
        class Son extends Father{
            constructor(x,y){
                //  super必须放在最前面
                super(x,y)
                this.x =x
                this.y = y
            }
            deCreate(){
                console.log(this.x -this.y);
            }
        }
        var son = new Son(5,2)
        son.deCreate()
	// 因为Son通过extends和super继承了Father的sum方法,所以可以调用Father的sum
        son.sum()

17 -《定义对象属性》

除了传统的通过点运算符 . 定义对象属性外,JS 中还提供了一个 Object.defineProperty() 方法来定义对象的属性。

一、基础语法

Object.defineProperty(对象, '键名', {
    // ... 其他配置
})

二、控制数据权限

传统的对象的属性,都可以任意进行修改、删除、遍历的操作。而通过 Object.defineProperty() 定义的属性,我们可以自己来控制数据的操作权限。

例如,我们给 person 对象添加一个 name 属性:

const person = { age: 22 };
Object.defineProperty(person, 'name', {
    value: '李四',
});

通过以上方式添加的 name 属性,默认情况下,不能删除、不能修改、不能遍历。

但是我们可以再设置以下三个属性,来更改 name 的操作权限:

const person = { age: 22 };
Object.defineProperty(person, 'name', {
    value: '李四',
    configurable: true,  // 可以删除
    writable: true,  // 可以修改
    enumerable: true,  // 可以遍历
});

三、检测数据操作

Object.defineProperty() 还可以给对象的属性添加 get 和 set 方法:

const person = { age: 22 }
let age = person.age;
Object.defineProperty(person, 'age', {
    // 只要访问 person.name,就会自动触发 get 方法
    get() {
        return age;
    },
    // 只要修改 person.name,就会自动触发 set 方法
    set(value) {
        // console.log('value', value);
        age = value;
    }
});
// console.log(person.age);  // 22
person.age = 30;
console.log(person.age); // 30

18 -《深拷贝和浅拷贝》

深拷贝和浅拷贝,都只针对引用类型的数据。

  • 浅拷贝:当一个变量的数据拷贝到另一个变量身上后,只要拷贝后的数据中有任意一条数据的地址没有发生改变,都属于“浅拷贝”;

  • 深拷贝:当一个变量的数据拷贝到另一个变量身上后,所有数据的地址都发生了改变,就属于“深拷贝”;

一、浅拷贝

1、扩展运算符

const student = { name: '张三', age: { num: 20 } };
// 浅拷贝
const newStudent = {...student};

2、Object.assign()

const student = { name: '张三', age: { num: 20 } };
// 浅拷贝
const newStudent = Object.assign({}, student);
// student.name = '李四';
student.age.num = 30;
console.log(newStudent);

二、深拷贝(面试多)

1、JSON 对象的方法

const student = { name: '张三', age: { num: 20 } };
// 深拷贝
const newStudent = JSON.parse(JSON.stringify(student));
student.age.num.a = 30;
console.log(newStudent);

2、手写函数实现深拷贝

const student = { name: '张三', age: { num: 20 } };
function deepClone(data) {
    if (typeof data === 'object') {
        const newData = {};
        for (const key in data) {
            newData[key] = deepClone(data[key]);
        }
        return newData;
    }
    return data;
}
const newStudent = deepClone(student);

3.借助第三方JS库:jQuery、lodash

lodash深克隆_.cloneDeep(value)

这个方法类似_.clone,除了它会递归拷贝 value。(注:也叫深拷贝)。

参数

  1. value (*): 要深拷贝的值。

返回

(*): 返回拷贝后的值。

例子

var objects = [{ 'a': 1 }, { 'b': 2 }];
 
var deep = _.cloneDeep(objects);
console.log(deep[0] === objects[0]);
// => false

扩展lodash常用api:

一、数组:

1._.chunk(array, [size=1])

将数组(array)拆分成多个 size 长度的区块,并将这些区块组成一个新数组。 如果array 无法被分割成全部等长的区块,那么最后剩余的元素将组成一个区块。

参数:

  1. array (Array): 需要处理的数组

  2. [size=1] (number): 每个数组区块的长度

_.chunk(['a', 'b', 'c', 'd'], 2);
// => [['a', 'b'], ['c', 'd']]
 
_.chunk(['a', 'b', 'c', 'd'], 3);
// => [['a', 'b', 'c'], ['d']]

2._.compact(array)

创建一个新数组,包含原数组中所有的非假值元素。例如false, null,0, "", undefined, 和 NaN 都是被认为是“假值”。

参数:

  1. array (Array): 待处理的数组

_.compact([0, 1, false, 2, '', 3]);
// => [1, 2, 3]

3._.concat(array, [values])

创建一个新数组,将array与任何数组 或 值连接在一起。

参数

  1. array (Array): 被连接的数组。

  2. [values] (...*): 连接的值。

var array = [1];
var other = _.concat(array, 2, [3], [[4]]);
 
console.log(other);
// => [1, 2, 3, [4]]
 
console.log(array);
// => [1]

4._.difference(array, [values])

创建一个具有唯一array值的数组,每个值不包含在其他给定的数组中。(注:即创建一个新数组,这个数组中的值,为第一个数字(array 参数)排除了给定数组中的值。)该方法使用SameValueZero做相等比较。结果值的顺序是由第一个数组中的顺序确定。

参数

  1. array (Array): 要检查的数组。

  2. [values] (...Array): 排除的值。

_.difference([3, 2, 1], [4, 2]);
// => [3, 1]

5._.indexOf(array, value, [fromIndex=0])

使用SameValueZero 等值比较,返回首次 value 在数组array中被找到的 索引值, 如果 fromIndex 为负值,将从数组array尾端索引进行匹配。

6._.join(array, [separator=','])

array 中的所有元素转换为由 separator 分隔的字符串。

7._.last(array)

获取array中的最后一个元素。

8._.remove(array, [predicate=_.identity])

移除数组中predicate(断言)返回为真值的所有元素,并返回移除元素组成的数组。predicate(断言) 会传入3个参数: (value, index, array)

9._.slice(array, [start=0], [end=array.length])

裁剪数组array,从 start 位置开始到end结束,但不包括 end 本身的位置。

10._.uniq(array)

创建一个去重后的array数组副本。使用了SameValueZero 做等值比较。只有第一次出现的元素才会被保留。

...

二、对象

1._.assign(object, [sources])

分配来源对象的可枚举属性到目标对象上。 来源对象的应用规则是从左到右,随后的下一个对象的属性会覆盖上一个对象的属性。

2._.findKey(object, [predicate=_.identity])

这个方法类似_.find 。 除了它返回最先被 predicate 判断为真值的元素 key,而不是元素本身。

...

19 -《同步和异步》

一、同步和异步的概念

在代码中,同步,指的是在同一个时间段内只能做一件事情,如果上一件事情没有做完,下一件事情只能处于等待状态。异步,指的是在同一个时间段内可以同时处理多件事情,如果上一件事情没有做完,不会影响到下一件事情的处理。

同步和异步的概念,对应到代码中来,同步代码必须从上往下依次执行,上一段代码没有执行完,下一段代码就只能等待。异步代码,可以同时执行多段异步代码,上一段异步代码没有执行完,并不会影响到后面代码的执行。

二、JavaScript 中的同步和异步

JavaScript 是一门单线程语言。单线程,就意味着 JavaScript 永远都不可能在同一个时间段内同时执行多段代码。

因此,JavaScript 中的异步,跟传统的异步有一些区别:

JS 引擎会按照代码的书写顺序,从上往下依次执行所有的同步代码,一旦在执行过程中遇到异步代码,会将异步代码添加到任务队列中进行等待(会先启动异步代码,但是会将异步代码的回调函数添加到任务队列中进行等待)。直到所有的同步代码执行完成后,会将任务队列中出结果的异步回调重新放到主线程中来继续执行。

20 -《AJAX》

AJAX 的全称是“Asynchronous JavaScript and XML”,翻译过来就是“异步的 JS 和 XML”。

  • Asynchronous:异步的,表示 AJAX 是一个异步代码,不会阻塞后面代码的执行;

  • JavaScript:使用 JavaScript 来实现前端(浏览器)和后端(服务器)的通信;

  • XML:XML 是一种数据格式,但是现在慢慢被 JSON 格式的数据替代了;

AJAX 的核心就是:

  • 实现前端和后端的异步通信;

  • 实现前端页面的局部刷新;

原生 AJAX 的使用步骤

// 第一步:创建核心对象
const xhr = new XMLHttpRequest();
// 第二步:建立前后端的连接
xhr.open('GET', 'http://nocat.life:3000/students/getStudents');
// 第三步:发送请求
xhr.send();
// 第四步:处理后端返回的结果
// onreadystatechange:当 readystate 的值发生改变时会触发该事件
xhr.onreadystatechange = function () {
    // readyState == 4 表示后端处理完成了
    // status == 200 表示后端处理成功了
    if (xhr.readyState == 4 && xhr.status == 200) {
        // responseText 接收到的就是后端传输给前端的数据
        console.log(JSON.parse(xhr.responseText));
    }
}

jQuery的AJAX

$.ajax({
    type: 'GET',                                         // 请求类型
    url: 'http://nocat.life:3000/students/getStudents',  // 请求资源的路径
    // data: {},                                         // 前端发送给后端的数据
    success(res) {                                       // 请求成功时执行的回调函数
        console.log(res);                                // 后端传输给前端的数据
    }
})

21 -《Promise》**

Promise 是 ES6 中新增的一种异步代码解决方案。

一、回调地狱

由于一些业务需求,导致我们的 AJAX 等异步代码需要嵌套编写,这样就会行为回调函数之间的不断嵌套,从而出现回调地狱的情况。

因此,ES6 中新增了 Promise,就是为了解决回调地狱的问题。

二、基础语法

const p = new Promise((resolve, reject) => {
    // 异步代码成功 
    // resolve();
    // 异步代码失败
    // reject();
});

1、Promise 的状态

Promise 对象有三种状态,它们分别是:

  • pending: 等待中,或者进行中,表示还没有得到请求结果;

  • fulfilled: 已成功,表示得到了我们想要的结果,可以继续往下执行;

  • rejected: 已失败,虽然也已经完成,但是由于结果不是我们想要的,因此拒绝继续执行;

当 Promise 开始执行时,状态默认为“pending”,例如:

 const p = new Promise((resolve, reject) => {
 })
 console.log(p);   // Promise {<pending>}

当 resolve 方法被调用时,状态从“pending”变成“fulfilled”,例如:

 const p = new Promise((resolve, reject) => {
     resolve();
 })
 console.log(p);   // Promise {<fulfilled>: undefined}

当 reject 方法被调用时,状态从“pending”变成“rejected”,同时抛出报错。例如:

 const p = new Promise((resolve, reject) => {
     reject();
 })
 console.log(p);   // Promise {<rejected>: undefined}

注意:Promise 的状态只能从 pending 变成 fulfilled,或者从 pending 变成 rejected 这两种情况。

2、Promise 的 then 方法

每一个 Promise 实例对象身上都有一个 then 方法,该方法带有两个参数,分别是:

  1. resolve 调用时触发的回调函数

  2. reject 调用时触发的回调函数

示例代码如下:

 const p = new Promise((resolve, reject) => {
     resolve();
     // reject();
 })
 p.then(() => {
     console.log('成功时触发');
 }, () => {
     console.log('失败时触发');
 })

3、Promise 中的数据传递

resolve 方法和 reject 方法在触发对应的回调函数的同时,还可以传递数据到回调函数中。

示例代码如下:

 const p = new Promise((resolve, reject) => {
     resolve(1);
     // reject(2);
 })
 p.then((data) => {
     console.log('成功时触发', data);
 }, (error) => {
     console.log('失败时触发', error);
 })

三、Promise 的其他方法

1、catch

Promise 的 catch 方法,实际上就是 then 方法中第二个回调函数的另一种写法:

 const p = new Promise((resolve, reject) => {
     resolve(1);
     // reject(2);
 })
 p.then((data) => {
     console.log('成功时触发', data);
 }).catch(() => {
     console.log('失败时触发', error);
 })

2、all

Promise.all() 可以接收一个数组作为参数,数组中保存的是 Promise 的实例对象。

const p1 = new Promise((resolve, reject) => {
    resolve(1);
})
const p2 = new Promise((resolve, reject) => {
    resolve(2);
})
Promise.all([p1, p2]).then((data) => {
    console.log(data);
})

以上代码表示,只有当 p1 和 p2 两个实例对象的状态都变为”成功”,才会执行 then 方法中的第一个回调函数。

示例代码:

const a = new Promise((resolve, reject) => {
    $.ajax({
        url: 'http://nocat.life:3000/test/a',
        success(res) {
            resolve(res);
        }
    })
})
const b = new Promise((resolve, reject) => {
    $.ajax({
        url: 'http://nocat.life:3000/test/b',
        success(res) {
            resolve(res);
        }
    })
})
Promise.all([a, b]).then((res) => {
    console.log(res);
})

3、race

Promise.race() 可以接收一个数组作为参数,数组中保存的是 Promise 的实例对象。

const p1 = new Promise((resolve, reject) => {
    resolve(1);
})
const p2 = new Promise((resolve, reject) => {
    resolve(2);
})
Promise.race([p1, p2]).then((data) => {
    console.log(data);
})

以上代码表示,只要 p1 和 p2 中任意一个实例对象的状态变为了”成功”,就会执行 then 方法中的第一个回调函数。

22 -《async 和 await》*

async 和 await,是 ES8 中提供的异步的最终解决方案。

一、async

async 可以用来定义一个异步函数,async 函数本身并不是一个异步代码,而是这个函数内部可以用来处理异步代码。

基础语法:

async function foo() {
}
foo();
const bar = async function() {
}
bar();

async 函数和普通函数的区别:

  1. async 函数的返回值永远都是一个 Promise 对象;

  2. async 函数内部可以使用 await;

二、await

await 用来等待一个异步处理结果,通常我们会用 await 来接收一个 Promise 对象中 resolve 的结果。

基础语法:

const res = await new Promise((resolve) => {
    resolve(res);
});
console.log(res);

await 的特点:

  • await 只能在 async 函数中使用

  • await 会阻塞当前 async 函数中后续代码的执行;

因此,上面的代码,完整的写法应该如下:

async function main() {
    const res = await new Promise((resolve) => {
        resolve(res);
    });
    console.log(res);
}
main();

三、代码案例

function getTestA() {
    return new Promise((resolve) => {
        $.ajax({
            url: 'http://nocat.life:3000/test/a',
            success(res) {
                resolve(res);
            }
        })
    })
}
function getTestB() {
    return new Promise((resolve) => {
        $.ajax({
            url: 'http://nocat.life:3000/test/b',
            success(res) {
                resolve(res);
            }
        })
    })
}
function getTestC() {
    return new Promise((resolve) => {
        $.ajax({
            url: 'http://nocat.life:3000/test/c',
            success(res) {
                resolve(res);
            }
        })
    })
}
// 返回值是一个 Promise 对象
async function main() {
    const resA = await getTestA();
    console.log(resA);
    const resB = await getTestB();
    console.log(resB);
    const resC = await getTestC();
    console.log(resC);
}
main();
	//学生数据渲染

 <body>
    <table>
        <thead>
            <tr>
                <th>学生姓名</th>
                <th>学生年龄</th>
                <th>学生性别</th>
            </tr>
        </thead>
        <tbody id="studentsList"></tbody>
    </table>

    <select id="students"></select>

    <script src="../utils/jquery-3.6.0.min.js"></script>
    <script>
        // 获取学生数据
        function getStudents() {
            return new Promise((resolve) => {
                $.ajax({
                    url: 'http://nocat.life:3000/students/getStudents',
                    type: 'GET',
                    success(res) {
                        if (res.code) {
                            resolve(res.data.rows);
                        }
                    }
                })
            })
        }

        // 将学生数据渲染成 table
        function tableRender(studentsData) {
            const studentsHTML = studentsData.map(item => {
                return (
                    `<tr>
                        <td>${item.name}</td>    
                        <td>${item.age}</td>    
                        <td>${item.gender}</td>    
                    </tr>`
                )
            }).join('');
            $('#studentsList').html(studentsHTML)
        }

        // 将数据渲染成 select
        function selectRender(studentsData) {
            const studentsHTML = studentsData.map(item => `<option value="">${item.name}</option>`).join('');
            $('#students').html(studentsHTML);
        }


        async function main() {
            const studentsData = await getStudents();
            tableRender(studentsData);
            selectRender(studentsData)
        }
        main();
    </script>
</body>

复习

一、ES6

1、let、const

let a = 1;
const arr = [1, 2, 3];
arr.push(4);   		  // 正确
arr = [1, 2, 3, 4];   // 报错

注意:const 创建的变量如果值是引用类型的数据,只要数据的地址没有发生改变,const 都不会报错。

2、模板字符串

const name = "张三";
const str = `你好,${name}`;

说明:在 ${} 中可以设置任意 JS 表达式。

如何去分析一段 JS 代码是不是 JS 表达式,就看这段代码执行完成后,会不会得到一个最终的值。

// 不可以
if(true) {
    console.log('hello')
}
const arr = [1, 2, 3];

// 可以
arr.map(function(item, index) {
    return item * 2;
}).join("");
// 可以
true ? 'hello' : 'world';

3、symbol(了解)

symbol 可以用来创建一个唯一值。

const s1 = Symbol();
const s2 = Symbol();
console.log(s1 === s2);  // false

通常会用 symbol 值来作为对象的键。

4、函数相关

1)箭头函数

基础语法:

const foo = function() {
    
}

const foo = () => {
    
}

arr.map(() => {
    
})

setTimeout(() => {}, 1000)

简写:

箭头函数的简写分为两种情况:

  • 当只有一个形参时,参数的小括号可以省略不写;

  • 当函数体内,有且仅有一行 return 语句时,可以将 return 和大括号一起省略;

// 省略小括号
const foo = x => {
    console.log(x);
}
// 省略 return 和 {}
const foo = (x, y) => {
    return x + y;
}
// 简写
const foo = (x, y) => x + y;


// 返回值是一个对象
const foo = (x, y) => {
    return { name: '张三' };
}
// 简写
const foo = (x, y) => ({ name: '张三' });


// 返回值内容需要换行
const foo = () => {
    return (
    	`<tr>
            <td></td>
        </tr>`
    )
}
// 简写
const foo = () => (
    `<tr>
        <td></td>
     </tr>`
)

箭头函数中的 this:

箭头函数中没有 this,如果我们需要在箭头函数中使用 this,实际上使用的是该箭头函数所在父级中的 this。

因此,如果当我们看到一个代码是在箭头函数中使用 this 时,需要分析两件事:

  1. 该箭头函数所在的父级是谁(箭头函数在哪个函数中创建的,谁就是它的父级);

  2. 父级的 this 指向谁;

2)函数形参的默认值

function foo(x = 1) {
    console.log(x);  // 1
}
foo();

3)不定参数

function foo(a, ...b) {
    console.log(a);   // 1
    console.log(b);   // [2, 3, 4]
}
foo(1, 2, 3, 4)

5、解构赋值

数组的解构赋值:

const arr = [1, 2, 3];
const [a, b, c] = arr;
console.log(a, b, c);

对象的解构赋值:

const obj = { a: 1, b: 2, c: 3 };
const { a: a, b: b } = obj;
// 简写
const { a, b } = obj;

6、扩展运算符

扩展运算符,用来将一条数据展开,我们通常会用于:

引用类型数据的复制,同时复制后创建一个新的地址:

const arr = [1, 2, 3];
const newArr = [...arr];

想要给引用类型的数据中添加内容,同时添加完后创建一个新的地址:

let arr = [1, 2, 3];
arr = [...arr, 4];

let obj = { a: 1, b: 2 };
obj = {
    ...obj,
    c: 3
}

7、class 类

创建类:

class Person {
    constructor(name) {
        this.name = name;
    }
    sayHello() {
        console.log('hello')
    }
}

调用类:

const zhangsan = new Person('张三');

说明:类内部的 this,指向的是在调用时通过 new 得到的实例对象(上例中的 zhangsan)。

子类继承父类:

class Father {
    // ...
}

class Child extends Father {
    constructor(name) {
        super(name);
    }
}

8、对象的简写

ES6 中针对对象的属性,有两种简写形式:

  1. 当值是一个变量,且键和值是同一个单词时,可以省略键和冒号 键:

  2. 当值是一个函数时,可以省略冒号和 function : function

const name = '张三';
const age = 20;
const student = {
    name: name,
    age: age
}
// 简写
const student = {
    name, 
    age
}


const student = {
    say: function() {
        console.log('hello')
    }
}
// 简写
const student = {
    say() {
        console.log('hello')
    }
}

9、前端模块化

1)暴露

暴露一条:

export default 一条数据;

暴露多条:

export 数据;
export 数据;
// ...

2)引入

引入 export default

import 任意变量名 from '路径';

引入 export

import { 对应的变量名一, 对应的变量名二 } from '路径';

10、Promise

new Promise((resolve, reject) => {
    // 异步代码
    resolve(数据);
}).then((数据) => {
    
})

11、async await

1)async

async 用来定义一个异步函数:

async function foo() {
    return 1;
}

异步函数和普通函数的区别:

  1. 异步函数的返回值永远都是 Promise 对象(如果想要获取到异步函数真正的返回值,需要通过 await 等待 Promise 对象);

  2. 异步函数内部可以使用 await;

2)await

await 用来等待一个 Promise 对象中 resolve 的结果。

async function foo() {
    const result = await new Promise((resolve) => {
        resolve('hello')
    });
    console.log(result);  // hello
}

12、Set、Map(了解)

Set 实现数组的去重:

const arr = [1, 2, 3, 1, 3, 4];

const newArr = [...new Set(arr)];

Set 类似数组,区别在于 Set 中的值不能重复;

Map 类似对象,区别在于 Map 的键可以是任意类型;

二、原理

1、变量提升和函数提升

在代码执行之前,浏览器有一个“预解析”的过程。

在这个过程中,会找到当前作用域中所有通过 var 声明的变量,然后将变量的声明提升到当前作用域头部,并赋值为 undefined。同时,还会找到所有的声明式函数,然后将整个函数声明提升到当前作用域头部。

console.log(a);
console.lg(foo);
foo();

var a = 1;
function foo() {
    console.log('foo');
}

以上代码经过预解析后,可以看作是变成了以下形式:

var a = undefined;
function foo() {
    console.log('foo');
}
foo();

console.log(a);
console.log(foo);

a = 1;

2、基本类型和引用类型

基本类型的数据值是保存在栈里面,引用类型的数据值是保存在堆里面。但是,引用类型数据的“引用地址”会保存在栈里面。

所以,在对基本类型和引用类型的数据进行赋值的时候,基本类型是“值传递”,引用类型是“址传递”。

3、执行上下文

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

全局上下文是在浏览器打开页面时产生,而局部函数上下文是在函数调用时产生。

每调用一次函数,都会产生一个对应的局部上下文。

4、变量对象

在创建执行上下文的同时,在上下文内部会产生变量对象。

变量对象在创建时,会做三件事情:

  1. 找到当前上下文中所有的参数并保存;

  2. 找到当前上下文中所有的声明式函数并保存;

  3. 找到当前上下文中所有通过 var 声明的变量并保存同时赋值为 undefined;

5、作用域链

作用域链的作用,可以理解为:当在自己的作用域找访问一个不存在的数据时,就可以沿着作用域链到父级的作用域中去查找。

实际上,作用域链真正底层的原理,应该理解为:所有的数据都是保存在每一个执行上下文的变量对象中,因此,当在自己的变量对象中访问一个不存在的数据时,就可以沿着作用域链到父级的变量对象中去查找。

6、作用域

作用域,指的是变量或函数的作用范围。

作用域也分为了全局作用域和局部作用域,但是,局部作用域是在函数创建时就产生了。

function foo() {
    var a = 1;
}
console.log(a);

7、闭包

概念:当一个函数的变量对象,在函数执行完成后,本来该销毁时,由于其他地方还需要访问该变量对象的数据,导致这个变量对象在应该销毁的时候没有销毁,从而形成了闭包。

从代码层面上看,闭包形成的条件:

  1. 两个函数之间形成嵌套;

  2. 内部函数在访问外部函数中的数据;

  3. 内部函数在创建该函数以外的其他地方被调用;

闭包主要作用是延长变量的作用时间,避免全局变量的命名冲突,但是滥用闭包,会造成内存的浪费。

说说你对闭包的理解?闭包使用场景

闭包是指有权访问另⼀个函数作⽤域中变量的函数。(创建闭包的最常⻅的⽅式就是在⼀个函数内创建另⼀个函数,创建的函数可以访问到当前函数的局部变量。 )

闭包有两个常⽤的⽤途;

● 闭包的第⼀个⽤途是使我们在函数外部能够访问到函数内部的变量。通过使⽤闭包,可以通过在外部调⽤闭包函数,从⽽在外部访问到函数内部的变量,可以使⽤这种⽅法来创建私有变量。

● 闭包延伸了变量的作用范围。

闭包的应⽤:

1、模仿块级作⽤域。2、保存外部函数的变量。3、封装私有变量

⽐如,函数 A 内部有⼀个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包。

//闭包应用-3秒钟之后,打印所有1i元素的内容
var lis = document . querySelector( ' . nav' ) . querySelectorAll( 'li' );
for (var i = 0; i < lis.length; i++) {
    (function(i) {           //立即执行函数
        setTimeout (function() {
            console . log(lis[i]. innerHTML);
            }, 3000)
        })(i);
    }

8、原型和原型链

每一个对象,都有一个 __proto__ 属性,指向构造该对象的函数的 prototype

function Person() {
    
}
const p = new Person();
console.log(p.__proto__ === Person.prototype);

原型链就是当我们访问一个实例对象身上不存在的属性或方法时,就可以沿着 __proto__prototype 组成的链条,到对应的原型身上去查找。

9、new 关键字

new 关键字做了哪四件事情:

  1. 创建一个空对象;

  2. 将函数中的 this 指向空对象;

  3. 执行函数中的代码;

  4. 返回这个对象;

10、this 指向

简单来说,this 的指向分为两种情况:

  1. 函数外:全局,this 指向 window;

  2. 函数里:

    1. 箭头函数:指向箭头函数所在父级的 this;

    2. 非箭头函数:谁调用这个函数,this 就指向谁;

function foo() {
    console.log(this);  // window
}

foo();

const obj = {
    sayHello() {
        console.log(this);
    }
}
obj.sayHello();  // obj


div.onclick = function() {
    console.log(this);   // div
}

class Person {
    sayHello() {
        console.log(this);
    }
}
const p = new Person();
p.sayHello()   // p

11、改变 this 指向

  • call:在改变 this 指向的同时,会立即执行该函数;所有参数都依次传递即可;

  • apply:在改变 this 指向的同时,会立即执行该函数;所有参数都必须放在一个数组中进行传递;

  • bind:在改变 this 指向后,返回一个新的函数;调用新函数时正常传递参数即可;

在实际开发中,大部分时候,如果 this 不是我们想要的指向,通常来说我们都希望它可以指向父级。这种情况下,我们优先考虑使用箭头函数来改变 this 的指向。

12、浅拷贝和深拷贝

浅拷贝和深拷贝都只存在于引用类型中。

  • 浅拷贝:引用类型的数据在拷贝的过程中,只要有一条数据地址没有发生改变,都属于浅拷贝。

  • 深拷贝:引用类型的数据在拷贝的过程中,所有引用类型的数据全都创建了新的地址,都属于深拷贝。

实现浅拷贝的方式有:

  • 扩展运算符

  • Object.()

实现深拷贝的方式有:

  • JSON 对象的两个方法

  • 手写递归函数

  • 借助第三方 JS 库:jQuery、lodash

13、异步解决方案的发展

  1. 回调函数

  2. Promise

  3. Generator

  4. async await

14、AJAX 的工作步骤

  1. 创建核心 XMLHttpRequest 对象

  2. 建立前后端的连接

  3. 发送请求

  4. 接收并处理后端返回的结果

15、Promise 三种状态

  • 等待中(进行中)

  • 已成功:调用 resolve 方法,状态从“等待中”变为“已成功”;

  • 已失败:调用 reject 方法,状态从“等待中”变为“已失败”;

01 -《前端模块化》

模块化,指的就是将每一个 JS 文件都看作是一个模块。

默认情况下,如果多个 JS 文件在同一个 HTML 中引入,他们是共享同一个全局作用域。

从 ES6 开始,JS 里面原生支持模块化开发。每一个 JS 文件都是一个独立的模块,每一个 JS 文件都有一个自己独立的作用域,JS 与 JS 之间,互相不能进行数据的访问。

一、模块化

1、设置模块

<script src="./js/a.js" type="module"></script>
<script src="./js/b.js" type="module"></script>

也可以引入一个index.js,只在它身上设置type='module'

2、JS 中引入模块

模块化的开发中,我们可以在一个 JS 文件中通过 import 引入另一个 JS 文件:

import 'JS文件的路径'

注意:同级路径之间必须加 ./

二、暴露和引入

默认情况下,每一个模块之间是互相独立的,数据都是私有的。但是,我们可以通过暴露的方式,将模块内的数据共享出去,同时,通过引入的方式,获取到其他模块暴露出来的数据。

1、暴露

暴露的语法分为两种情况:

  1. 只暴露一条数据:export default 数据

  2. 暴露多条数据:export 数据

const name = "张三";
export default name;

2、引入

根据暴露方式的不同,我们引入的语法也分为两种:

  • 引入 export default 暴露的数据:import 任意变量名 from "JS文件路径" 常用

  • 引入 export 暴露的数据:import { 暴露数据名称 } from 'JS文件的路径'

import b from './b.js'
console.log(b);

import { name, age, foo } from './b.js';
console.log('name', name);
console.log('age', age);
console.log('foo', foo);

三、引入(扩展)

1、重命名

import { name, age as newAge, foo } from './b.js';
const age = 30;
console.log(newAge);

2、合并多条数据

当我们要引入的模块中通过 export 暴露了很多条数据出来,我们引入时又想把所有暴露的数据全部拿到。这种情况下,如果还是按照暴露名称一一对应的方式来引入的话,就非常的不方便了。

所以,ES6 的模块化中还提供了一种写法,可以在引入时将 export 暴露的所有数据合并到一个对象中:

import * as obj from './b.js'
console.log(obj);

02 -《webpack 准备工作》

webpack,是一个前端资源构建工具。

webpack 可以实现的功能有:

  1. 代码转换:将浏览器不能识别的代码,转换成浏览器能识别的代码,例如 sass 转换为 css;

  2. 代码合并:将多个分散的文件,根据代码逻辑,合并成一个文件;

  3. 压缩代码:将代码中不必要的注释、空格、换行以及变量名函数名进行处理;

一、准备工作

1、安装 Node.js

下载 | Node.js 中文网

点击全部安装包

根据自己电脑系统选择对应的版本进行安装,可以不安装在 C 盘。

win7用12.22.9的版本,

其他的比较稳定的14,16的比较稳定,一般64位,

msi结尾是一般电脑,pkg结尾的是苹果电脑。

安装的时候,除了修改安装盘,其他的都下一步。

2、npm

Node.js Package Manager,Node.js 的包管理工具。

npm -v

设置 npm 的淘宝镜像:

npm config set registry https://registry.npm.taobao.org

02-01npm优雅的卸载npm包

windows系统切换node版本

首先下载nvm 下载地址:Releases · coreybutler/nvm-windows · GitHub 或者 https://download.csdn.net/download/xiaofiy/85235297 下载安装过程建议傻瓜式安装,直接安装到c盘

使用nvm install 14/ nvm install 16 安装14版本和16版本的node(默认会安装最新最稳定版本),完成之后用nvm ls列出所有可管理的node版本,如下图:

如果安装node失败,可以让同事将他的node文件发你,但是必须放在和nvm路径一样的文件夹里面,否则不能后续切换版本操作 查看nvm路径命令 : nvm root

切换使用node时,只需nvm use 版本号,指定当前使用的node版本,(此处以node版本16.14.2为例)。

Node –v查看当前node版本,如下图所示即成功。

常用nvm命令如下图所示:

注意: node14+ : vue2.0项目和vue3.0项目都可以运行 node16+ : vue2.0项目运行失败。vue3.0项目可以运行 npm依赖下载必须使用不同版本(vue2.0项目使用14+,vue3.0项目使用16+),否则下载失败。

03 -《项目初始化》

一、创建项目

1、创建项目根目录文件

我们自己创建一个空文件夹作为项目的根目录。文件名不要出现中文、大写字母,同时不要只用 webpack 命名。

例如,我们要创建一个电影后台管理系统的项目,我们可以命名为“movies-system”。

2、项目初始化

将终端定位到项目根目录的路径,然后执行以下命令对项目进行初始化:

 npm init -y

该命令执行完成后,会自动在项目的根目录中生成一个 package.json 文件,该文件是项目的描述文件。

二、下载 webpack

npm 中下载依赖包用到的命令都是 npm install 包名称

执行以下命令下载 webapck 和 webpack-cli:

 npm i webpack webpack-cli -D

说明:

  • 下载时需要将终端定位到当前项目的根目录;

  • install 可以简写为 i;

  • 如果一次性要下载多个依赖包,每个依赖包的名称之间用空格隔开;

  • -D 表示当前下载的依赖包只在开发时使用。

三、运行 webpack

我们在项目根目录中创建一个 src 的目录,然后再在 src 中创建一个 index.js

在终端中执行以下命令(注意终端路径在项目根目录):

npx webpack --mode=development

执行完该命令后,会自动在项目根目录中生成一个 dist 目录,该目录中存放的就是项目代码编译后的新代码。

04 -《生产环境和开发环境》

在项目的开发过程中,我们会将项目分为开发阶段和生产阶段。因为在两个阶段中项目中用到的一些依赖包是不一样的,所以,我们会在 webpack 中针对两个阶段进行不同的配置。

  • 开发环境:development,指的是项目在开发过程中需要用的一个配置,为了方便我们开发人员运行调试代码;

  • 生产环境:production,指的是项目已经开发完成,并且已经经过测试无明显异常,需要发布上线供用户使用;

一、创建配置文件

考虑到开发环境和生产环境中还是有很多相同的配置,所以,我们还需要用一个公共配置文件来设置两个环境中相同的配置代码。

在项目根目录中创建以下三个文件:

  • webpack.base.config.js:两个环境的公共配置;

  • webpack.dev.config.js:开发环境的配置;

  • webpack.prod.config.js:生产环境的配置;

二、webpack 配置初始模板

一个标准的 webpack 的配置文件的初始结构:(只是模板,不用复制)

module.exports = {
    mode: "",      // 生产环境、开发环境二选一
    entry: {},     // 配置 webpack 打包的入口文件
    output: {},    // 配置 webpack 打包的出口文件
    module: {},    // 配置代码转换规则
    plugins: [],   // 插件配置
    devServer: {}  // 配置开发服务器
}

三、合并公共配置

公共配置如果需要合并到两个环境中,需要使用插件 webpack-merge。

下载插件:

npm i webpack-merge -D

合并公共配置:(生产环境和开发环境各一个)

// 引入公共配置
const base = require('./webpack.base.config.js');
// 用于合并公共配置和当前环境配置
const { merge } = require('webpack-merge');
module.exports = merge(base, {
    // 开发环境自己的配置
    mode: 'development',
})
// 引入公共配置
const base = require('./webpack.base.config.js');
// 用于合并公共配置和当前环境配置
const { merge } = require('webpack-merge');
module.exports = merge(base, {
    // 生产环境自己的配置
     mode: 'production',
})

四、配置启动命令

因为我们需要在不同情况下启动不同的配置文件,为了方便使用,我们将两个启动命令重新命名。

1、下载开发服务器

npm i webpack-dev-server -D

2、配置开发环境中的服务器

module.exports = merge(base, {
    // 开发环境自己的配置
    mode: 'development',
    devServer: {
        port: '8080', // 端口号2000-6W+随便写
         open:'home.html',//打开网页默认为首页。
    }
})

3、配置启动命令

找到项目的 package.json 文件:

{
    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1",
        "build": "webpack --config webpack.prod.config.js --mode production",
        "dev": "webpack-dev-server --hot --config webpack.dev.config.js --mode development"
    },
}

其中,webpack.prod.config.js 是我们前面创建好的生产环境的配置文件名。

配置完成后,我们就可以通过以下命令来运行配置文件了:

npm run build
npm run dev

build后将默认dist(或者设置的其他文件夹名)压缩发后端(zip格式)。

05 -《webpack 基础配置》

一、入口文件 entry

项目中,每一个 .html 中都应该有一个主 .js 文件,这个 JS 文件,就是当前页面的入口文件。

例如,我们的项目中有登录、注册、首页三个页面,那么对应的就有三个入口文件:

src文件夹中创建js文件夹,后再创建home.js、login.js、register.js文件。

module.exports = {
    entry: {
        home: './src/js/home.js',
        login: './src/js/login.js',
        register: './src/js/register.js',
    }
}

二、出口文件 output

webpack 对入口文件进行打包编译后,会生成对应的出口文件,我们需要告诉 webpack 出口文件的位置以及名称。

//公共配置
const path =require('path');

module.exports = {
    output: {
        // 配置出口文件的保存路径:项目根目录的 dist
        path: path.resolve(__dirname, 'dist'),
        // 出口文件的名称
        filename: 'js/[name].js'
    }
}

综合起来

src文件夹中创建js文件夹,后再创建home.js、login.js、register.js文件。

//公共配置
const path =require('path');

module.exports = {
    entry: {
        home: './src/js/home.js',
        login: './src/js/login.js',
        register: './src/js/register.js',
    },
     output: {
        // 配置出口文件的保存路径:项目根目录的 dist
        path: path.resolve(__dirname, 'dist'),
        // 出口文件的名称
        filename: 'js/[name].js'
    }
}

三、配置 HTML

webpack 默认情况下,只能处理 .js 代码。如果我们要开始编译 HTML 代码,需要下载插件。

1、下载 html-webpack-plugin

npm i html-webpack-plugin -D

2、配置插件

在src文件夹中创建home.html、login.html、register.html文件

const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
    plugins: [
        new HtmlWebpackPlugin({
            template: './src/home.html',   // 源文件路径
            filename: 'home.html',         // 打包后的文件名
            chunks: ['home']               // 入口文件的名称
        }),
        new HtmlWebpackPlugin({
            template: './src/login.html',   // 源文件路径
            filename: 'login.html',         // 打包后的文件名
            chunks: ['login']               // 入口文件的名称
        }),
        new HtmlWebpackPlugin({
            template: './src/register.html',   // 源文件路径
            filename: 'register.html',         // 打包后的文件名
            chunks: ['register']               // 入口文件的名称
        }),
    ]
}

四、复制文件*

当我们的项目中,有一些文件代码不需要 webpack 进行处理,只需要直接复制到打包后的目录中即可。那么,也需要进行对应的配置。

1、下载 copy-webpack-plugin

npm i copy-webpack-plugin -D

2、配置插件

在src文件夹中创建static文件夹,static文件夹创建libs文件夹,将jquery-3.6.0.min.js文件放入libs文件夹中。

同时,也可以在static文件夹中创建一个css文件夹,用于存放静态的公共样式代码common.css,在需要的页面(home.html)调用css文件,路径为src中的路径。

最后在home.html文件(有需要的文件)中用<link rel="stylesheet" href="./static/css/common.css">和 <script src="./static/libs/jquery-3.6.0.min.js"></script>引入。

const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = {
    plugins: [
        // ...
        new CopyWebpackPlugin({
            patterns: [
                { from: './src/static', to: './static' }
            ]
        })
    ]
}

五、配置 SASS

1、下载插件

npm i node-sass sass-loader css-loader mini-css-extract-plugin -D

如果下载报错,基本上都是因为 node-sass 的问题,执行以下命令重新下载 node-sass:

npm install node-sass --sass-binary-site=https://npm.taobao.org/mirrors/node-sass

2、配置

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
    module: {
        rules: [
            // 配置 SASS
            {
                test: /\.scss$/i,           // 需要编译以 .scss 为结尾的文件
                exclude: /node_modules/,    // 不需要解析 node_modules
                use: [
                    MiniCssExtractPlugin.loader,
                    'css-loader',
                    'sass-loader'
                ]
            }
        ]
    },
    plugins: [
        //... 
        new MiniCssExtractPlugin({
            filename: 'css/[name].css'
        })
    ]
}

说明:按照我们当前的配置,项目中的 .scss 文件不能放在 static 中。建议在 src 目录中创建一个 sass 目录,用来存放所有页面的 .scss 文件。

当某一个页面中需要引入 .scss 时(home.html),我们应该找到该页面的入口 JS 文件(home.js),然后在 JS 中通过 import 来引入对应 .scss

import '../sass/home.scss';

六、配置 source map

在开发环境中进行以下配置:可以将报错定位还原。

module.exports = {
    devtool: 'inline-source-map'
}

06 -《webpack 优化配置》

一、删除废除文件

1、下载插件

npm i clean-webpack-plugin -D

2、配置插件

在生产环境中进行以下配置:

const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
    // ...
    plugins: [
        new CleanWebpackPlugin()
    ]
}

二、最小化 JS 和 CSS

1、下载插件

npm i terser-webpack-plugin css-minimizer-webpack-plugin -D

2、配置插件

const TerserWebpackPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
module.exports = {
    //...
    // 生产环境优化
    optimization: {
        minimize: true,
        minimizer: [
            new TerserWebpackPlugin(),
            new CssMinimizerPlugin()
        ]
    }
}

01 -《Node.js 准备工作》

一、node.js 和 npm 安装

之前已经安装

二、npm 常用命令

命令说明
npm install 包名称下载开发和生产使用的依赖包(记录在 package.json 文件 dependencies 属性中),新项目直接复制粘贴后输入该命令,自动下载。
npm uninstall 包名称删除依赖包
npm install 包名称 -D下载开发环境使用的依赖包(记录在 package.json 文件 devDependencies 属性中)
npm install 包名称 -g下载全局的依赖包
npm install 包名称@版本号下载指定版本的依赖包
npm install 包名称一 包名称二 …同时下载多个依赖包(包名称之间用空格隔开)

三、Nodejs 运行 JS 代码

在 Node.js 中,可以直接脱离浏览器,使用 Nodejs 来运行 JS 代码。

创建任意一个 JS 文件,然后在终端中定位到该 JS 文件所在路径,通过 node 文件名 的命令就可以运行 JS 文件了。

例如,我们创建一个 a.js

console.log('hello a');

然后在终端中定位到该 a.js 文件所在路径,执行 node a.js 命令,运行 a.js

四、后端模块化

后端和前端一样,也要模块化的语法。

1、暴露

基础语法:

module.exports = {
    属性名一: 数据值一,
    属性名二: 数据值二,
}
module.exports.属性名一 = 数据值一;
module.exports.属性名二 = 数据值二;

示例代码:

const name = "张三";
const age = 20;
module.exports = {
    name, age   // 简写
}
// 或
module.exports.name = name; 
module.exports.age = age;

2、引入

如果我们只是需要引入一个文件让其执行,不需要获取该文件中的任何数据。那么,直接 require() 该文件即可:

require("JS文件的路径");

如果我们既要引入一个 JS 文件,同时还需要获取该 JS 文件中暴露出来的数据。那么 require() 之后还需要用变量来接收数据:

const 任意变量名 = require("JS文件的路径");

示例代码:

例如我们在引入前面例子中的 a.js,如果不需要获取 a.js 的数据:

require('./a.js');

如果我们引入 a.js 的同时需要获取 a.js 的数据:

const student = require('./a.js'); 
console.log(student); // { name: '张三', age: 20 }

或者直接解构:

const { name, age } = require('./a.js'); 
console.log(name, age);   // '张三'  20

C:\Users\Administrator\Desktop\20220427\node-movies> ·cd ..可以实现返回上一级终端

C:\Users\Administrator\Desktop\20220427> ·cd node-movies可以实现跳转下一级终端

02 -《MongoDB 准备工作》

一、安装数据库服务器

二、安装可视化工具

03 -《搭建 Express 服务器项目》

一、创建项目根目录

自己手动创建一个空文件夹,作为项目的根目录,例如:movies-system-server

二、创建 Express 项目

将终端的路径定位到项目的根目录,然后执行以下命令:

npx express-generator

该命令执行完成后,会自动在项目的根目录中生成 Express 服务端项目的基本结构。

三、下载项目所有依赖包

将终端的路径定位到项目的根目录,执行以下命令,根据 package.json 文件下载项目需要的所有依赖包:

npm i

四、启动项目

将终端的路径定位到项目的根目录,执行以下命令启动项目:

npm start

五、访问项目

在浏览器中输入 localhost:3000 可以访问到服务器项目的欢迎界面:

六、下载 nodemon 插件

每一次 express 项目中的代码发生改变后,都需要重启服务器,来让新的代码生效。所以,我们需要借助 nodemon 插件来帮助我们自动重启服务器。

1、下载插件

npm i nodemon -g

2、更改启动命令

将项目根目录的 package.json 文件中的 start 属性的值更改为以下命令:

{
    "scripts": {
        // "start": "node ./bin/www"   // 旧的
        "start": "nodemon ./bin/www"   // 更改后的
    },
}

注意:nodemon 插件是根据代码是否重新保存来控制服务器是否重启。所以如果设置了“自动保存”代码给功能的话,代码每隔一两秒保存一次,服务器也就会每隔一两秒重启一次。建议使用 nodemon 启动服务器的话,就关闭代码的“自动保存”功能。

04 -《Express 连接 MongoDB》

Express 的服务器项目如果需要连接 MongoDB 数据库服务器,需要在 Express 中进行配置。这里,我们选择使用 mongoose 插件用来处操作 MongoDB。

一、下载 mongoose

将终端的路径定位到项目的根目录,然后执行以下命令,下载 mongoose 插件:

npm i mongoose

二、配置连接 MongoDB

在项目根目录中创建一个文件夹 dao,该目录中专门用来存放所有关于数据库操作的代码。

dao 的目录中,创建一个 db.js 文件(db 取的是 database 单词中的两个字母),在该文件中配置以下代码来连接 MongoDB:

const mongoose = require('mongoose');
// 其中:woniumovies 是我们要连接的数据库的名称(如果数据库不存在,后续会自动创建)
mongoose.connect("mongodb://localhost:27017/woniumovies", () => {
    console.log('woniumovies 数据库连接成功');
})

三、执行配置代码

服务器的启动,默认会运行 app.js 文件。因此,如果需要运行 db.js 的话,需要在 app.js 中引入 db.js,参考代码如下:

// 引入mongoose的配置文件,连接 express 和 mongodb
require('./dao/db.js');
// ... 后续的代码位置
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');

查看本地id地址:1.win+r。2.cmd。3. ipconfig命令

四、启动服务器

最后,配置完成后,重新执行以下命令启动服务器:

npm start

启动成功后,我们会在终端中看到类似以下提示:

[nodemon] 2.0.15
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node ./bin/www`
woniumovies 数据库连接成功

到此,Express 连接 MongoDB 数据成功!

备注:有时mongoDB没有运行,需要我的电脑右键点管理,双击服务,找到mongoDB并运行。

05 -《Express 处理前端请求》

一、前端发送 AJAX 请求

通常我们的前端项目都搭建在 webpack 的服务器中。所以,前端发送 AJAX 请求,实际就是从 webpack 的服务器中发送一个请求到后端 Express 的服务器。

二、后端处理前端的请求

后端会根据前端请求的资源,在 Express 项目的 routes 目录中创建对应的 JS 文件。

1、创建文件

例如,前端发送了 /movies/getMovies 请求,那么我们在后端需要在 routes 中创建一个 movies.js 文件,然后将 user.js 中的代码复制到该文件中。

2、设置一级接口

在 Express 项目的 app.js 中,针对当前请求,进行以下配置:

var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
const moviesRouter = require('./routes/movies');  // 新增的
app.use('/', indexRouter);
app.use('/users', usersRouter);
app.use('/movies', moviesRouter);   // 新增的

配置完成后,前端发送的 /movies/getMovies 请求,一级接口就会和 app.js 文件中的配置匹配成功。

3、设置二级接口

匹配成功后,前端请求进入到对应的 JS 文件中。

movies.js 中继续配置二级接口:

var express = require('express');
var router = express.Router();
router.get('/getMovies', function (req, res, next) {
    console.log('进入到后端的 getMovies 了');
    res.send('后端发送给前端的数据');
});
module.exports = router;

当前端请求的类型、一级接口、二级接口都和后端匹配成功后,就会执行对应的函数了。

06 -《Express 操作 MongoDB》

一、配置数据模型(结构)

在通过 mongoose 插件操作 MongoDB 之前,我们需要对要操作的数据格式进行配置。

dao 目录中,再新建一个 models 的目录,用来存放所有项目中数据的数据结构的配置代码。

例如,我们要配置电影数据的结构,那么我们在 models 目录中,再新建一个 moviesModel.js 文件,用来配置电影数据相关的结构。

const { Schema, model } = require('mongoose');
// 定义数据结构
const moviesSchema = new Schema({
    name: String,  // 电影中文名
    eName: String, // 电影英文名
    actors: String, // 演员
    director: String, // 导演
    language: String,  // 语言
    region: String, // 上映地区
    desc: String,  // 简介
    duration: String,  // 时长
})
// 定义数据模型
// 参数说明
// 第一个参数:自己给数据模型取的名字(自己命名)
// 第二个参数:数据结构的变量名
// 第三个参数:MongoDB 服务器中数据集合的名字
module.exports = model('moviesModel', moviesSchema, 'movies');

为了让以上配置代码运行生效,我们在 db.js 中引入所有的数据模型的配置文件,让这些配置在服务器项目一启动时就生效。

require('./models/moviesModel.js');

二、通过模型操作数据

考虑到项目中操作数据的代码量比较大,通常我们还会在项目的根目录中创建一个 controllers 文件,该目录中用来存放所有操作数据库的代码。

例如我们要操作电影数据,在 controllers 目录中,再创建一个 moviesControllers.js 文件,用来处理所有关于电影数据的操作代码:

// 引入数据模型
const moviesModel = require('../dao/models/moviesModel');
// 将方法暴露给 routes/movies.js 中调用
module.exports.getMovies = async () => {
    // 通过数据模型开始获取数据
    const data = await moviesModel.find();
    // 将数据返回给 routes/movies.js 文件中
    return {
        code: 1,
        message: '电影数据获取成功',
        data
    }
}
module.exports.addMovies = () => {
}

三、调用 controllers 方法

routes 中的文件里,我们引入并调用 controllers 中暴露出来的方法:

var express = require('express');
var router = express.Router();
// 获取到 moviesControllers.js 中暴露的方法
const { getMovies, addMovies } = require('../controllers/moviesControllers');
router.get('/getMovies', async function (req, res, next) {
    // 开始获取 movies 集合中的电影数据
    const data = await getMovies();
    // 将数据结果返回给前端
    res.send(data);
});
router.post('/addMovies', function (req, res, next) {
});
module.exports = router;

四、前端发送数据给后端

1、前端发送数据

前端通过 AJAX 发送请求到后端,除了可以用来接收后端返回的数据外,也可以在发送请求的同时发送数据给后端。语法如下:

$.ajax({
    url: '资源路径',
    type: '请求类型',            // 获取数据用 GET,新增、修改、删除数据用 POST
    data: {                     // 发送给后端的数据
        数据键一: 数据值一,
        数据键二: 数据值二,
        // ...
    },
    success(res) {
        console.log(res);
    }
})

2、后端接收数据

Express 后端接收前端数据,是在路径匹配成功时执行的函数的参数 req 中。

我们以新增电影数据为例,当前端发送了要新增的电影数据到后端后,后端接收数据:

const { getMovies, addMovies } = require('../controllers/moviesControllers');
router.post('/addMovies', async function (req, res, next) {
    // 接收前端发送的数据
    const params = req.body;
    // 调用 controllers 中新增电影的方法,并将数据传递到 controllers 中
    const data = await addMovies(params);
    // 将新增成功或失败的结果返回给前端
    res.send(data);
});

说明:前端不管是使用 GET 还是 POST 请求,其实都可以发送数据给后端。发送数据的语法都一样,都是通过 data 进行发送。但是,后端接收的方式不一样。

如果是 GET 请求,后端需要通过 req.query 来接收,如果是 GET 以外的其他任何请求,后端都通过 req.body 来接收(上例代码中的形式)。

五、mongoose 的方法

mongoose 中,所有对数据库的操作都是由数据模型提供的。例如我们前面获取电影数据用到的 find() 方法。除了 find() 以外,mongoose 还提供了很多的方法用来操作 MongoDB 数据,这里我们列举几个常用的方法:

1、获取所有数据

const data = await 数据模型.find();

2、按条件查询数据

const data = await 数据模型.find({查询键: 查询值});

例如要查询 name 值为“哈利·波特与魔法石”的电影数据:

const data = await moviesModel.find({ name: "哈利·波特与魔法石" });

3、新增数据

const result = await 数据模型.create({ 新增数据键: 新增数据值 });

例如我们要新增一条 name 值为“星际穿越”的电影数据(新增数据时,可以只传数据模型中的部分数据):

const result = await moviesModel.create({ name: "星际穿越" });

4、按条件删除数据

const result = await 数据模型.deleteOne({ 删除数据键: 删除数据值 });

例如我们想要根据“哈利·波特与魔法石”的 _id 将该条电影数据删除:

const result = await 数据模型.deleteOne({ _id: '6154072d28f56c648410d7a3' });

5、按条件修改数据

const result = await 数据模型.updateOne({ 查询条件键: 查询条件值 }, { 新数据键: 新数据值 });

例如我们想要通过星际穿越的 _id 找到该条数据,并将 name 改为“星际不穿越”:

const result = await 数据模型.updateOne({ _id: '6154071228f56c648410d7a2' }, { name: "星际不穿越" });

6、分页获取数据

mongoose 中提供了 limit()skip() 两个方法来实现分页获取数据。

  • limit():设置需要获取的数据条数

  • skip():设置跳过的数据条数

例如,如果我们需要获取电影列表第一页的 3 条数据,参考代码如下:

const data = await moviesModel.find().skip(0).limit(3);

因此,如果后端要做分页功能,前端至少需要发送两个参数给后端,分别是(数据名称可自定义):

  • currentPage:当前页码

  • pageSize:每页显示的数据条数

最后,将这两个参数应用到 limit()skip() 两个方法中:

const { currentPage, pageSize } = params;
const data = moviesModel.find().populate('type').skip((currentPage - 1) * pageSize).limit(pageSize);

7、获取数据总条数

在做分页时,我们通常都只需要获取部分的数据。但是在计算总页数时,我们需要知道列表数据的总条数。mongoose 中提供了专门的方法 countDocuments()来获取数据集合中数据的总条数。

例如我们获取电影数据的总条数:

const total = await moviesModel.countDocuments();

有了总条数和每页显示的条数,就可以自己计算出总页数了:

const pages = Math.ceil(total / pageSize);

07 -《前后端分离项目的启动步骤》

一、启动 Node.js 后端项目

终端中定位到项目根目录,执行启动命令:

npm start

正常情况下,Node.js 的后端服务器启动成功后,会自动连接到 MongoDB 数据库。

如果没有提示“数据库连接成功”,就有可能是数据库的服务器没有开启。

如果提示了“数据库连接成功”,就跳过第二步。

二、启动 MongoDB 服务器

右键【此电脑】-【管理】-【服务与应用程序】-【服务】-【MongoDB Server】-【启动】

三、启动 webpack 前端项目

终端中定位到项目根目录,执行启动命令:

npm run dev

08 -《封装网络请求》

在实际开发中,会将所有的 AJAX 请求全都封装到一个目录中来进行处理。

一、创建 API 目录

我们在 src 目录中创建一个 api 目录,用来存放所有数据相关的 AJAX 请求。

二、创建 API 文件

我们根据数据的类型,把 AJAX 请求再做一个分类。例如:电影数据相关的请求,统一放在一个文件中;用户数据相关的请求,统一放在另一个文件中…

所以,我们在 API 目录中创建对应的 .js 文件。例如:

  • moviesAPI.js

  • usersAPI.js

三、封装 AJAX 请求

例如我们在 moviesAPI.js 中用 Promise 封装关于电影数据的请求:

// 获取电影数据
export function getMoviesAsync() {
    return new Promise((resolve, reject) => {
        $.ajax({
            url: '/api/movies/getMovies',
            type: 'GET',
            success(res) {
                resolve(res);
            }
        })
    })
}
// 删除电影数据
export function deleteMoviesAsync(data) {
    return new Promise((resolve, reject) => {
        $.ajax({
            url: '/api/movies/deleteMovies',
            type: 'POST',
            data,
            success(res) {
                resolve(res);
            }
        })
    })
}
// ...

通过 export 将所有的请求方法暴露出去。后续需要发送请求时,只需要引入该方法并调用即可。

四、调用 AJAX 请求

例如我们想要发送 AJAX 请求获取电影数据。

我们在处理电影的 JS 文件 movies.js 中,引入封装好的 API,并调用:

import { getMoviesAsync } from '../api/moviesAPI.js'
moviesRender();
async function moviesRender() {
    // 发送请求获取所有的电影数据
    const res = await getMoviesAsync();
    console.log(res);
}

五、渲染数据

在第四步中获取到数据后,我们就可以开始渲染数据了:

async function moviesRender() {
    // 发送请求获取所有的电影数据
    const res = await getMoviesAsync();
    // 判断后端是否有成功的获取到数据
    if(res.code) {
        // 渲染所有的电影数据
        render(res.data);
    }
}
function render(moviesData) {
    const moviesHTML = moviesData.map(item => (
        `<tr>
            <td>${item.name}</td>
            <td>${item.eName}</td>
            <td>${item.director}</td>
            <td>${item.actors}</td>
            <td></td>
            <td></td>
            <td></td>
            <td>
                <a href="#">修改</a>
                <a href="#" class="deleteBtn" data-id="${item._id}">删除</a>
            </td>
        </tr>`
    )).join("");
    $('#moviesTbody').html(moviesHTML);
}

09 -《新增电影的前后端流程》

前后端代码执行流程参考:

我们以“新增电影”功能为例。

一、前端

1、封装“新增电影”的请求

我们是将项目中所有的 AJAX 请求全都统一在 API 的目录中进行处理,找到该目录中电影的 API 文件,例如:moviesAPI.js。开始通过 Promise 封装 AJAX 请求:

// 新增电影数据
export function addMoviesAsync(data) {
    return new Promise((resolve, reject) => {
        $.ajax({
            url: '/api/movies/addMovies',
            type: 'POST',
            // AJAX 的 data 属性,用来向后端发送数据
            // 当前请求中,具体的 data 值,是外部调用该方法时传递进来的
            data,   
            success(res) {
                resolve(res);
            }
        })
    })
}

2、页面调用 AJAX 请求

我们在项目的 js 目录中创建了一个 movies.js 文件,专门用来处理管理系统页面中所有关于电影的操作。

因此我们现在要做“新增电影”的功能,需要在该文件中,给“新增电影”的按钮身上添加一个点击事件,在事件中调用封装好的 AJAX 方法,发送请求。

参考代码如下:

import { addMoviesAsync } from '../api/moviesAPI.js'
// 新增电影
$('#addMoviesBtn').click(async function () {
    // 获取页面中用户输入的电影数据
    const name = $('#addMovieName').val();
    const eName = $('#addMovieEName').val();
    // ... 获取页面中其他数据
    // 调用 AJAX 方法发送请求
    const res = await addMoviesAsync({ name, eName });
    // 输出后端返回的请求结果
    console.log(res);
})

3、测试请求是否成功

前端页面和功能的代码完成后,我们先不着急写后端,需要先检查一下前端的代码是否有问题。

根据写好的代码,操作页面,触发事件发送请求。然后依次进行以下检查:

1)查看后端的终端中是否有当前请求的日志

正常情况下,我们还没有开始写后端的代码,那么这个时候前端请求发送到后端后,终端的日志中应该会有一个 404 的请求记录。

参考下图:

出现类似以上日志,说明前端的请求已经成功的发送到后端。

如果后端终端中没有对应的请求日志,则说明前端请求可能有问题,我们进行下一步检查。

2)查看浏览器 F12 控制台的【Network】

在浏览器的 F12 控制台中查看网络请求,正常情况下,我们能在 Network 中查看到当前页面发送的所有请求。

参考下图:

如果请求列表里没有出现我们发送的 AJAX 的接口名称,则说明前端根本没有发送 AJAX 请求。

二、后端

1、创建 routes 中对应的文件

在 Express 的后端中,有一个 routes 的目录,专门用来匹配所有前端请求的二级接口。

通常来说,会在该目录中创建不同的 .js 文件,用来处理不同类型的数据请求。

例如,我们要处理电影数据,就会在 routes 目录中创建一个 movies.js 文件,专门用来处理所有关于电影数据的请求。

在该文件中添加以下初始代码:

var express = require('express');
var router = express.Router();
// router.get('/', function(req, res, next) {
//     res.send('respond with a resource');
// });
module.exports = router;

2、在 app.js 中配置一级接口

在 Express 后端项目的 app.js 中,通过以下代码,来引入我们刚刚创建好的 routes 中的文件,同时设置对应的一级接口:

// 引入 routes 中的文件
const moviesRouter = require('./routes/movies');
// ....
// 配置一级接口
app.use('/movies', moviesRouter);

3、在 routes 的文件中配置二级接口

根据当前前端发送的 AJAX 请求的 URL,在后端对应的 routes 的文件中,配置二级接口。

例如:前端通过 POST 发送了一个“新增电影”的请求,我们需要在 routes/movies.js 中进行以下配置:

// 新增电影
router.post('/addMovies', function (req, res, next) {
    console.log('123123123')
});

我们完成这一部分配置后,重新回到前端页面,操作页面发送请求,在后端的终端中查看当前的输出是否有执行。

如果输出执行了,则表示我们前端的请求成功的进入到后端。

4、在 routes 中接收前端发送的数据

如果当前的请求,前端有发送数据给后端,我们需要在后端的 routes 的文件中,接收到前端发送过来的数据。

后端接收前端数据的方式有两种:

  • 前端发送 GET 请求:后端通过 req.query 接收参数;

  • 前端发送除 GET 外的其他请求:后端通过 req.body 接收参数;

例如,新增电影的请求需要接收前端发送过来的新电影的数据:

// 新增电影
router.post('/addMovies', function (req, res, next) {
    const params = req.body;
    console.log(params);
});

正常情况下来说,后端输出 req.body,就可以查看到前端发送过来的数据。

如果后端输出没有数据,可以在浏览器的 F12 的控制台的【Network】中,找到对应的请求,点击进去,查看请求中是否有【payload】。

确认前端发送的数据没问题后,我们就可以继续去处理后端了。

5、创建 controllers 中对应的文件

在 Express 的后端中,我们会在项目根目录创建一个 controllers 的目录,专门用来存放所有关于操作 MongoDB 数据库的代码。

例如,我们要新增电影,需要操作电影数据,因此需要在该目录中创建一个 moviesControllers.js 文件,专门用来处理所有关于电影数据的操作代码。

6、在 controllers 中暴露方法

moviesControllers.js 文件中创建并暴露一个方法,通过形参 params 来接收 routes 中获取到的前端数据:

// 新增电影
// params 用来接收 routes 中传递的参数
module.exports.addMovies = async (params) => {
    console.log('接收 routes 传递的数据:', params)
}

7、routes 中引入并调用 controllers 的方法

routes 中需要将接收到的前端数据,发送给 controllers。因此需要引入并调用 controllers 中的方法:

const {  addMovies  } = require('../controllers/moviesControllers');
// 新增电影
router.post('/addMovies', function (req, res, next) {
    const params = req.body;
    // 调用 controllers 的方法,并将参数传递给 controllers
    addMovies(params);
});

8、controllers 操作数据库

controllers 的方法中确认接收到数据后,就可以使用 mongoose 提供的各种方法去操作 MongoDB 数据库了。

例如,新增电影应该使用 create 方法,并将新增数据组成一个对象作为参数传递给 create:

module.exports.addMovies = async (params) => {
    const result = await moviesModel.create(params);
    console.log(result);
}

数据库操作完成后,通过 async await 接收到数据库返回的结果,并输出查看结果。

9、返回结果给 routes

在 controllers 中,对数据库返回的结果进行判断处理,然后通过 return 返回处理结果给 routes:

// 新增电影
// params 用来接收 routes 中传递的参数
module.exports.addMovies = async (params) => {
    const result = await moviesModel.create(params);
    if(result) {
        return {
            code: 1,
            message: '电影数据新增成功'
        }
    }
    return {
        code: 0,
        message: '电影数据新增失败'
    }
}

10、routes 接收结果返回给前端

最后,routes 中接收到 controllers 中处理好的结果,通过 res.send() 返回给前端。

// 新增电影
router.post('/addMovies', async function (req, res, next) {
    const params = req.body;
    // 调用 controllers 的方法,并将参数传递给 controllers
    const result = await addMovies(params);
    res.send(result);
});

最终,前端 AJAX 的 success 中通过形参 res 可以接收到后端返回的结果。

10 -《数据集合的关联》

MongoDB 中的数据集合与集合之间是可以通过 _id 来进行关联。

例如,我们希望将电影集合和分类集合进行关联,就可以给每一条电影数据都添加一个 type 属性,用来保存对应分类的 _id。

一、数据模型配置

我们以电影集合为例。电影集合中的每一条电影数据,我们都希望有一个 type 属性,用来记录当前电影所属分类。

所以,在配置电影集合的数据模型时:

const { Schema, model } = require('mongoose');
// 定义数据结构
const moviesSchema = new Schema({
    // ... 其他常规属性
    // 负责去关联“分类”的集合
    type: {
        type: Schema.Types.ObjectId,  // 固定格式
        ref: 'typesModel'  // 需要关联的另一个集合模型名称
    }
}, { versionKey: false })
module.exports = model('moviesModel', moviesSchema, 'movies');

其中,type: Schema.Types.ObjectId 是关联属性的固定格式,用来表示当前属性值的数据类型。ref 的值用来设置需要关联的另一个集合的模型名称。

模型配置成功后,后续我们再新增电影数据时,除了新增常规的电影数据外,还需要包括当前电影对应的分类的 _id,例如:

二、关联查询

我们在查询电影数据时,如果还是只通过 find() 方法查询,会导致我们查询出来的电影数据的 type 属性值只是一串 _id 数字。但是,我们真正需要的是,在查询电影数据时,除了获取到电影的常规属性外,还可以通过电影分类的 _id,查询到对应的分类详细信息。

因此,我们在做 find() 查询时,需要做一个关联查询:

module.exports.getMovies = async () => {
    // 关联查询
    const data = await moviesModel.find().populate('type');
    return {
        code: 1,
        message: '电影数据获取成功',
        data
    }
}

说明:populate() 方法是 mongoose 中提供的专门用于关联查询的方法。该方法的参数就是当前集合对外做关联的属性键名。

这样,关联查询到的结果中,电影的 type 属性值就是一个对象,该对象包含的就是当前分类的详细信息。例如:

三、后台处理倒序

module.exports.getMovies = async () => {
    // 关联查询
    const data = await moviesModel.find().populate('type').sort({'score':-1});
    return {
        code: 1,
        message: '电影数据获取成功',
        data
    }
}

11 -《图片上传》(头像上传)

一、前端图片预览

1、原理

用户在页面中选择一张图片时,并不确定就一定要保存这张图片。因此,在确定保存之前,用户选择的图片我们都只需要做一个临时的预览就可以了。

所以我们实现的思路是使用 FileReader 对象的 readAsDataURL() 方法来将图片转换为一个 base64 的字符串,然后直接渲染字符串来实现图片的预览。

2、代码实现

` 节点身上添加一个change` 事件:

$('#addUserAvatar').change(function () {
    // console.log(this.files); // 获取到用户选中的所有的图片信息
    const fileInfo = this.files[0];
    const reader = new FileReader();
    reader.readAsDataURL(fileInfo);
    reader.onload = function (e) {
        // console.log(e.target.result);   // base64 的字符串
        $('#userAvatar').attr('src', e.target.result);
    }
})

二、图片上传

1、原理

只有当用户点击确认按钮时,我们才可以确定用户要保存当前图片。

base64 的图片信息只是做一个临时的预览,因为 base64 的字符串太长了,我们并不会将该字符串保存到后端数据库。

所以,如果用户确认要保存一张图片时,我们要做的事情是:将图片信息通过 AJAX 发给后端服务器,后端服务器根据图片信息复制一张新的图片保存到服务器中,最后只需要将服务器上新图片的路径保存到数据库中即可。

2、前端代码实现

1)获取相关数据

我们以新增用户为例,当点击确认新增时,要获取到用户的基本信息,以及图片信息:

// 确认新增
$('#addUserBtn').click(async function () {
    const name = $('#addUserName').val();
    const account = $('#addUserAccount').val();
    const password = $('#addUserPassword').val();
    const gender = $('#addUserGender').val();
    // 图片信息
    const avatar = $('#addUserAvatar')[0].files[0];
})

2)处理数据格式

由于文件信息要以 FormData 的数据格式发送给后端,因此我们需要将图片 信息连同其他常规数据,一起进行处理:

// 确认新增
$('#addUserBtn').click(async function () {
    const name = $('#addUserName').val();
    const account = $('#addUserAccount').val();
    const password = $('#addUserPassword').val();
    const gender = $('#addUserGender').val();
    // 图片信息
    const avatar = $('#addUserAvatar')[0].files[0];
    // 因为后端需要以 FormData 的形式来接收图片数据
    const fd = new FormData();
    fd.append('file', avatar); // 将图片信息添加到 fd 对象身上 ,并取名叫 file
    fd.append('name', name);
    fd.append('account', account);
    fd.append('password', password);
    fd.append('gender', gender);
})

3)发送 AJAX 请求

数据处理完成后,我们就可以直接将 fd 对象通过 AJAX 的 data 属性发送给后端,但是 AJAX 中还需要额外添加三个属性。

封装的 AJAX 参考如下:

export function addUsersAsync(data) {
    return new Promise((resolve) => {
        $.ajax({
            url: '/api/users/addUsers',
            type: 'POST',
            // 以下三个属性,主要用来告诉 jQuery 不要处理 data 的数据格式
            processData: false,
            contentType: false,
            cache: false,
            data,
            success(res) {
                resolve(res);
            }
        })
    })
}

最后,确认新增的点击事件中调用 AJAX 方法:

// 确认新增
$('#addUserBtn').click(async function () {
    const name = $('#addUserName').val();
    const account = $('#addUserAccount').val();
    const password = $('#addUserPassword').val();
    const gender = $('#addUserGender').val();
    // 图片信息
    const avatar = $('#addUserAvatar')[0].files[0];
    // 因为后端需要以 FormData 的形式来接收图片数据
    const fd = new FormData();
    fd.append('file', avatar); // 将图片信息添加到 fd 对象身上 ,并取名叫 file
    fd.append('name', name);
    fd.append('account', account);
    fd.append('password', password);
    fd.append('gender', gender);
    // 以上处理完成后,所有的数据都添加到了 fd 对象身上
    const res = await addUsersAsync(fd);
})

3、后端代码实现

1)下载配置图片上传

后端要根据前端发送的图片信息,开始复制图片。这里我们需要用到一个插件 multer,所以需要现在后端终端中下载该依赖包:

npm i multer

下载完成后,在项目根目录中创建一个 utils/fileUpload.js 文件,用来配置图片上传的过程(拷贝我们发送的文件就行了)。

2)上传图片

在后端对应的接口文件中,引入并调用封装好的图片上传方法:

var express = require('express');
var router = express.Router();
const uploadFiles = require('../utils/fileUpload.js');  // 引入配置好的图片上传方法
router.post('/addUsers', function(req, res, next) {
    // 图片上传(将用户本地的图片复制到当前服务器项目中)
    const upload = uploadFiles();
    upload(req, res, (err) => {
        if(err) {
            console.log('图片上传失败', err);
        } else {
            console.log('图片上传成功');
        }
    })
});
module.exports = router;

图片上传成功后,会在 Node.js 项目的 public 目录中新增一个 images 目录,同时新增我们上传的图片。

3)获取数据

图片上传成功后,我们通过 req.files 获取到上传后的图片信息,以及通过 req.body 获取到其他常规数据:

var express = require('express');
var router = express.Router();
const uploadFiles = require('../utils/fileUpload.js');
const { addUsers } = require('../controllers/usersControllers.js')
router.post('/addUsers', function(req, res, next) {
    // 图片上传(将用户本地的图片复制到当前服务器项目中)
    const upload = uploadFiles();
    upload(req, res, async (err) => {
        if(err) {
            console.log('图片上传失败', err);
        } else {
            // console.log('图片上传成功', req.files);
            // 拼接图片地址
            const avatar = 'http://localhost:3000/images/' + req.files[0].filename;
            // 获取到其他的常规数据
            // console.log(req.body);
            const data = await addUsers({ ...req.body, avatar });
            res.send(data);
        }
    })
});
module.exports = router;

数据处理好后,按照之前的方式,把数据对象传递给 controllers 文件,最后新增到数据库。

12 -《身份认证》

在实际开发中,实现身份认证主要有两种方式:

  • token + localStorage

  • session + cookie

一、身份认证流程

  1. 当后端判断用户登录成功后,后端服务器中需要生成一个 token(令牌),返回给前端;

  2. 前端接收到后端返回的 token 后,保存到 localStorage。

  3. 后续在需要权限的页面中,发送的任何 AJAX 请求,都需要携带 token 到后端进行验证;

二、后端生成 token

Express 的后端项目中,我们需要借助 jsonwebtoken 插件来实现 token 的生成。

1、下载插件

在后端项目根目录执行以下命令下载插件:

npm i jsonwebtoken

2、引入插件

在需要生成 token 的 .js 文件中,引入插件:(后台controller中)

const jwt = require('jsonwebtoken');

3、生成并返回 token

判断用户登录成功后,调用 sign() 方法生成 token,并将 token 返回给前端:

module.exports.login = async (params) => {
    const data = await usersModel.find(params);
    if(data.length === 1) {
        // 生成 token
        const token = jwt.sign(
            { user: data[0] },   // 保存的用户信息
            'hello',             // 密钥,任意字符串
            { expiresIn: 10 }    // 设置 token 的有效期,数字默认单位为秒(字符串必须带单位)
        );
        return {
            code: 1,
            message: '登录成功',
            token: 'Bearer ' + token  // 返回 token 到前端
        }
    }
    return {
        code: 0,
        message: '登录失败'
    }
}

4、前端保存 token

// 登录
$('#loginBtn').click(async function () {
    const account = $('#account').val();
    const password = $('#password').val();
    const res = await loginAsync({ account, password });
    if (res.code) {
        alert('登录成功');
        localStorage.token = res.token;   // 保存 token
        location.href = './home.html';
    } else {
        alert('登录失败');
    }
})

三、前端发送 token

在需要登录权限的页面中,发送的所有 AJAX 请求都需要将 token 携带在请求头中,然后一并发送到后端进行验证。

1、统一处理 AJAX

为了一次性给所有的 AJAX 的请求头都添加上 token,我们在前端项目的 src 目录中创建一个 utils/ajaxUtils.js 文件,专门用来处理所有 AJAX 的请求头:

$.ajaxSetup({
    // 统一处理所有的 AJAX 的请求头
    headers: {
        Authorization: localStorage.token
    },
    // 统一处理所有的 AJAX 的错误情况
    error(err) {
        if (err.status === 401) {
             alert('请重新登录');
            location.href = './login.html';
        }
    }
})

然后在首页的 home.js 中引入该文件:

import '../sass/home.scss';
import '../sass/menu.scss';
// 引入 AJAX 的处理文件
import '../utils/ajaxUtils.js';
import './menu.js';
import './movies.js';
import './types.js';
import './cinemas.js';
import './theaters.js';
import './users.js';

2、浏览器查看请求头

处理完成后,后续首页发送的所有请求,我们都可以在浏览器的 Network 里查看到请求头中的 token:

四、后端验证 token

1、下载验证插件

npm i express-jwt

2、配置验证

我们在后端项目根目录中创建一个 utils/jwtAuth.js 文件,然后进行如下配置:

const { expressjwt } = require('express-jwt');
module.exports = expressjwt({
    secret: 'hello',  // 密钥(跟生成 token 时的密钥一致)
    algorithms: ['HS256'],   // 设置 JWT 的算法为 HS256(固定)
    // 首先,只要请求头中携带了 token,jwt 都会进行验证,credentialsRequired 主要是针对没有携带 token 的请求。
    // false:如果请求头中没有 token,则不进行任何验证,直接通过;
    // true:如果请求头中没有 token,依然要进行验证,直接不通过;
    credentialsRequired: true
}).unless({
    // 用来设置不需要验证的请求的 URL
    path: [
        '/users/login', 
        '/users/register', 
        // /\/images/
    ]
})

最后,在 app.js 中引入配置文件,并应用配置:

const { expressjwt } = require('express-jwt');
module.exports = expressjwt({
    secret: 'hello',  // 密钥(跟生成 token 时的密钥一致)
    algorithms: ['HS256'],   // 设置 JWT 的算法为 HS256(固定)
    // 首先,只要请求头中携带了 token,jwt 都会进行验证,credentialsRequired 主要是针对没有携带 token 的请求。
    // false:如果请求头中没有 token,则不进行任何验证,直接通过;
    // true:如果请求头中没有 token,依然要进行验证,直接不通过;
    credentialsRequired: true
}).unless({
    // 用来设置不需要验证的请求的 URL
    path: [
        '/users/login', 
        '/users/register', 
        // /\/images/
    ]
})

五、获取用户信息

通常在进入系统首页时,都需要显示用户信息。

所以,我们会在已进入页面时就发送一个 AJAX 请求来获取用户信息。

// 在前端usersAPI.js
export function getUserInfoAsync() {
    return new Promise((resolve) => {
        $.ajax({
            url: '/api/users/getUserInfo',
            success(res) {
                resolve(res);
            }
        })
    })
}
//  在前端js/home.js文件里面引入init.js 后,前端 js文件中的init.js,
import { getUserInfoAsync } from '../api/usersAPI.js'
getUserInfo();
// 获取用户信息
async function getUserInfo() {
    const res = await getUserInfoAsync();
    if(res.code) {
        $('#username').text(res.data.name);
    }
}
//home.html中加上
 <header>
 	欢迎你,<p id="username"></p>
  </header>

后端解码 token

const jwt = require('jsonwebtoken');
router.get('/getUserInfo', async function(req, res) {
    // 获取请求头的 token
    const tokenBearer = req.get('Authorization');
    const token = tokenBearer.split(" ")[1];
    // 对 token 解码,获取 token 中的用户信息
    const { user } = jwt.verify(token, 'hello');
    res.send({
        code: 1,
        message: '用户信息获取成功',
        data: user
    })
})

13 -《RESTful》

RESTful,一种前后端接口命名的规范。

在 RESTful 之前,我们的接口通常是由两部分组成:

  • 一级接口:表示要请求的资源(数据)名称

  • 二级接口:表示对资源进行的操作

在 RESTful 中,要求接口只有一个部分,用来表示要请求的资源(数据)名称。

例如,所有关于电影的请求,接口都是 /movies

但是,我们为了区别对同一个集合数据的不同操作,RESTful 中又引入了两种新的请求类型:PUT、DELETE。

一、请求类型

RESTful 中,常用的请求类型主要有以下四个:

  • GET:获取数据

  • POST:新增数据

  • PUT:修改数据

  • DELETE:删除数据

例如我们要对电影数据进行 CRUD 操作,接口参考如下:

  • 获取电影数据:GET /movies

  • 新增电影数据:POST /movies

  • 修改电影数据:PUT /movies

  • 删除电影数据:DELETE /movies

$.ajax({
    url: '/movies',
    type: 'DELETE',
    data: { _id: '1' },
    success(res) {
    }
})

二、请求参数中有 id

在 RESTful 中,如果请求的参数中有唯一值 id,我们可以将 id 通过 url 来进行发送。

例如,我们要根据电影 id 来获取电影:

$.ajax({
    url: '/movies/1',
    type: 'GET',
    success(res) {
    }
})

Express 后端在接收参数时,就需要在路径中定义变量名来接收 URL 中的参数:

// 根据 _id 获取电影数据
router.get('/:_id', async function(req, res) {
    const params = req.params;   // { _id: 1 }
    // ...
})

14 -《跨域》

一、域(源)

一个域,应该是由三部分组成:

  1. 协议:http/https

  2. IP:每一台计算机都应该有一个唯一的 IP 地址,例如:localhost、192.168.1.100

  3. 端口:计算机中安装或启动的每一个应用程序,都应该有一个唯一的端口号。例如:3000、8080…

例如我们课堂项目影院管理系统所在的域是:http://localhost:8080,而影院的后端服务器所在的域是:http://localhost:3000

二、同源策略

在浏览器中,处于安全考虑,要求我们只能在相同的域之间进行访问。意思是:在一个页面中发送请求时,当前页面的域的协议、IP、端口,和页面中发送的请求路径的协议、IP、端口,三者必须一致。只要有一个不一样,则为“不同源”,不同源,就属于跨域访问。

这个就是浏览器的“同源策略”。默认情况下,浏览器不支持跨域访问。

三、跨域的概念

简单来说,就是当我们从一个域向一个域发送请求,但是两个域之间协议、IP、端口三者有一个不一致,则形成了跨域。

四、跨域的解决方案

跨域的解决方案分为以下几种:

  1. 通过 webpack 配置开发服务器的 proxy(只能用于开发环境);

  2. JSONP:只支持 GET 请求;

  3. CORS

  4. 代理服务器

JSONP

前端发送 AJAX 请求时,添加 dataType 属性:

$.ajax({
    url: 'http://localhost:8080/movies',
    type: 'GET',
    dataType: 'jsonp',
    data,
    success(res) {
        resolve(res);
    }
})

后端在返回结果给前端时,res.send() 改为 res.jsonp()

router.get('/', async function (req, res, next) {
    const data = await getMovies(req.query);
    res.jsonp(data);
});

CORS

在后端中配置以下代码即可:

// CORS 配置跨域
var allowCrossDomain = function (req, res, next) {
    // 设置允许跨域访问的请求源(* 表示接受任意域名的请求)   
    res.header("Access-Control-Allow-Origin", "*");
    // 设置允许跨域访问的请求头
    res.header("Access-Control-Allow-Headers", "X-Requested-With,Origin,Content-Type,Accept,Authorization");
    // 设置允许跨域访问的请求类型
    res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
    // 同意 cookie 发送到服务器(如果要发送cookie,Access-Control-Allow-Origin 不能设置为星号)
    res.header('Access-Control-Allow-Credentials', 'true');
    next();
};
app.use(allowCrossDomain); 
// 后续开始配置 token、一级接口等

五、跨域相关面试题

  1. JSONP 的原理。

因为 script、link、img 等标签天生就支持跨域,JSONP 的原理就是利用 script 来发送跨域请求。

15 -《一次完整的 HTTP 事务》

当用户在浏览器的地址栏中输入 URL 后,发生了什么事情?

  1. 域名解析:将域名解析成对应的 IP 地址;

  2. 建立连接:通过 TCP 协议进行三次握手,来建立前后端之间的连接;

  3. 前端发送请求

  4. 后端处理请求:将前端请求的数据返回给浏览器;

  5. 断开连接:通过 TCP 协议进行四次挥手,来断开前后端之间的连接;

  6. 浏览器渲染页面

16 -《Git 基本使用》

Git,版本控制系统。

一、工作流程

二、工作步骤

1、初始化

配置账号和邮箱。

在 Git 终端的任意路径中,执行以下命令:

git config --global user.name "xuhao"
git config --global user.email "654240464@qq.com"

2、创建远程仓库

在码云中注册账号,然后创建远程仓库。

成员管理

远程仓库创建完成后,进入仓库的管理界面,邀请添加项目成员。

3、克隆远程仓库

复制远程仓库地址:

然后打开 Git 终端,定位到想要存放项目的目录路径,然后执行以下命令,将远程仓库克隆到本地:

git clone https://gitee.com/ihavenocat/snail-test.git

4、配置忽略项

在仓库根目录中创建一个 .gitignore 文件,在该文件中添加需要忽略的文件或文件夹:

node_modules
package-lock.json

5、常规操作流程

1)将工作区的代码添加到暂存区

git add .

2)将暂存区的代码提交到本地仓库

git commit -m "提交记录描述"

3)将本地仓库的代码推送到远程仓库

git push

最新:

1.gitee创建远程仓库

2.git init

3.git clone https://gitee.com/ihavenocat/snail-test.git

17 -《Git 分支操作》

一、新建子分支

通常,我们将远程仓库中的 master (主分支)代码克隆到本地后,就可以在 master 的基础上新建自己的本地子分支。

git checkout -b jianglan

二、切换分支

查看可切换的分支

git branch
 git branch -a

!!!!注意:在切换分支前,确保当前分支的代码已经进行了 add 和 commit 的操作。

git checkout master

三、合并分支

首先,将 Git 终端定位到目标分支。

例如,我们想要将自己的子分支代码合并到 master,那么我们需要切换到 master 中去:

git merge jianglan

通常,当我们将自己子分支的代码合并到主分支后,都需要将本地主分支的代码推送(上传)到远程仓库。

!!!!但是,我们在团队开发中,每次推送自己的代码时,先拉取(下载)远程仓库中最新的代码。

四、拉取远程仓库最新代码

拉取远程代码,都是拉取远程 master 主分支的代码。

所以我们在拉取时,先将本地的分支切换到 master 中,然后再拉取远程 master 的代码:

git pull

18 -《Git 使用流程》

一、创建远程仓库

一个小组创建一个远程仓库就可以。

1、仓库成员管理

给小组成员设置仓库权限。

二、项目初始化

可以先让一个同学将远程仓库克隆到本地,然后在本地仓库创建项目。

同时将所有可能用到的依赖包都进行下载,最后,配置 .gitignore 忽略文件。

项目初始完成后,将项目上传到远程仓库的 master:

git add .
git commit -m '项目初始化'
git push

三、克隆远程仓库

小组内其他成员开始克隆远程仓库到本地。然后在 VSCode 的终端中执行以下命令,下载项目中所有需要用到的依赖包:

npm i

小组内所有成员都新建自己的分支:

git checkout -b 自己的子分支名

四、编写代码

建议每隔一段时间在子分支中对自己的代码做一个保存:

 git add .
 git commit -m '相关功能描述'

五、合并代码

当我们一个功能写完了,想要将这个代码共享给组员。

1、本地合并

在本地子分支中代码编写完成后,先将本地子分支的代码合并到本地主分支:

 git checkout master
 git merge 自己的子分支名

这一步完成后,本地的 master 中就应该有我们自己的全部代码了。

注意:合并完成后运行代码查看是否有问题,有问题就解决冲突,然后再 add 和 commit。

2、拉取远程代码

在本地 master 中,执行以下命令,拉取远程的 master 代码(拉取组内其他成员的代码):

git pull

!!!注意:拉取完成后,也需要运行代码查看是否有问题,有问题就解决冲突,然后再 add 和 commit。

3、推送本地代码

确认本地 master 中,自己的代码和其他组员的代码没有冲突后,执行以下命令将本地合并好的代码,上传到远程仓库:

git push

六、继续编写代码

回到自己的本地子分支中:

git checkout 自己的子分支名

然后将本地 master 的最新代码合并到本地的子分支中:

git merge master

注意:合并完成后运行代码查看是否有问题,有问题就解决冲突,然后再 add 和 commit。

后续,就重复四、五、六的步骤。

18-1项目开发在vscode里面拉取,推送代码

1.在gitee上项目里新建一个自己的分支 例如新建一个自己的分支apple

2.在本地新建文件夹拉取代码 命令:

拉取自己分支名称下的代码:

git clone -b apple gitee地址

默认不加-b命令的话拉取的是master分支下的代码:

git clone gitee地址

3.开发过程 拉取完就可以进行代码开发了

4.提交代码 在Vscode中的左侧菜单栏中的第三个就是git管理工具。 在这里插入图片描述

1.将修改过的代码添加到暂存区 git add . 点击+号

在这里插入图片描述

2.提交代码到本地仓库 git commit -m ‘提交注释信息’

图中的消息就是注释信息。填完后选择提交即可。

在这里插入图片描述

3.提交代码之前拉取别的分支的代码

在这里插入图片描述

在这里插入图片描述

选择origin之后再选择你想要拉取的分支即可。

4.推送代码 提交完成后等待一会,在Vscode的底部栏的第二个选项可以看到文件等待上传,点击一下圆圈选择确定即可推送成功。第一个选项代表着当前分支。

19-新项目配置前端环境

一、新建文件夹

新建文件夹(英文名),用VSCode打开文件夹,在项目根目录中,点击在集成终端中打开

二.项目初始化

输入以下代码,项目初始化

npm init -y

在生成的package.json文件中,用以下代码,进行覆盖

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "webpack-dev-server --hot --config webpack.dev.config.js --mode development",
    "build": "webpack --config webpack.prod.config.js --mode production"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "clean-webpack-plugin": "^4.0.0",
    "copy-webpack-plugin": "^10.2.4",
    "css-loader": "^6.7.1",
    "css-minimizer-webpack-plugin": "^3.4.1",
    "html-webpack-plugin": "^5.5.0",
    "mini-css-extract-plugin": "^2.6.0",
    "node-sass": "^7.0.1",
    "sass-loader": "^12.6.0",
    "terser-webpack-plugin": "^5.3.1",
    "webpack": "^5.72.0",
    "webpack-cli": "^4.9.2",
    "webpack-dev-server": "^4.8.1",
    "webpack-merge": "^5.8.0"
  }

输入以下代码,下载package.json文件中所有的依赖包

npm i

三、创建三个文件

根目录中创建webpack.base.config.js文件,输入以下代码

// 公共配置
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
    entry: {
        home: './src/js/home.js',
        login: './src/js/login.js',
        register: './src/js/register.js',
    },
    output: {
        // 配置出口文件的保存路径:项目根目录的 dist
        path: path.resolve(__dirname, 'dist'),
        // 出口文件的名称
        filename: 'js/[name].js'
    },
    module: {
        rules: [
            // 配置 SASS
            {
                test: /\.scss$/i,           // 需要编译以 .scss 为结尾的文件
                exclude: /node_modules/,    // 不需要解析 node_modules
                use: [
                    MiniCssExtractPlugin.loader,
                    'css-loader',
                    'sass-loader'
                ]
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: './src/home.html',   // 源文件路径
            filename: 'home.html',         // 打包后的文件名
            chunks: ['home']               // 入口文件的名称(当前 HTML 中需要引入的 JS 文件名)
        }),
        new HtmlWebpackPlugin({
            template: './src/login.html',   // 源文件路径
            filename: 'login.html',         // 打包后的文件名
            chunks: ['login']               // 入口文件的名称
        }),
        new HtmlWebpackPlugin({
            template: './src/register.html',   // 源文件路径
            filename: 'register.html',         // 打包后的文件名
            chunks: ['register']               // 入口文件的名称
        }),
        new CopyWebpackPlugin({
            patterns: [
                { from: './src/static', to: './static' }
            ]
        }),
        new MiniCssExtractPlugin({
            filename: 'css/[name].css'
        })
    ],
    performance: {
        hints: false,
    },
}

根目录中创建webpack.dev.config.js文件,输入以下代码

// 开发环境配置

// 引入公共配置
const base = require('./webpack.base.config.js');
// 用于合并公共配置和当前环境配置
const { merge } = require('webpack-merge');

module.exports = merge(base, {
    // 开发环境自己的配置
    mode: 'development',
    devtool: 'inline-source-map',
    // 前端的开发服务器
    devServer: {
        port: '9080', // 端口号
        open: 'home.html',

        // 配置跨域:就是为了让前端的 8080 可以访问后端的 3000
        proxy: {
            // 前端项目中所有以 /api 开头的请求,都会进行以下配置:
            "/api": {
                target: "http://localhost:3000",   // 将请求转发到 3000 的服务器
                pathRewrite: {
                    "^/api": "",                   // 将前端所有以 /api 开头的请求,转发到服务器后,都重写为空
                }
            }
        }
    }
})

根目录中创建webpack.prod.config.js文件,输入以下代码

// 生产环境配置

const base = require('./webpack.base.config.js');
const { merge } = require('webpack-merge');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const TerserWebpackPlugin = require('terser-webpack-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')

module.exports = merge(base, {
    // 生产环境自己的配置
    mode: 'production',
    plugins: [
        new CleanWebpackPlugin()
    ],
    // 生产环境优化
    optimization: {
        minimize: true,
        minimizer: [
            new TerserWebpackPlugin(),
            new CssMinimizerPlugin()
        ]
    }
})

如果报错,有可能是8080被占用,可改端口号解决。

四、创建src文件,开始写代码

20-配置后端环境

一、新建文件夹

新建文件夹(英文名),用VSCode打开文件夹,在项目根目录中,点击在集成终端中打开

二、创建 Express 项目

将终端的路径定位到项目的根目录,然后执行以下命令:

npx express-generator

该命令执行完成后,会自动在项目的根目录中生成 Express 服务端项目的基本结构。

三、下载项目所有依赖包

将终端的路径定位到项目的根目录,执行以下命令,根据 package.json 文件下载项目需要的所有依赖包:

先在package.json文件覆盖以下代码

 "scripts": {
    "start": "nodemon ./bin/www"
  },
  "dependencies": {
    "cookie-parser": "~1.4.4",
    "debug": "~2.6.9",
    "express": "~4.16.1",
    "express-jwt": "^7.7.0",
    "http-errors": "~1.6.3",
    "jade": "~1.11.0",
    "jsonwebtoken": "^8.5.1",
    "mongoose": "^6.3.1",
    "morgan": "~1.9.1",
    "multer": "^1.4.4"
  }
npm i

四、app.js中覆盖以下代码

 // 通过 npm 下载的第三方插件,以及 Nodejs 自带的插件,在引入时只需要在 require 中写插件名即可。
 var createError = require('http-errors');
 var express = require('express');
 var path = require('path');
 var cookieParser = require('cookie-parser');
 var logger = require('morgan');
 const jwtAuth = require('./utils/jwtAuth.js');
 ​
 // 引入自己创建的文件时,需要通过相对路径去进行查找(.js 后缀名可以省略不写)
 ​
 // 引入mongoose的配置文件,连接 express 和 mongodb
 require('./dao/db.js');
 ​
 // 需要我们自己根据需求编写的(开始) --------------------------------
 var indexRouter = require('./routes/index');
 var usersRouter = require('./routes/users');
 // 需要我们自己根据需求编写的(结束) --------------------------------
 ​
 ​
 var app = express();
 ​
 // view engine setup
 app.set('views', path.join(__dirname, 'views'));
 app.set('view engine', 'jade');
 ​
 app.use(logger('dev'));
 app.use(express.json());
 app.use(express.urlencoded({ extended: false }));
 app.use(cookieParser());
 app.use(express.static(path.join(__dirname, 'public')));
 ​
 // 需要我们自己根据需求编写的(开始)--------------------------------
 ​
 // 让 token 的验证功能生效
 app.use(jwtAuth);
 ​
 // 配置前后端接口的一级路径
 app.use('/', indexRouter);
 app.use('/users', usersRouter);
 // 需要我们自己根据需求编写的(结束) --------------------------------
 ​
 ​
 // catch 404 and forward to error handler
 app.use(function(req, res, next) {
   next(createError(404));
 });
 ​
 // error handler
 app.use(function(err, req, res, next) {
   // set locals, only providing error in development
   res.locals.message = err.message;
   res.locals.error = req.app.get('env') === 'development' ? err : {};
 ​
   // render the error page
   res.status(err.status || 500);
   res.render('error');
 });
 ​
 ​
 module.exports = app;
 ​

五、添加utilsdaocontrollers文件夹

如何查看本机IP地址?

查机IP地址法如下:1、点击电脑开—运行——cmd

2、点击确定后就进入了dos界面。

3、输入查看本机ip地址命令 ipconfig /。

4、输入查看本机ip地址命令ipconfig /all 显示的就是本机的详细信息。

5、还有一种通过百度来查看本机ip地址的简单方法,首先打开百度。

6、在搜索框中输入本机ip地址点搜索。

7、在搜索结果中就可以看到所在网络的ip地址了。 电脑的ip地址怎么查你是查网络IP还是IP网络IP在电脑的浏览器上面输入“IP”就示网络IP电脑IP:点击快带链接小图标——在打开网络共享中心 点击更改适配器设置 点击右键点击本地链接——选择状态点击详情信息可看见电脑IP地址 查找本机IP地址方法:1、打开网络共享中心2、查看本地连接属性3、本地连接状态——常规——详细信息4、详细信息里显示本机IP地址

数据处理

一、将1000000变为1,000,000

 (number+ '').replace(/(\d{1,3})(?=(\d{3})+(?:$|\.))/g,'$1,')
 // number为数字

二、保留两位小数

 number.toFixed(2)
 // number为数字

三、获取最近count天的日期

 /**
  * 获取最近count天的日期
  * @param count 天数
  * @returns {*[]} 两个时间点
  */
 export function getLateDate(count) {
   let time1 = new Date();
   time1.setTime(time1.getTime() - 24 * 60 * 60 * 1000);
   let Y1 = time1.getFullYear();
   let M1 =
     time1.getMonth() + 1 > 9
       ? time1.getMonth() + 1
       : "0" + (time1.getMonth() + 1);
   let D1 = time1.getDate() > 9 ? time1.getDate() : "0" + time1.getDate();
   let timer1 = Y1 + "-" + M1 + "-" + D1; // 当前时间
   let time2 = new Date();
   time2.setTime(time2.getTime() - 24 * 60 * 60 * 1000 * count);
   let Y2 = time2.getFullYear();
   let M2 =
     time2.getMonth() + 1 > 9
       ? time2.getMonth() + 1
       : "0" + (time2.getMonth() + 1);
   let D2 = time2.getDate() > 9 ? time2.getDate() : "0" + time2.getDate();
   let timer2 = Y2 + "-" + M2 + "-" + D2; // 之前的7天或者30天
   return [timer1, timer2];
 }
 ​
 /**
  * 获取最近count天到count+count的日期
  * @param count 天数
  * @returns {*[]} 两个时间点
  */
 export function getLateDateLast(count) {
   let time1 = new Date();
   time1.setTime(time1.getTime() - 24 * 60 * 60 * 1000 * (count+1));
   let Y1 = time1.getFullYear();
   let M1 =
     time1.getMonth() + 1 > 9
       ? time1.getMonth() + 1
       : "0" + (time1.getMonth() + 1);
   let D1 = time1.getDate() > 9 ? time1.getDate() : "0" + time1.getDate();
   let timer1 = Y1 + "-" + M1 + "-" + D1; // 之前的7天或者30天
 ​
   let time2 = new Date();
   time2.setTime(time2.getTime() - 24 * 60 * 60 * 1000 * count*2);
   let Y2 = time2.getFullYear();
   let M2 =
     time2.getMonth() + 1 > 9
       ? time2.getMonth() + 1
       : "0" + (time2.getMonth() + 1);
   let D2 = time2.getDate() > 9 ? time2.getDate() : "0" + time2.getDate();
   let timer2 = Y2 + "-" + M2 + "-" + D2; // 之前的7天或者30天
   return [timer1, timer2];
 }

AES加密+md5加密

一、引入srypto-js.js和md5.js文件(文件参考AH-MemberSystem/js/common/crypto-js.js和md5.js)

  <script src="./js/common/crypto-js.js" type="text/javascript" charset="utf-8"></script>
  <script src="js/common/md5.js" type="text/javascript"></script>

二、定义方法

 //广电鉴权AES加密base64
 function aesEncryptBase64 (data, key, iv) {
   var key = CryptoJS.enc.Utf8.parse(key);
   var iv = CryptoJS.enc.Utf8.parse(iv);
   var data = CryptoJS.enc.Utf8.parse(data);
   var encrypted = CryptoJS.AES.encrypt(data, key, {
     iv: iv,
     mode: CryptoJS.mode.CBC,
     padding: CryptoJS.pad.Pkcs7
   });
   var oldHexStr = CryptoJS.enc.Hex.parse(encrypted.ciphertext.toString().toUpperCase());
   return CryptoJS.enc.Base64.stringify(oldHexStr);
 }
 ​

三、调用方法

 var aesData = {"transactionID":"00000001202005151022290000006571","platformID":"00000002","opFlag":"3","epgFlag":"1","customerType":"0","userAccount":"test20220423","userToken":"777777yyyy09Mo0242270b4e3f1d1765f82db","contentID":"content000000000000000012354","categoryID":"","serviceID":"","productID":"packageIDa0000000000000000000010,packageIDa0000000000000000000011","timeStamp":"1631929181808","deviceID":"","ip":"","mac":""}
 var aesStr = JSON.stringify(aesData) 
 var aesRes = aesEncryptBase64(aesStr,'ahbcforbytuetech','ahbcforbytuetech')
 var md5Data = hex_md5(aesRes)
  console.log('加密后',aesRes);
  console.log('md5',md5Data);

四、对比网站结果

*加密方法*

1.1 *调用接口参数加密*

AES加密模式: CBC

填充:pkcs7padding

数据块:128位

密码: ahbcforbytuetech

偏移量:ahbcforbytuetech

输出:base64

字符集:utf8

AES : http://tool.chacuo.net/cryptaes

MD5 : MD5在线加密 - MD5加密工具 - MD5在线生成

1.2 *请求接口示例*

明文

{"transactionID":"00000001202005151022290000006571","platformID": "00000002","opFlag":"3","epgFlag":"1","customerType": "0","userAccount":"test20220423","userToken": "777777yyyy09Mo0242270b4e3f1d1765f82db","contentID": "content000000000000000012354","categoryID":"","serviceID": "","productID":"packageIDa0000000000000000000010,packageIDa0000000000000000000011","timeStamp":"1631929181808","deviceID":"","ip": "","mac": ""}

加密后

{"ENCRYPT":"37O4WRcP+b8qcfl77XySz6TNBji0ifmc5jSQoOzsnrv1AnQj2xqziiYEWCLTpC87USrMkemeJ4x+n4AMh5E0RDoywBKbzQYKddvaeuY3Nk4B5w7nwqXriEpjLVGDMfJsKl6Cauek85vcnOTNOxjn/ndhRNjH/5myUBE7rzJ0t9Cf4DgGmgulVSqKg/1iiiygPpIt6XwzG1N0JMuVMPU/FgjJ83M8cjFBT9wixd36d1eeMXXQnhpbMytLQudRrcLzFCtkNtftqGUJAHeD+TFBOvi9f0/j2sleus0PF0FQDaULxywK8om6GHXrc0DppfVaFkx5SiaZzFrkC6Lzal0lwIxY/3h1FPJnpxybGYB+pxywOhUFTWBcf9W45p9HjJDDGtylfNBDBOAPgezk+WtqJ2egg8px6CmlNRY1E8/Gx7GngJPxLfQeOXV/nZclopsi/ymnoPH2E87sT+4fDubj6IumewofCF4CYBAQCZNsQOERUf1aAqzd+EZVcuvpKVEAiImWSSviP7biPIGBDIRTqRGQFVVIHuDDm+3bbvfUUed0Trm0jgq3RaWkknO71De3hU6TqYD4N5HG2rH4RsZinWOLsBlZPslpWxqXkWdh9nc=","SUMMARY":"157668573168766865cd94e84e88b92d"}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值