JavaScript 基础入门

JavaScript 基础入门


简介

前端三层:

语言功能
结构层HTML搭建结构、放置部件、描述语义
样式层CSS美化页面、实现布局
行为层JavaScript实现交互效果、数据收发、表单验证等

ECMAScript是JavaScript的标准:

  • 1997年,欧洲计算机制造商协会(ECMA)设置了JavaScript的标准,命名为 ECMAScript

一、JS 基础语法

1、JS 语法与变量

(1)JavaScript 的书写位置

  • 在 < body >的< script >标签中,在内部书写JavaScript代码
  • 将代码单独保存为 .js 格式文件,然后在 HTML 文件中使用 < script src="" > 这样的方式引入它

(2)认识输入输出语句

  • prompt() 语句     - 弹出输入框
  • alert() 语句       - 弹出警告框
    alert() 是内置函数,函数就是功能的“封装”,调用函数需要使用圆括号
  • console.log() 语句    - 控制台输出
    console是JS的内置对象,通过“打点”可以调用它内置的 log “方法”,所谓方法就是对象能够调用的函数
  • 语句末尾应该书写英文状态下的分号

(3)学会处理报错

  • Uncaught SyntaxError: Invalid or unexpected token (未捕获的语法错误:不合法或错误的符号)
  • Uncaught ReferenceError: xxx is not defined (未捕获的引用错误:xxx 没有被定义)

REPL(Read、Eval、Print、Loop)环境:

  • 浏览器控制台是一个 REPL 环境,可以使用它临时测试表达式的值
  • read:读
  • eval:执行
  • print:打印
  • loop:循环

(4)变量

  • 变量是计算机语言中能存储计算结果或能表示值的抽象概念
  • 变量不是数值本身,它们仅仅是一个用于存储数值的容器

标识符的命名规则:

  • 只能由字母、数字、下划线、$符组成,但不能以数字开头
  • 不能是关键字或保留字
  • 变量名大小写敏感,a和A是两个不同的变量
// 合法命名举例
str aBc_123 $2 $0o_o0$ $
// 非法命名举例
2a aBc#123 ¥2 true

优秀的变量命名方法:

  • 驼峰命名法:mathScore 不建议的命名:mathscore
  • c风格:math_test_score

定义变量:

  • 要想使用变量,第一步就是声明它,并给它赋值
  • 使用逗号同时声明和初始化多个变量
// 使用 var 关键字定义变量
// = 表示赋值,将右边的数值赋值给左边的变量
var a = 5;
// 使用逗号同时声明和初始化多个变量
var b = 2, c = 3, d = 4;

使用变量:

  • 当变量被赋值后,就可以使用它
// 变量使用时不能加引号,加引号就变成字符串了
console.log(a);

改变变量的值:

  • 变量的值可以被改变,当改变变量值时不再需要写关键字 var 了
var a = 10;
a = 12;  // 更改变量a的值为12

变量的默认值:

  • 一个变量只定义,但没有赋初值,默认值是 undefined
  • 一个变量只有被 var 定义,并赋初值之后,才能算正式初始化完成

变量定义的常见错误:

  • 不用 var 定义,而直接将值赋予它,虽然不引发报错,但 会产生作用域问题
  • 使用一个既没有被 var 定义过,也没有赋值过的字符,就会产生引用错误

(5)变量声明提升

  • 变量声明提升:可以提前使用一个稍后才声明的变量,而不会引发异常
  • 在执行所有代码前,JS 有预解析阶段,会预读所有变量的定义
  • 变量声明提升只提升定义,不提升值
console.log(a);  // 先使用变量,由于不提升值,所有输出 undefined
var a = 12;  // 后定义变量

2、JS 基本数据类型

(1)数据类型简介和检测

JS 中两大类数据类型:

  • 基本数据类型:Number、String、Boolean、Undefined、Null
  • 复杂数据类型:Object、Array、Function、RegExp、Date、Map、Set、Symbol等等

typeof 运算符:

  • 使用 typeof 运算符可以检测 值或变量 的类型
var a = 12;
console.log(typeof a);  // number

5种基本数据类型的 typeof 检测结果:

类型名typeof 检测结果
数字类型number
字符串类型string
布尔类型boolean
undefined类型undefined
null类型object

(2)Number(数字)类型

  • 所有数字不分大小、不分整浮、不分正负,都是数字类型
  • 小数中0可以省略

科学计数法:

  • 较大或较小数(绝对值较小)可以写成科学计数法形式
3e8;  // 300000000
3e-4;  // 0.0003
-3e4;  // -30000
.3e5;  // 30000

不同进制的数字:

  • 二进制数值以0b开头,例:0b11
  • 八进制数值以0开头,例:017
  • 十六进制数值以0x开头,例:0xf

一个特殊的数字型值 NaN:

  • NaN是英语“not a number”的意思,即“不是一个数”,但它 是一个数字类型的值
typeof NaN;  // number
  • 0 除以 0 的结果是 NaN,在数学运算中,若结果不能得到数字,其结果往往都是 NaN
  • NaN 有一个“奇怪”的性质:不自等
console.log(NaN == NaN);  // false

(3)String(字符串)类型

  • 字符串要用引号包裹,双引号或者单引号均可
  • 数字11和字符串’‘11’'在语义上是不同的,前者表达一个数量,后者是一个文本
typeof 11;  //number
typeof '11';  // string
  • 加号可以用来拼接多个字符串
'hello' + ' ' + 'world';  //hello world
  • 字符串可以和变量进行拼接
var a = 10;
var str = '我有' + a + '元钱';
  • 一些时候需要使用空字符串,直接书写闭合的引号对即可
var str = '';
  • 字符串的 length 属性表示字符串的长度
'JS'.length   // 2
''.length     // 0

字符串的常用方法:

  • “方法”就是能够打点调用的函数,字符串有丰富的方法
方法功能
charAt()得到指定位置字符
substring()提取字符串
substr()提取字符串
slice()提取字符串
toUpperCase()将字符串变为大写
toLowerCase()将字符串变为小写
indexOf()检索字符串
  • charAt() 方法可以得到指定位置的字符,当指定的位置超出字符串长度,则返回空字符串
'我喜欢JS'.charAt(0)   // '我'
'我喜欢JS'.charAt(10)   // ''
  • substring(a, b) 方法得到从 a 开始到 b 结束(不包括b处)的子字符串
  • substring(a, b) 方法如果省略第二个参数,返回的子串会一直到字符串的结尾
  • substring(a, b) 中,a 可以大于 b,数字顺序将自动调整为小数在前
'我喜欢JS'.substring(1,3)   // '喜欢'
'我喜欢JS'.charAt(2)        // '欢JS'
'我喜欢JS'.substring(3,1)   // '喜欢'
  • substr(a, b) 中,将得到从 a 开始的长度为 b 的子字符串
  • substr(a, b) 中,b 可以省略,表示到字符串结尾
  • substr(a, b) 中,a 可以是负数,表示倒数位置,最后一位位置是-1
'我喜欢JavaScript'.substr(2,3)     // '欢Ja'
'我喜欢JavaScript'.substr(3)       // ' JavaScript'
'我喜欢JavaScript'.substr(-5,4)    // 'crip'
  • slice(a, b) 方法得到从 a 开始到 b 结束(不包括 b 处)的子字符串
  • slice(a, b) 的参数 a 可以是负数
  • slice(a, b) 中,参数 a 必须小于参数 b;如果大于则没有结果,返回空字符串
'我喜欢JavaScript'.slice(2,3)      // '欢Ja'
'我喜欢JavaScript'.slice(-6,-1)    // ' Scrip'
'我喜欢JavaScript'.slice(5,4)      // ''
  • toUpperCase() 将字符串全转为大写
  • toLowerCase() 将字符串全转为小写
'i like js'.toUpperCase()     // 'I LIKE JS'
'HELLO'.toLowerCase()         // 'hello'
  • indexOf() 方法返回某个指定的字符串值在字符串中首次出现的位置
  • 如果要检索的字符串值没有出现,则返回 -1
'abcdefg'.indexOf('b')     // 1
'abcdefg'.indexOf('ef')    // 4
'abcdefg'.indexOf('h')     // -1

(4)Boolean(布尔)类型

  • 布尔型值只有两个:true 和 false
3 < 5   // true
5 < 3   // false

(5)Undefined 类型

  • 一个没有被赋值的变量的默认值是 undefined,而 undefined 的类型也是 undefined
  • undefined 既是值,又是一种类型,这种类型只有它自己一个值
  • 在变量声明提升时,变量的值是 undefined
typeof undefined;       // undefined

console.log(a);         // undefined
console.log(typeof a);  // undefined
var a = 12;

(6)Null 类型

  • null 表示“空”,它是“空对象”
  • 当我们需要将对象销毁、数组销毁或者删除事件监听时,通常将它们设置为 null
  • 用 typeof 检测 null 结果是 object
  • 类型和 typeof 检测结果并不总是一一对应,比如数组用typeof 检测结果也是object
typeof null;  // object

(7)数据类型转换

其他值 -> 数字(Number)

  • 使用 Number() 函数,可以将其它值转换为数字
  • 纯数字字符串能转变为数字,不是纯数字的字符串中就会转为 NaN
  • 空字符串会转为 0
  • 布尔值转变为数字:true变为1;false变为0
  • undefined 和 null 变为数字:undefined 变为 NaN;null 变为 0
Number('123');     // 123
Number('123.4');   // 123.4
Number('12月');    // NaN
Number('2e3');    // 2000
Number('');       // 0

Number(true);        // 1
Number(false);       // 0

Number(undefined);       // NaN
Number(null);            // 0

使用 parseInt() 函数:

  • parseInt() 函数的功能是将字符串转为整数
  • parseInt() 会自动截掉第一个非数字字符之后的所有字符
  • parseInt() 可以将所有文字截掉
  • 如果字符串不是以数字开头,则转为 NaN
  • parseInt()函数不会四舍五入
parseInt('3.14');          // 3
parseInt('3.14圆周率');     // 3
parseInt('圆周率是3.14');   // NaN
parseInt('3.89');         // 3

使用 parseFloat() 函数:

  • parseFloat() 函数的功能是将字符串转为浮点数
parseFloat('3.14');          // 3.14
parseFloat('3.14圆周率');     // 3.14
parseFloat('圆周率是3.14');   // NaN
parseFloat('3.89');         // 3.89

使用 String() 函数:

  • 使用 String() 函数可以将其它值转变为字符串
  • 数字->字符串:变为“长得相同”的字符串;科学计数法和非十进制数字会转为 十进制的值
  • 布尔->字符串:变为“长的相同”的字符串
  • undefined 和 null -> 字符串:变为“长的相同”的字符串
String(123);             // '123'
String(3.14);            // '3.14'
String(2e3);             // '2000'
String(NaN);             // 'NaN'
String(Infinity);        // 'Infinity'
String(0xf);             // '15'
String(true);            // 'true'
String(false);           // 'false'
String(undefined);       // 'undefined'
String(null);            // 'null'

使用 toString() 方法:

  • 使用 toString() 方法将其它值转为字符串
  • 几乎所有的值都有 toString() 方法,功能是将值转为字符串
  • 数字需要加圆括号才能调用 toString() 方法
(3).toString();     // "3"

使用 Boolean() 函数:

  • 使用 Boolean() 函数可以将其它值转为布尔值
  • 数字->布尔值:0和NaN转为false;其他数字都转为 true
  • 字符串->布尔值:空字符串变为false;其它都转为true
  • undefined 和 null->布尔值:都转为false
Boolean(13);          // true
Boolean(0);           // false
Boolean(NaN);         // false
Boolean(Infinity);    // true
Boolean(-Infinity);   // true
Boolean('');          // false
Boolean('adc');       // true
Boolean('false');     // true
Boolean(undefined);   // false
Boolean(null);        // false

3、表达式与操作符

(1)什么是表达式运算符

(2)算术表达式

  • 加减的符号和数学一致,乘号是 * 号,除号是 / 号
  • 默认情况,乘除的优先级要高于加减;可以用圆括号来改变运算顺序
运算符名称运算符
+
-
*
/
取余%
  • 加号有作“加法”和“连字符”两种作用
  • 如果加号两边的操作数都是数字,则为“加法”,否则为“连字符”
  • 取余运算也叫作“求模运算”,用百分号 % 表示
  • a % b 表示求 a 除以 b 的余数

隐式类型转换:

  • 如果 参与数学运算的某操作数不是数字型,那么JavaScript会自动将此操作数转换为数字型
  • 隐式转换的本质是内部调用 Number() 函数
3 * '4'       // 12
true + true   // 2
false + 1     // 1
3 * '2天'     // NaN

有关IEEE754:(toFixed()方法)

  • 在JavaScript中,有些小数的数学运算不是很精准
0.1 + 0.2   // 0.30000000000000004
  • JavaScript使用了IEEE754二进制浮点数算术标准,这会使一些个别的小数运算产生“丢失精度”问题
  • 解决方法:在进行小数运算时,要调用数字的 toFixed() 方法保留指定的小数位数
Number((0.1 + 0.2).toFixed(2));  // 0.3

幂和开根号:

  • JavaScript中没有提供幂计算、开根号的运算符,需要使用Math对象的相关方法进行计算
Math.pow(2, 3)  // 8
Math.pow(3, 2)  // 9

Math.sqrt(81)   // 9
Math.sqrt(-81)  // NaN 负数不能开根号

向上取整和向下取整:

  • Math.ceil() 向上取整;Math.floor() 向下取整
Math.ceil(2.4)       // 3
Math.floor(2.4)      // 2
Math.ceil(-2.4)      // -2
Math.floor(-2.4)     // -3
Math.ceil(2)         // 2
Math.floor(2)        // 2

(3)关系表达式

运算符名称运算符
大于>
小于<
大于或等于>=
小于或等于<=
等于==
不等于!=
全等于===
不全等于!==
  • 大于 >、小于 < 两个符号和数学相同
  • “大于等于”运算符是 >=,“小于等于”运算符是 <=
  • 如果想比较两个值是否相等,此时应该使用 == 运算符
  • 两个等号 == 运算符 不比较值的类型,它会进行隐式转换后比较值是否相等
  • 三个等号 === 运算符,不仅比较值是否相等,也比较类型是否相同
5 == '5'    // true
5 === '5'   // false

1 == true       // true
1 === true      // false

0 == false           // true
0 === false          // false

0 == undefined          // false
0 === undefined         // false

undefined == null         // true
undefined === null        // false  (typeof) undefined object
  • != 表示不相等,!== 表示不全等
5 != 6     // true
5 !== 6    // true

5 != '5'    // false
5 !== '5'   // true

NaN不自等:

  • NaN 作为一个特殊的数字类型值,它在用 == 比较的时候也有特殊结果
NaN == NaN    // false
NaN === NaN   // false
  • isNaN() 函数可以用来判断变量值是否为 NaN
isNaN(NaN)       // true
isNaN(3)         // false
  • 但 isNaN() 也不好用,它的机理是:只要该变量传入 Number() 的执行结果是 NaN,则 isNaN() 函数都会得到true
isNaN(undefined)    // true
isNaN('3年')        // true
isNaN(null)         // false

(4)逻辑表达式

运算符名称运算符
&&
||
!

与运算:

  • && 表示“且”,称为“与运算”,口诀:全真才真
true && true         // true
false && true        // false
false && false       // false
true && false        // false

或运算:

  • || 表示“或者”,称为“或运算”,口诀:有真即真
true || true         // true
false || true        // true
false || false       // false
true || false        // true

非运算:

  • ! 表示“非”,也可以成为“置反运算”
  • ! 是一个“单目运算符”,只需要一个操作数
  • 置反运算的结果一定是布尔值
!true         // false
!false        // true
!0            // true
!undefined    // true
!''           // true
!'hello'      // false
!!true        // true
!!0           // false
!!''          // false
!!'hello'     // true

短路计算:

  • a && b 运算中:a 真,表达式值为 b;a 假,表达式值为 a
3 && 6               // 6
undefined && 12      // undefined
null && 2            // null
'' && 13             // ''
NaN && undefined     // undefined

逻辑运算的优先顺序:

  • 逻辑运算的优先级:非 -> 与 -> 或
!true || true       // true
3 && 4 || 5 && 6    // 4

(5)赋值表达式

运算符名称运算符
赋值=
快捷赋值+=、-=、*=、/=、%=
自增运算++
自减运算

赋值运算符:

  • JS中,= 表示赋值,== 是判断是否相等(不比较类型),=== 是判断是否全等(比较类型)
  • 赋值运算符会将赋值符号右边的数值,赋予赋值符号左边的变量
  • 赋值运算也会产生值,符号后面的值将作为“赋值运算的值”
var a;
console.log(a = 3);     // 3
  • 可以连续使用赋值运算符(公司一般不允许这样做)
var a,b,c;
a = b = c = 12;
console.log(a);      // 12
console.log(b);      // 12
console.log(c);      // 12

++a 和 a++ 的区别:

  • ++a 先加再用;a++ 先用再加
  • 同理,–a 先减再用;a-- 先用再减
var a = 3;
var b = a++;
console.log(b);      // 3

a = 3;
var c = ++a;
console.log(c);      // 4

a = 3;
var m = --a;
console.log(m);      // 2

a = 3;
var n = a--;
console.log(n);      // 3

(6)综合表达式

  • 运算顺序:非运算 -> 数学运算 -> 关系运算 -> 逻辑运算
5 < 3 + 3               // true
3 > 2 && 8 > 3 + 4      // true
3 > 2 && 8 > 3 + 5      // false
!3 < 5 - 3              // true
!3 < 5 - 5              // false

变量的范围表示:

  • 验证变量 a 是否介于 3 到 12 之间
a >= 3 && a <= 12

闰年判断:

  • 公历闰年的简单计算方法,满足以下两个条件之一
  • 能被 4 整除且不能被100整除
  • 能被100整除也能被400整除
// 弹出输入框
var year = Number(prompt('请输入年份'));

// 根据两个条件判断(满足以下两个条件之一)
// 能被4整除且不能被100整除
// 能被100整出也能被400整除
alert((year / 4 == 0 && year % 100 != 0)||(year % 100 == 0 && year % 400 == 0));

小结

二、JS 流程结构与数组

1、流程控制语句

(1)if~else 和 else if

  • if 语句是最简单的 条件语句,也称为 选择语句。它通常结合 else 一起使用,表示 如果…就…否则…
  • if~else 语句中 else 可以省略
  • 如果 if 语句体中只有一行语句,可以省略大括号和换行
  • else~if() 条件分支“暗含”不符合之前的所有条件
if(条件1) {
   // 符合条件1执行
   // 语句块1
}else if(条件2) {
	// 符合条件2执行
	// 语句块2
}
……
else {
	// 以上条件都不符合
	// 语句块
}

算法题:判断水仙花数

  • 水仙花数是这样的三位数:它的每个数位的立方和等于它的本身
  • 如何拆位:
    1)数学方法:百位是原数字除以100取整;十位是原数字除以10取整,再与10求模;个位是原数字与10求模
    2)字符串方法:直接将原数字变为字符串,然后使用 charAt() 方法得到每个数位的值
var m = Number(prompt('请输入一个三位数'));
// 对用户输入的数值,进行合法性教验
if(!NaN(m) && m >= 100 && m <= 999) {
	var m_str = m.toString();
	// 百分位
	// Math.floor() 向下取整;Math.ceil() 向上取整
	// var a = Math.floor(m / 100);
	var a = Number(m_str.charAt(0));
	// 十分位
	// var b = Math.floor(m / 10) % 10;
	var b = Number(m_str.charAt(1));
	// 个位
	// var c = m % 10;
	var c = Number(m_str.charAt(2));
	// 根据水仙花条件判断
	if(Math.pow(a,3) + Math.pow(b,3) + Math.pow(c,3) == m) {
		alert(m + '是水仙花数!');
	} else {
		alert(m + '不是水仙花数');
	}
}else {
	alert('您的输入数字不合法!');
}

(2)switch语句

  • 除 if 语句之外,JS 还提供了另外一种选择语句:switch语句
  • switch 语句的用途:当一个变量被分类讨论时

(3)三元运算符

()for循环语句

()while 和 do~while

()break 和 continue

()什么是算法

()累加器和累乘器

()穷举法

2、数组

(1)数组简介和定义

  • 数组(Array),用来存储一组相关的值,从而方便进行求和、求平均数、逐项遍历等操作。
  • 数组是一种非常重要的数据结构

定义数组:

  • 定义数组只需要使用方括号 [] 即可
var arr = ['a','b','c','d'];
  • 定义数组还可以使用 new Array()
var arr = new Array('a','b','c','d');
  • 下面的代码表示定义一个长度为 4 的数组,但是这 4 项都是 undefined
var arr = new Array(4);

访问数组项:

  • 数组每一项都有下标,下标从 0 开始
  • 为了防止误会,一般称呼某项为“下标为x的项”,而不称呼为“第x项”
  • 可以使用 在方括号中书写下标的形式,访问数组的任一项
var arr = ['a','b','c','d'];
console.log(arr[0]);        // a
console.log(arr[1]);        // b
console.log(arr[2]);        // c
console.log(arr[3]);        // d

下标越界:

  • JavaScript中规定,访问数组中不存在的项会返回 undefined,不会报错
var arr = ['a','b','c','d'];
console.log(arr[4]);          // undefined
console.log(arr[-1]);         // undefined
console.log(arr[100]);        // undefined

数组的长度:

  • 数组的 length 属性 表示它的长度
  • 数组最后一项的下标是数组的长度减 1
var arr = ['a','b','c','d'];
console.log(arr.length);          // 4

更改数组项:

  • 数组并不是只读的,我们可以通过数组下标找到对应的项,然后更改值
var arr = [1,2,3,4];
arr[0]++;
arr[3] = 5;
console.log(arr);        // [2, 2, 3, 5]
  • 如果更改的数组项超过了 length - 1,则会创造这项;length - 1 对应的项与创建的项之间的项为 undefined
var arr = [1,2,3,4];
arr[6] = 5;
console.log(arr);        // [1, 2, 3, 4, undefined, undefined, 5]

数组遍历:

  • 数组的最大优点就是 方便遍历
var arr = [1,2,3,4];
for(var i = 0; i < arr.length; i++) {
	console.log(arr[i]);        // [1, 2, 3, 4]
}

(2)数组类型和检测

  • 数组用 typeof 检测的结果是 object
  • Array.isArray() 方法可以用来检测数组(圆括号里放要检测的东西),结果是布尔值

(3)数组的常用方法

数组的头尾操作方法:

方法功能
push()在尾部插入新项
pop()在尾部删除
unshift()在头部插入新项
shift()在头部删除

push() 方法:

  • push() 方法用来 在数组末尾推入新项,参数就是要推入的项
  • 如果要推入多项,可以用逗号隔开,推入的项直接放到数组尾部,相当于在原数组后补充一个数组
  • 调用 push() 方法后,数组会立即改变,不需要赋值
var arr = [1,2,3];
arr.push(11);
arr.push(21,22,23);
console.log(arr);     // [1, 2, 3, 11, 21, 22, 23]

pop() 方法:

  • 与 push() 方法相反,pop() 方法用来 删除数组中的最后一项
  • pop() 方法不仅会删除数组末项,而且还会返回被删除的项
  • 如果在 pop() 方法的圆括号内添加参数,参数没有任何意义,也不会报错
var arr = [11, 22, 33, 44, 55];
var item = arr.pop();
console.log(item);          // 55
console.log(arr);           // [11, 22, 33, 44]

unshift() 方法:

  • unshift() 方法用来 在数组头部插入新项,参数就是要插入的项
  • 如果要推入多项,可以用逗号隔开,推入的项直接放到数组头部,相当于在原数组前补充一个数组
  • 调用 unshift() 方法后,数组会立即改变,不需要赋值
var arr = [1,2,3];
arr.unshift(11);
arr.unshift(21,22,23);
console.log(arr);     // [21, 22, 23, 11, 1, 2, 3]

shift() 方法:

  • 与 unshift() 方法相反,shift() 方法用来 删除数组中下标为 0 的项
  • shift() 方法不仅会删除数组末项,而且还会返回被删除的项
  • 如果在 shift() 方法的圆括号内添加参数,参数没有任何意义,也不会报错
var arr = [11, 22, 33, 44, 55];
var item = arr.shift();
console.log(item);          // 11
console.log(arr);           // [22, 33, 44, 55]

splice() 方法:

  • splice(start, length, [params …]) 方法用于 替换数组中的指定项
  • start 表示起始项,从下标为 start 的项开始替换
  • length 表示替换多少项
  • [params …] 表示替换成的内容
  • splice() 方法会以数组形式返回被删除的项
var arr = ['a','b','c','d','e','f'];
// 1.表示从下标为3项开始,将后面的 2 项替换为 1,2,3
arr.splice(3,2,1,2,3);
console.log(arr);     // ['a','b','c', 1, 2, 3,'f']
// 2.表示在数组下标为 3 的位置插入 1,2,3
arr.splice(3,0,1,2,3);
console.log(arr);     // ['a','b','c', 1, 2, 3, ‘d', 'e', 'f']
// 3.表示从数组下标为 3 的位置开始删除 2 项
arr.splice(3,2);
console.log(arr);     // ['a','b','c', 'f']
// 4.表示从数组下标为 3 的位置开始删除后面所有项
var items = arr.splice(3);
console.log(arr);     // ['a','b','c']
console.log(items);     // ['d','e','f']

slice() 方法:

  • slice() 方法用于得到 子数组,类似于字符串的 slice() 方法
  • slice(a, b) 截取的子数组 从下标为 a 的项开始,到下标为 b 的项结束(不包括下标为 b 的项)
  • slice() 方法不会更改原数组
  • slice() 如果不提供第二个参数,则表示从指定项开始,提取所有后续项作为子数组
  • slice() 方法的 参数允许为负数,表示数组的倒数第几项
var arr = ['a','b','c','d','e','f'];

var child_arr1 = arr.slice(3,5);
var child_arr2 = arr.slice(3);
var child_arr3 = arr.slice(3,-2);
var child_arr4 = arr.slice(-4, -2);

console.log(child_arr1);        // ['d','e']
console.log(child_arr2);        // ['d','e','f']
console.log(child_arr3);        // ['d']
console.log(child_arr4);        // ['c', 'd']
console.log(arr);               // ['a','b','c','d','e','f']

join() 和 split() 方法:

  • 数组的 join() 方法可以 使数组转为字符串;字符串的 split() 方法可以使字符串转为数组
  • join() 的参数表示 以什么字符作为连接符,如果 留空则默认以逗号分隔,如同 toString() 方法
  • split() 的参数表示以什么字符拆分字符串,一般不能留空
var arr = ['a','b','c','d'];
// 不写参数,默认使用“,”分隔
var arr_str = arr.join();
console.log(arr_str);                  // a,b,c,d
// 使用“-”作为分隔符
var arr_str1 = arr.join('-');
console.log(arr_str1);                 // a-b-c-d
// 使用空字符串作为分隔符
var arr_str2 = arr.join('');
console.log(arr_str2);                 // abcd
// 不写参数,会将整个字符串当做一个数组项
var str_arr = arr_str.split();
console.log(str_arr);                  // ["a,b,c,d"]
// 使用空字符串,会连带将分隔符一起作为数组项
var str_arr = arr_str.split('');
console.log(str_arr);                  // ["a", ",", "b", ",", "c", ",", "d"]
// 使用分隔符分隔,会将每一个字符作为一项
var str_arr = arr_str.split(',');
console.log(str_arr);                  // ["a", "b", "c", "d"]

字符串和数组更多相关特性:

  • 字符串也可以使用方括号内写下标的形式访问某个字符,等价于 charAt() 方法
  • 这样的话,就不常使用 charAt() 了
  • 字符串的一些算法问题有时候会转换为数组解决
var str = "hello world!";
console.log(str[0]);        // "h"
console.log(str[2]);        // "l"
console.log(str[5]);        // " "
console.log(str[11]);       // "!"
console.log(charAt(0));        // "h"
console.log(charAt(2));        // "l"
console.log(charAt(5));        // " "
console.log(charAt(11));       // "!"

concat() 方法:

  • concat() 方法可以 合并连结多个数组
  • concat() 方法并不会改变原数组
var arr1 = [1,2,3];
var arr2 = [4,5,6];
var arr3 = [7,8,9,10,11];
var arr = arr1.concat(arr2, arr3);
console.log(arr);   // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
console.log(arr1);  // [1, 2, 3]

reverse() 方法:

  • reverse() 方法用来将一个数组中的全部项顺序置反
var arr = ['A','B','C','D','E','F','G'];
arr.reverse();
console.log(arr);    // ["G", "F", "E", "D", "C", "B", "A"]
// 将一个字符串置反
var str = "abcdefg";
console.log(str.split('').reverse().join(''));  // "gfedcba"
// 逐步拆解如下:
var str_arr = str.split('');
console.log(arr);  // ["a", "b", "c", "d", "e", "f", "g"]
arr.reverse();
var arr_str = arr.join('');
console.log(arr_str);  // "gfedcba"

indexOf 和 includes() 方法:

  • indexOf() 方法的功能是 搜索数组中的元素,并返回它所在的位置,如果元素不存在,则返回 -1
var arr = ['a','b','c','d','e','c'];
// 搜索数组中的元素,并返回它所在的位置(下标数)
var index1 = arr.indexOf('a');
console.log(index1);     // 0
// 当数组中多次出现某个元素,则返回第一次出现时所在的下标数
var index2 = arr.indexOf('c');
console.log(index2);     // 2
// 当搜索的元素不存在,则返回 -1
var index3 = arr.indexOf('g');
console.log(index3);     // -1
  • includes() 方法的功能是 判断一个数组是否包含一个指定的值,返回布尔值
var arr = ['a','b','c','d','e','c'];
// 数组是否包含一个指定的值,返回布尔值
var flag1 = arr.includes('a');
console.log(flag1);     // true
var flag2 = arr.includes('g');
console.log(flag2);     // false
  • indexOf 和 includes() 方法在判断项的位置或项的值时使用的是 === 进行比较(既比较类型也比较值)
var arr = ['a','b','c',11,22];
var flag1 = arr.includes('a');
console.log(flag1);     // true
var flag2 = arr.includes(22);
console.log(flag2);     // true
var flag3 = arr.includes('11');
console.log(flag3);     // false
var flag4 = arr.includes(b);
console.log(flag4);     // 报错
var index1 = arr.indexOf('b');
console.log(index1);     // 1
var index2 = arr.indexOf(22);
console.log(index2);     // 4
var index3 = arr.indexOf('11');
console.log(index3);     // -1

(4)数组去重和随机样本

数组去重:

var arr = [1,1,3,4,2,3,5,5,7,6,7,3,2,9];
var result = [];
for(var i = 0; i < arr.length; i++) {
	if(!result.includes(arr[i])) {
		result.push(arr[i]);
	}
}
console.log(result);  // [1, 3, 4, 2, 5, 7, 6, 9]

随机样本:

  • 题目:请随机从原数组中取3项
  • 思路:准备一个空结果数组,遍历原数组,随机选择一项推入结果数组,并将这项删除
var arr = [1,3,2,5,9,3,7,6,4,8,10];
var result = [];
// 3 表示执行三次
for(var i = 0; i < 3; i++) {
	// [a,b]区间的随机整数是:parseInt(Math.random() * (b - a + 1)) + a;
	var randomIndex = parseInt(Math.random() * arr.length);
	// 将随机索引下标项推入结果数组
	result.push(arr[randomIndex]);
	// 原数组删除这项
	// splice(a,b) 表示从 a 位置开始删除 b 项
	arr.splice(randomIndex,1);
}
console.log(result);
console.log(arr);

(5)冒泡排序

  • 这里先给出一个案例,算法原理暂时先不说
  • 具体的算法原理及案例参见:算法
var arr = [1,3,2,5,9,7,6,4,8,10];

for(var i = 0; i < arr.length - 1; i++) {
	for(var j = 0; j < arr.length - i - 1; j++) {
		if(arr[j] > arr[j + 1]) {
            var temp = arr[j];
            arr[j] = arr[j + 1];
            arr[j + 1] = temp;
        }
    }
}
console.log(arr);

(6)二维数组

  • 二维数组:以数组作为数组元素的数组,即“数组的数组
  • 二维数组可以看做是矩阵
var matrix = [
	[11,22,33],
	[1,2,3],
	[4,5,6,7],
	[45,54,66]
];
console.log('数组长度为:' + matrix.length);	  // 4
// 遍历二维数组中的每一项
for(var i = 0; i < matrix.length; i++) {
	for(var j = 0; j < matrix[i].length; j++) {
		console.log(matrix[i][j]);
	}
}

三、JS 函数

1、函数的基础

(1)什么是函数

  • 函数就是语句的封装,可以让这些代码方便的被复用,调用函数需要使用圆括号
  • 函数具有“一次定义,多次调用”的优点
  • 使用函数,可以简化代码,让代码更具有可读性
  • 定义函数时后面不写分号“;”

(2)函数的定义和调用

  • 和变量类似,函数必须先定义后使用
  • 使用 function 关键字 定义函数,function 是“功能”的意思

函数的定义:

// function 表示定义函数
// fun 是函数名,函数名必须符合 JS 标识符命名规则
// () 里面是行参列表,即使没有行参,也必须书写圆括号
// {} 大括号中就是函数体语句
function fun() {
    // 函数体语句
}

函数表达式:

  • 相当于定义一个变量指向匿名函数
// function 是匿名函数
var fun = function() {
    // 函数体语句
}

函数的调用:

  • 执行函数体中的所有语句,就称为“调用函数”
  • 调用函数非常简单,只需要在函数名字后书写圆括号对即可
fun();  // 调用函数

(3)语句的执行顺序

  • 在主程序中按顺序执行
  • 函数不调用内部语句就不会执行,一旦调用函数,调用权就会移交给被调用的函数
  • 函数体内所有语句执行完毕后,将语句执行权交还给主程序
function fun() {
   console.log('A')
   console.log('B')
   console.log('C')
}

console.log('1')
console.log('2')
console.log('3')
fun();
console.log('4')
console.log('5')
console.log('6')

// 在浏览器控制台输出
1 2 3 A B C 4 5 6

(4)函数声明的提升

  • 声明的提升:调用在定义之前
  • 和变量声明提升类似,函数声明也可以被提升
  • 在 JS 所有代码执行之前,有一个预解析阶段,在预解析阶段函数会被提升
fun();
function fun() {
    console.log('函数体被执行');
}
  • 用函数表达式的写法定义函数没有提升特性,否则会引发错误
    引发错误的原因: 这本质上是在定义一个变量,只不过是将函数赋值给变量而已,而变量只提升定义,不提升值,所以 function 没有被提升,提升的是变量fun的定义,这个fun的值是undefined,而undefined是不能加圆括号运行,因为只有函数才能加圆括号运行
fun();  // 引发错误
var fun = function() {
    console.log('函数体被执行');
}

函数的优先提升:

  • 函数优先提升
  • 然后才是变量声明的提升,无法覆盖提升的函数
fun();  // 打印 B
var fun = function() {
    console.log(' A');
}
function fun() {
    console.log('B');
}
fun(); // 打印 A

(5)函数的参数与返回值

函数的参数:

  • 参数是函数内的一些待定值,在函数调用时,必须传入这些参数的具体值
  • 函数的参数可多可少,函数可以没有参数,也可以有多个参数,多个参数之间要用逗号隔开
// 这里的 a 和 b 就是函数的行参
function sum(a, b) {
    var add = a + b;
    console.log("两个参数之和为:" + add);
}
// 这里的 3 和 5 就是调用函数传入的实参
sum(3,5);

行参和实参个数不同的情况:

  • 当行参数大于实参数,则没有收到实参的行参的值就会为undefined;un defined 进行任何运算结果都是 NaN
function fun(a, b, c) {
    var sum = a + b + c;
    console.log(sum);
}
fun(1,2);

// 输出结果为
NaN
  • 当行参数小于实参数,则会造成没有行参来接收实参
function fun(a, b) {
    var sum = a + b;
    console.log(sum);
}
fun(2,3,5); // 控制台报错

arguments:

  • 函数内 arguments 表示它接收到的实参列表,它是一个类数组对象
  • 不管用户传入多少个实际参数,永远能够得到传入的值
  • 类数组对象:所有属性均为从0开始的自然数序列,并且有length属性,和数组类似可以用方括号“[]”书写下标访问对象的某个属性值,但是不能调用数组的方法
function fun() {
    var sum = 0;
    for(var i = 0; i < arguments.length; i++) {
        sum += arguments[i];
    }
    console.log(sum);
}

fun(12,23,32,55);
fun(6,7);
fun(3,-2,-5,6);

函数的返回值:

  • 函数体内可以使用 return 关键字 表示 “函数的返回值”
  • 函数的返回值可以被变量接收
  • 调用一个有返回值的函数,可以被当做一个普通值,从而可以出现在任何可以书写值的地方
function sum(a,b) {
    return a + b;
}
var result = sum(1,2) * sum(5,7);
  • 调用函数时,一旦遇见 return 语句就会立即退出函数,将执行权交还给调用者
  • 结合 if 语句的时候,往往不需要写 else 分支了
function fun() {
    console.log('A');
    return 'B';
    console.log('C'); // 不会执行
}
console.log(1)
var ch = fun();
console.log(ch);
console.log(2)

// 输出结果
1 A B 2

2、递归(factorial)

  • 函数的内部语句可以调用这个函数自身,从而发起对函数的 迭代。在新的迭代中,又会执行调用函数自身的语句,从而又产生一次迭代。当函数执行到某一次时,不再进行新的迭代,函数被一层层返回,称为函数递归。
  • 边界条件:确定递归到何时终止,也称为递归出口
  • 递归模式:大问题是如何分解为小问题的,也称为递归体
// 求一个数的阶乘
// 思路:n! 就是 n*(n-1)!
function factorial(n) {
    // 递归的出口
    if(n == 1) return 1;
    return n * factorial(n - 1);
}
var result = factorial(10);
console.log(result);

3、函数算法题

(1)寻找喇叭花数

  • 喇叭花数:有一个整数,其中每一位数字的阶乘之和恰好等于它本身,即abc = a! + b! + c!,其中 abc 是一个三位数。
// 计算一个数字的阶乘
function factorial(n) {
    var result = 1;
    for(var i = 1; i <= n; i++) {
        result *= i;
    }
    return result;
}
// 利用穷举法寻找100到999的喇叭花数
for(var i = 100; i <= 999; i++) {
    // 把数字变为字符串
    var i_str = i.toString();

    var a = Number(i_str[0]);  // 百分位
    var b = Number(i_str[1]);  // 十分位
    var c = Number(i_str[2]);  // 个位
    if(i == factorial(a) + factorial(b) + factorial(c)) {
        console.log(i);
    }
}

(2)JavaScript 内置 sort() 方法

  • 数组排序可以使用 sort() 方法,这个方法的参数又是一个函数
  • 这个函数中的 a、b 分别表示数组中靠前和靠后的项,如果需要将它们交换位置,则返回任意整数;否则返回负数
var arr = [12,3,1,4,6,2];
// 调用 sort() 方法排序
arr.sort(function(a,b) {
    if(a > b) {
        return 1;
    } else {
        return -1;
    }
});
console.log(arr);
// 也可以简写为
var arr = [12,3,1,4,6,2];
// 调用 sort() 方法排序
arr.sort(function(a,b) {
    return a - b;  // 升序
    // return b - a;  // 降序
});
console.log(arr);

(3)递归常见算法题——斐波那契额数列

  • 斐波那契数列示例:1、1、2、3、5、8、13、21 ……
  • 规律:数列下标为 0 和 1 的项的值都是 1,从下标为2的项开始,每项等于前面两项的和
function fib(n) {
    // 数列的下标为0的项和下标为1的项的值都是1
    if(n == 0 || n == 1) return 1;
    // 除了前两项,后面的都是 等于前两项的和
    return fib(n - 1) + fib(n - 2);
}

4、实现深克隆

  • JavaScript 中的数据类型有两类:基本类型和引用类型
举例当 var a = b 变量传值时当用 == 比较时
基本类型数字、字符串、布尔型、undefined内存中产生新的副本比较值是否相等
引用类型对象、数组内存中不产生新的副本,而是让新的变量指向同一个对象比较内存地址是否相同,即比较是否是同一个对象

浅克隆:

  • 使用 var arr1 = arr2 这样的语句 不能 实现数组的克隆
  • 浅克隆:准备一个空的数组,然后使用 for 循环遍历原数组,将遍历到的项都推入空的数组
  • 浅克隆只克隆数组的一层,如果数组是多维数组,则克隆的项会“藕断丝连”,即除一维外的其它维数组将会以引用的方式克隆到新数组中,与原数组“藕断丝连”
var arr = [11,33,22,55,44];
// 空的数组
var result = [];

// 遍历原数组,将遍历到的项都推入到空数组中
for(var i = 0; i < arr.length; i++) {
    result.push(arr[i]);
}

console.log(result);  // 11,33,22,55,44
console.log(result == arr);  // false

实现深克隆:

  • 使用递归思想,如果遍历到的项是基本类型值,则直接推入到结果数组中;如果遍历到的项又是数组,则重复执行浅克隆的操作。
// 原数组
var arr = [33,44,22,55,[12,2,31,6],10,[11,32,[9,4,5,7,2]]];

function deepClone(arr) {
    // 结果数组
    var result = [];
    // 遍历数组的每一项
    for(var i = 0; i < arr.length; i++) {
        // 如果遍历到的项是数组
        if(Array.isArray(arr[i])) {
            // 递归
            result.push(deepClone(arr[i]));
        } else {
            // 如果不是数组项,而是基本类型值,就直接推入到结果数组中
            // 相当于递归的出口
            result.push(arr[i]);
        }
    }
    // 返回结果数组
    return result;
}

var temp = deepClone(arr);

5、作用域和闭包

(1)变量作用域

  • JavaScript 是 函数级作用域 编程语言:变量只在其定义时所在的 function 内部有意义。
  • 定义在函数内部的变量,只能被该函数使用的变量,称为局部变量
  • 行参也是局部变量
function fun() {
    // 变量 a 是在 fun函数中被定义的,所以变量a只在fun函数内部有定义,
    // fun函数就是a的作用域,变量a被称为局部变量 
    var a = 10;
}
fun();
console.log(a); // 此时会报错:a is not defined
  • 如果不将变量定义在任何函数的内部,此时这个变量就是全局变量,它在任何函数内都可以被访问和更改
var a = 10;
function fun() {
    a++;
    console.log(a); // 输出 11
}
fun();
console.log(a); // 输出 11
  • 如果函数中也定义了和全局同名的变量,则函数内的变量会将全局的变量“遮蔽”,不是替换也不是覆盖,而是遮蔽
var a = 10;
function fun() {
    var a = 5;
    a++;
    console.log(a); // 输出 6
}
fun();
console.log(a); // 输出 10

(2)变量作用域——注意考虑变量声明提升的情况

  • 由于JS有预解析功能,所以会产生变量声明提升,此时局部变量 a 被自增
  • 由于变量提升只提升定义不提升值,所以 提升后的 a 值为 undefined
  • 自增后的值为:NaN
  • 执行完 a++ 后跳到下一行,此时变量 a 被赋值为 5
  • 最终输出的结果为 5
var a = 10;
function fun() {
    a++;
    var a = 5;
    console.log(a); // 输出 5
}
fun();
console.log(a); // 输出 10,局部变量不影响全局变量

(3)作用域链

  • 函数的嵌套:一个函数的内部也可以定义一个函数。和局部变量类似,定义在一个函数内部的函数是局部函数。
function fun() {
    function inner() {          // 该函数是局部函数
        console.log("局部函数");
    }
    inner();  // 调用内部函数
}
fun();  // 调用外部函数
  • 在函数嵌套中,变量会从内到外逐层寻找它的定义
var a = 10;
var b = 20;
function fun() {
    var c = 30;
    function inner() {          // 该函数是局部函数
        var a = 40;
        var d = 50;
        console.log(a, b, c, d); // 使用变量时,JS 会从当前层开始,逐层向上寻找定义
    }
    inner();
}
fun();  // 输出 a=40 b=20 c=30 d=50

(4)不加 var 将定义全局变量

  • 在初次给变量赋值时,如果没有加 var ,则将定义全局变量
function fun() {
    a = 5;
    console.log(a); // 输出 5
}
fun();
console.log(a); // 输出 5

(5)闭包

  • 内部 函数被移动到了 外部 执行
function fun() {
    // 定义一个局部变量
    var name = "ABC";
    // 返回一个局部函数
    return function() {
        console.log(name);
    }; // 别忘了“;”,return 返回的结果要有“;”结尾
    // 或者写为下面这种形式
    // function innerFun() {
    //     console.log(name);
    // }
    // return innerFun;
}
// 调用外部函数,就能得到内部函数,用变量 inn 来接收
var inn = fun();
// 执行 inn 函数,就相当于在 fun 函数的外部,执行了内部函数
inn(); // 内部函数被移动到了外部执行
  • JavaScript 中函数会产生 闭包(closure)。闭包是函数本身和该函数声明时所处的环境状态 的组合。
  • 函数能够“记忆住”其定义时所处的环境,即使函数不在其定义的环境中被调用,也能访问定义时所处环境的变量。
  • 在JavaScript 中,每次创建函数时都会创建闭包
  • 但是,闭包特性往往需要将函数“换一个地方”执行,才能观察出来,类似前面的内部函数移动到外部执行
  • 闭包非常实用,因为它允许我们将数据与操作该数据的函数关联起来。
  • 闭包的功能:记忆性、模拟私有变量

闭包用途1–记忆性:

  • 当闭包产生时,函数所处环境的状态会始终保持在内存中,不会在外层函数调用后被自动清除。这就是闭包的记忆性
  • 记忆性在下面的函数中表现为传入checkTemp函数的值时,他能一直记住
// 闭包的记忆性举例
//   创建体温检测函数 checkTemp(n),可以检查体温n是否正常,函数会返回布尔值
//   但是,不同的小区有不同的体温检测标准,比如A小区温度合格线时37.3,而B小区体温合格线是37.1
function checkTemp(standardTemp) {
    return function(n) {
        if(n <= standardTemp) {
            alert("体温正常")
        } else {
            alert("体温偏高")
        }
    };
}
var checkTemp_A = checkTemp(37.3);
var checkTemp_B = checkTemp(37.1);
// 输入值测试
checkTemp_A(37.0); // 输出:体温正常
checkTemp_A(37.4); // 输出:体温偏高
checkTemp_B(37.2); // 输出:体温偏高
checkTemp_B(37.0); // 输出:体温正常

闭包用途1–模拟私有变量:

  • 在Java、C++等语言中有私有属性的概念,但是在JavaScript中只能用闭包来模拟
  • 模拟私有变量就是在函数内部定义函数去使用局部变量,然后在外部调用内部函数得到局部变量的值,此时定义的局部变量对于外部来说就是私有的
// 题目:请定义一个变量a,要求是能保证这个a 只能被进行指定操作,而不能进行其它操作
// 封装一个函数,这个函数的功能就是私有化变量
function fun {
    // 定义一个局部变量
    var a = 0;
    return {
        getA:function() {
            return a;
        },
        add:function() {
            return a++;
        }
    };
}
var obj = fun();
// 想在fun函数外面使用变量a,唯一的方法就是调用getA() 方法
console.log(obj.getA())

使用闭包的注意点:

  • 不能滥用闭包,否则会造成网页的性能问题,严重时可能造成内存泄漏。所谓内存泄漏是指程序中已动态分配的内存由于某中原因未释放或无法释放。

6、立即执行函数

  • IIFE(Immediately Invoked Function Expression,立即调用函数表达式)是一种特殊的JavaScript函数写法,一旦被定义,就立即被调用
  • ()()中第二个圆括号内放的是 IIFE中的行参,也即是第一个圆括号中函数的行参,即第二个圆括号相当于第一个圆括号内function()中的圆括号
// 包裹函数的圆括号的功能:将函数变为表达式
// 后面的圆括号的功能:运行函数
(function() {
    // 任意语句
})();
  • 函数不能直接加圆括号被调用
  • 函数必须转为“函数表达式”才能被调用
// 错误示例
function() {
    //语句
}();

// 正确示例
(function() {
    // 语句
})();

IIFE 的作用1——为变量赋值:

  • 为变量赋值:当给变量赋值需要一些较为复杂的计算时(如if语句),使用IIFE显得语法更紧凑
var age = 23;
var sex = "男";
// 定义变量,变量的值由上面的变量决定
var title = (function() {
    if(age < 18) {
        return ""小朋友;
    } else {
        if(sex == "男") {
            return '先生';
        } else {
            return '女士';
        }
    }
})();

console.log(title);

IIFE 的作用2——将全局变量变为局部变量:

  • IIFE 可以在一些场合(如for循环中)将全局变量变为局部变量,语法显得紧凑
var arr = [];
for(var i = 0; i < 5; i++) {
    (function(i) {
        arr.push(function() {
            alert(i);
        });
    })(i);
}
arr[2]();  // 弹出 2

小结

  • 课程重点: 什么是函数?函数为编程带来了哪些便利?
  • 课程重点: 函数的参数和返回值
  • 课程重点: 函数的相关算法题
  • 课程难点: 递归、递归算法题
  • 课程难点: 作用域和闭包
  • 课程难点: IIFE
  • 函数声明的提升:类似于学习课文之前用眼睛扫描全篇内容,做一个简单的预习

四、DOM

1、DOM基本概念

  • DOM(Document Object Model,文档对象模型) 是JavaScript操作HTML文档的接口,使文档操作变得非常简便。
  • DOM 最大的特点就是将文档表示为节点树
  • 节点有:元素节点、属性节点、文本节点
    在这里插入图片描述

nodeType常用属性值:

  • 节点的nodeType属性可以显示这个节点具体的类型
nodeType值节点类型
1元素节点,例如 p标签 和 div标签
3文字节点
8注释节点
9document 节点
10DTD节点

2、访问元素节点 —— document对象

  • 所谓“访问”元素节点,就是指“得到”、“获取”页面上的元素节点
  • 对节点进行操作,第一步就是得到它
  • 访问元素节点主要依靠document对象

(1)认识document对象

  • document对象是DOM中最重要的东西,几乎所有DOM的功能都封装在了document对象中
  • document对象也表示整个HTML文档,它是DOM节点树的根
  • document对象的 nodeType 属性值是 9

(2)访问元素节点的常用方法

方法功能兼容性
document.getElementById()通过 id 得到 元素IE6
document.getElementsByTagName()通过 标签名 得到 元素数组IE6
document.getElementsByClassName()通过 类名 得到 元素数组IE9
document.querySelector()通过 选择器 得到 元素IE8部分兼容,IE9完全兼容
document.querySelectorAll()通过 选择器 得到 元素数组IE8部分兼容,IE9完全兼容

getElementById():

  • document.getElementById() 功能是 通过 id 得到元素节点
  • 参数就是元素节点的 id,注意参数不要写 # 号,错误示例: (’#box’)
  • 如果页面上有相同 id 的元素,则只能得到第一个
  • 不管元素藏的位置有多深,都能通过 id 把它找到
<div id = "box1">我是第一个</div>
<p id = "box2">我的第二个</p>

// JS 代码:
var box1 = document.getElementById('box1');
var box2 = document.getElementById('box2');

getElementsByTagName():

  • document.getElementsByTagName() 方法的功能是 通过标签名得到节点数组
  • 数组方便遍历,从而可以批量操控元素节点
  • 即使页面上只有一个指定标签名的节点,也将得到长度为1的数组
  • 任何一个节点元素也可以调用 getElementsByTagName() 方法,从而得到其内部的某种类的元素节点
<div id = "box">
    <p>一段文字</p>
    <p>一段文字</p>
</div>
<div id = "box1">
    <p>一段文字</p>
    <p>一段文字</p>
</div>

// JS 代码:
// var ps = document.ElementsByTagName('p'); //获得所有
var box = document.getElementById('box');
var ps_box = box.getElementsByTagName('p');  // 获得box盒子下的p

getElementsByClassName():

  • document.getElementsByClassName() 方法的功能是 通过类名得到节点数组
  • 注意不要写“.”号,错误示例:(’.box’)
  • getElemensByClassName() 方法从IE9开始兼容
  • 某个节点元素也可以调用 getElementsByClassName() 方法,从而得到其内部的某类名的元素节点
<div class = "box">我是一个盒子</div>
<div>我是一个盒子</div>
<div class = "box1">我是一个盒子</div>
<div class = "box">我是一个盒子</div>

// JS 代码:
var box = document.getElementsByClassName('box');

querySelector():

  • querySelector() 方法的功能是 通过选择器得到元素
  • 这里的是选择器,所以必须加CSS对应的符号:id选择器是“#”,类选择器是“.”(下面示例中两个选择器间的空格表示是使用:后代选择器,CSS相关的内容参考:https://blog.csdn.net/weixin_49809163/article/details/115440235
  • querySelector() 方法从IE8开始兼容,但从IE9开始支持CSS3的选择器,如:nth-child()、:[src^=‘cat’]等CSS选择器形式都支持
<div id = "box">
    <p>我是段落</p>
    <p class="spec">我是段落</p>
    <p>我是段落</p>
</div>

// JS 代码:
var the_p = document.querySelector('#box .spec');

querySelectorAll():

  • querySelectorAll() 方法的功能是 通过选择器得到元素数组
  • 即使页面上只有一个符合选择器的节点,也将得到长度为1的数组
<div id = "box">
    <p>我是段落</p>
    <p class="spec">我是段落</p>
    <p>我是段落</p>
</div>

// JS 代码:
var ps = document.querySelectorAll('#box p');

(3)延迟运行

  • 在测试DOM代码时,通常 JS 代码一定要写到HTML节点的后面,否则 JS 无法找到相应HTML节点
  • 若想将 JS 代码写在页面结构代码之前,可以使用 window.onload = function() {} 事件,使页面加载完毕后,再执行指定的代码

(4)节点的关系

  • DOM中,文本节点也属于节点,在使用节点的关系时一定要注意
  • 在标准的W3C规范中,空文本节点也算节点,但是在IE8及以前的浏览器中会有一定的兼容问题,它们不把空白文本节点当做节点
  • 从IE9开始支持一些“只考虑元素节点”的属性
    在这里插入图片描述
关系考虑所有节点只考虑元素节点
子节点childNodeschildren
父节点parentNodesparentNodes
第一个子节点firstChildfirstElementChild
最后一个子节点lastChildlastElementChild
前一个兄弟节点previousSiblingpreviousElementSibling
后一个兄弟节点nextSiblingnextElementSibling

封装节点关系函数:

  • 书写IE6也能兼容的“寻找所有元素子节点”函数
  • 书写IE6也能兼容的“寻找前一个元素兄弟节点”函数
  • 如何编写函数,获得某元素的所有的兄弟节点?
// 封装一个函数,这个函数可以返回元素的所有子元素节点(兼容到IE6),类似children的功能
function getChildren(node) {
    // 结果数组
    var children = [];
    // 遍历node这个节点的所有子节点,判断每一个子节点的nodeType属性是不是1
    // 如果是1(nodeType值为 1 表示的是元素节点),就推入结果数组
    for(var i = 0; i < node.childNodes.length; i++) {
        if(node.childNodes[i].nodeType == 1) {
            children.push(node.childNodes[i]);
        }
    }
    return children;
}

// 封装一个函数,这个函数可以返回元素的前一个兄弟节点(兼容到IE6),类似previousElementSibling的功能
function getElementPrevSibling(node) {
    var o = node;
    // 使用while语句
    while(o.previousSibling != null) {
        if(o.previousSibling.nodeType == 1) {
            // 结束循环
            return o.previousSibling;
        }

        // 让 o 成为它的前一个节点
        o = o.previousSibling;
    }
    // 没有前一个兄弟节点时返回 null
    return null;
}

// 封装第三个函数,这个函数可以返回元素的所有元素兄弟节点
function getAllElementSibling(node) {
    // 前面的元素兄弟节点
    var prevs = [];
    // 后面的元素兄弟节点
    var nexts = [];

    var o = node;
    // 遍历node前面的节点
    while(o.previousSibling != null) {
        if(o.previousSibling.nodeType == 1) {
            prevs.unshift(o.previousSibling);
        }
        o = o.previousSibling;
    }
    
    o = node;
    // 遍历node后面的节点
    while(o.nextsSibling != null) {
        if(o.nextsSibling.nodeType == 1) {
            nexts.push(o.nextsSibling);
        }
        o = o.nextsSibling;
    }
    // 将两个数组进行合并,然后返回
    return prevs.concat(nexts);
}

(5)节点操作

改变元素节点内容:

  • 改变元素节点的内容可以使用两个相关属性:innerHTML 和 innerText
  • innerHTML 属性能以 HTML语法 设置节点中的内容
  • innerText 属性只能以 纯文本的形式 设置节点中的内容

改变元素节点的CSS样式:

  • 标准W3C属性,如src、href等,只需要通过获得的元素节点直接打点进行更改即可
  • CSS 属性要写成“驼峰”形式
  • CSS属性值要设置成完整形式
  • 书写CSS时要注意单位
  • 改变元素节点的CSS样式需要使用下面示例语句:
var oBox = document.getElementsByClassName('box');
oBox.style.backgroungColor = 'blue';
oBox.style.backgroungImage = 'url(images/1.jpg)';
  • 对于不符合W3C标准的属性,要使用 setAttribute() 和 getAttribute() 来设置、读取
var oBox = document.getElementsByClassName('box');
oBox.setAttribute('data-n', 10);
var n = oBox.getAttribute('data-n');

(6)节点的创建、移除和克隆

节点的创建:

  • document.createElement() 方法用于创建一个指定 tag name 的HTML元素。
// 示例:
var cDiv = document.createElement('div');
  • 新创建出的节点是“孤儿节点”,这意味着它 并没有被挂载到DOM树上
  • 必须继续使用 appendChild() 方法 或 insertBefore() 方法 将孤儿节点插入到DOM树上
  • appendChild(): 任何已经在DOM树上的节点,都可以调用 appendChild() 方法,它可以将孤儿节点挂载到它的内部,成为它的最后一个子节点
父节点.appendChild(孤儿节点);
  • insertBefore(): 任何已经在DOM树上的节点,都可以调用 insertBefore() 方法,他可以将孤儿节点挂载到它的内部,成为它的“标杆子节点”之前的节点
父节点.insertBefore(孤儿节点, 标杆节点);
  • 小练习:创建九九乘法表
// html 代码
<table id = "mytable"></table>

// CSS代码:
td {
    width: 120px;
    height: 20px;
    border: 1px solid #000;
}

// JS 代码:
var mytable = document.getElementById('mytable');
for(var i = 1; i <= 9; i++) {
    // 创建了新的tr标签
	var ctr = document.createElement('tr');
	for(var j = 1; j <= i; j++) {
		// 创建新的td标签
		var ctd = document.createElement('td');
		// 设置td内部的文字
		ctd.innerText = j + 'x' + i + '=' + (i * j);
	    // 让tr追加td标签
	    ctr.appendChild(ctd);
	}
	// 让mytable追加tr标签
	mytable.appendChild(ctr);
}

移动节点:

  • 如果将已经挂载到DOM树上的节点成为 appendChild() 或 insertBefore() 的参数,这个节点将会移动
新父节点.appendChild(已经有的父节点);
新父节点.insertBefore(已经有的父节点, 标杆子节点);
  • 这意味着一个节点不能同时位于DOM树的两个位置

删除节点:

  • removeChild() 方法从DOM中删除一个子节点
父节点.removeChild(要删除的子节点);
  • 节点不能主动删除自己,必须由父节点删除它

克隆节点:

  • cloneNode() 方法可以克隆节点,克隆出的节点是孤儿节点
var 克隆得到的孤儿节点 = 老节点.cloneNode();
var 克隆得到的孤儿节点 = 老节点.cloneNode(true);  // 连带后代
var 克隆得到的孤儿节点 = 老节点.cloneNode(false); // 只有自身
  • 参数是一个布尔值,表示 是否采用深度克隆: 如果为true,则该节点的所有后代节点都会被克隆;如果为false,则只克隆该节点本身

3、事件监听

  • DOM允许我们书写JavaScript代码让HTML 元素对事件做出反应
  • 什么是“事件”:用户与网页的交互动作。如:用户点击元素时,鼠标移动到元素上时,页面加载完时 ……
  • 监听:就是让计算机随时能够发现这个事件发生了,从而执行程序员预先编写的一些程序
  • 设置事件监听的方法主要有 onXXX 和 addEventListener() 两种

(1)简单的事件监听——onxxx

  • 最简单的给元素设置事件监听的方法就是 设置它们的 onxxx 属性,示例如下:
oBox.onclick = function() {
    // 点击盒子时,执行这里的语句
};

常见的鼠标事件监听:

事件名事件描述
onclick当鼠标 点击 某个对象时触发事件
ondblclick当鼠标 双击 某个对象时触发事件
onmousedown当鼠标上某个按键在某个对象上 按下 时触发事件
onmouseup当鼠标上某个按键在某个对象上 松开 时触发事件
onmousemove当鼠标上某个按键在某个对象上 移动 时触发事件
onmouseenter当鼠标 进入 某个对象时触发事件(相似事件 onmouseover 冒泡)
onmouseleave当鼠标 离开 某个对象时触发事件(相似事件 onmouseout 冒泡)
onmousewheel当鼠标 滚轮滚动 时触发事件(它的事件对象e提供deltaY属性表示滚轮滚动方向,向下为正值,向上为负值)

常见的键盘事件监听:

事件名事件描述
onkeypress当某个键盘的按键被 按下(系统按钮如箭头键和F1-F12功能键无法得到识别)时触发事件
onkeydown当某个键盘的键被 按下(系统按钮可以识别,并且会优先于 onkeypress 发生)时触发事件
onkeyup当某个键盘的键被 松开 时触发事件

常见的表单事件监听:

事件名事件描述
onchange用户改变域的内容 时触发事件
oninput用户在域内输入内容 时触发事件(回退不触发)
onfocus某元素获得焦点(比如tab键或鼠标点击)时触发事件
onblur某元素失去焦点 时触发事件
onsubmit表单被提交时 触发事件
onreset表单被重置时 触发事件

常见的页面事件监听:

事件名事件描述
onload页面或图片被完成加载 时触发事件
onunload用户退出页面 时触发事件

(2)事件传播

  • 事件的传播是:先从外到内,然后再从内到外
  • 先从外到内 称为 捕获阶段(capturing phase)
  • 再从内到外 称为 冒泡阶段(Bubbling phase)
  • onxxx这样的事件监听方式 只能监听冒泡阶段

addEventListener() 方法:

  • DOM0 级事件监听:只能监听冒泡阶段
oBox.onclick = function() {
    // 触发事件后的执行语句
};
  • DOM1 级没有对事件监听有任何修改,所有就没有 DOM1 级事件监听,DOM1 级和 DOM0 级一样
  • DOM2 级事件监听:可以通过设置参数来决定监听哪个阶段
    第一个参数:事件名没有 on
    第二个参数:事件处理函数
    第三个参数:布尔值(true/false),true表示监听捕获阶段;false表示监听冒泡阶段
oBox.addEventListener('click', function() {
    // 触发事件后的执行语句 
}, true);
  • 如果 既有捕获事件又有冒泡事件,事件传播的顺序是:先捕获阶段后冒泡阶段
  • 最内层的执行顺序是谁在前先执行谁。下面示例中,由于最内层box3的捕获阶段在冒泡阶段之前,所以先执行捕获阶段
<div id="box1">
    <div id="box2">
        <div id="box3"></div>
    </div>
</div>

// JS 代码:
var oBox1 = document.getElementById('box1');
var oBox2 = document.getElementById('box2');
var oBox3 = document.getElementById('box3');

oBox1.addEventListener('click', function() {
    console.log('我是box1的捕获阶段');
}, true);
oBox2.addEventListener('click', function() {
    console.log('我是box2的捕获阶段');
}, true);
oBox3.addEventListener('click', function() {
    console.log('我是box3的捕获阶段');
}, true);

oBox1.addEventListener('click', function() {
    console.log('我是box1的冒泡阶段');
}, false);
oBox2.addEventListener('click', function() {
    console.log('我是box2的冒泡阶段');
}, false);
oBox3.addEventListener('click', function() {
    console.log('我是box3的冒泡阶段');
}, false);

  • 对于 DOM0 级和 DOM2 冒泡阶段事件同时存在,谁在前先执行谁

注意事项:

  • 最内部元素不再区分捕获和冒泡阶段,会先执行写在前面的监听,然后执行写在后面的监听
  • 如果给元素设置相同的两个或多个同名事件,则 DOM0 级写法 后写的会覆盖先写的;而 DOM2 级会按照顺序执行
<div id="box1">
    <div id="box2">
        <div id="box3"></div>
    </div>
</div>

// JS 代码:
var oBox1 = document.getElementById('box1');
var oBox2 = document.getElementById('box2');
var oBox3 = document.getElementById('box3');

oBox1.onclick = function() {
    console.log('A');
};
oBox1.onclick = function() {
    console.log('B');
};

oBox1.addEventListener('click', function() {
    console.log('C');
}, false);
oBox1.addEventListener('click', function() {
    console.log('D');
}, false);

// 输出结果
B C D

(3)事件对象

  • 事件对象: 事件处理函数提供一个形式参数,它是一个对象,封装了本次事件的细节
  • 这个参数通常用 单词 event 或 字母 e 来表示
// 对象 e 就是这次事件的“事件对象”
oBox.onclick(e) {
    // 事件被触发时执行的语句
}

在鼠标事件当中,描述鼠标位置的一些属性:

属性属性描述
offsetX鼠标指针相对于 事件源元素 的水平坐标
offsetY鼠标指针相对于 事件源元素 的垂直坐标
clientX鼠标指针相对于 浏览器 的水平坐标
clientY鼠标指针相对于 浏览器 的垂直坐标
pageX鼠标指针相对于 整张网页 的水平坐标
pageY鼠标指针相对于 整张网页 的垂直坐标
<div id = "box"></div>

// JS 代码:
var oBox = document.getElementById('box');
oBox.onmousemove = function(e) {
    console.log('offsetX/Y: ' + e.offsetX + ' , ' + e.offsetY);
    console.log('clientX/Y: ' + e.clientX + ' , ' + e.clientY);
    console.log('pageX/Y: ' + e.pageX + ' , ' + e.pageY);
};

按键的 e.charCode 和 e.keyCode 属性:

  • e.charCode 属性通常用于 onkeypress 事件中,表示用户输入的字符的“字符码”
  • e.keyCode 属性通常用于 onkeydown 事件和 onkeyup 事件中,表示用户按下的按键的“键码”

charCode 字符码:

字符字符码
数字 0~948 ~ 57
大写字母 A~Z65 ~ 90
小写字母 a~z97 ~ 122

keyCode 键码:

按键键码
数字 0~948 ~ 57(与charCode字符码完全相同)
字母不分大小写 a~z65 ~ 90 (与charCode字符码的大写字母A~Z相同,而keyCode不分大小写,一律为65~90)
四个方向键:左⬅️、上⬆️、右➡️、下⬇️37、38、39、40
回车键13
空格键32
  • 小案例:通过键盘方向键控制页面上的盒子移动
// html
<div id="box"></div>
// CSS
#box {
    position: absolute;
    top: 200px;
    left: 200px;
    width: 100px;
    height: 100px;
    background-color: blue;
}
// JS 代码:
var oBox = document.getElementById('box');

// 定义全局变量 t、l分别表示盒子的 top属性和left属性值
var t = 200;
var l = 200;

// 监听document对象的键盘按下事件监听,表示用户在整个网页上按下按键
document.onkeydown = function(e) {
    // 条件是以后按下的方向键左37、上38、右39、下40
    switch(e.keyCode) {
        case 37:
            l -= 10;
            break;
        case 38:
            t -= 10;
            break;
        case 39:
            l += 10;
            break;
        case 40:
            t += 10;
            break;
    }
    // 更改样式
    oBox.style.top = t + 'px';
    oBox.style.left = l + 'px';
};

e.preventDefault() 方法:

  • e.preventDefault() 方法用来 阻止事件产生的“默认动作”
  • 一些特殊的业务需求需要阻止事件的“默认动作”

小案例1: 制作一个文本框,只能让用户在其中输入小写字母和数字,其它字符输入没有效果

<div>
    <p>只能输入小写字母和数字:</p>
    <input type="text" id="field">
</div>

//JS 代码
var oField = document.getElementById('field');
oField.onkeypress = function(e) {
    // 根据用户输入字符的字符码(e.charCode)决定是否显示
    // 数字0-9字符码为:48-57
    // 小写字母a-z字符码为:97-122
    if(!(e.charCode >= 48 && e.charCode <= 57 || e.charCode >= 97 && e.cahrCode <= 122)) {
        // 阻止浏览器的默认行为:输入一个字符显示一个字符的行为
        e.preventDefault();
    }
};

小案例2: 制作鼠标滚轮事件:当鼠标在盒子中向下滚动时,数字加1,反之,数字减1

  • 鼠标滚轮事件是 onmousewheel,它的事件对象 e 提供 deltaY 属性表示鼠标滚动方向,向下滚动时返回正值,向上滚动时返回负值
<div id="box"></div>
<h1 id="info">0</h1>

//JS 代码
var oBox = document.getElementById('box');
var oInfo = document.getElementById('info');

// 定义全局变量
var a = 0;
// 给盒子添加鼠标滚轮滚动事件监听
oField.onmousewheel = function(e) {
    // 阻止浏览器的默认行为:当用户在盒子里面滚动鼠标滚轮的时候,此时不会引起页面滚动条的滚动
    e.preventDefault();
    if(e.deltaY > 0) {
        a++;
    } else {
        a--;
    }
    oInfo.innerText = a;
};

e.stopPropagation() 方法:

  • e.stopPropagation() 方法用来 阻止事件继续传播
  • 在一些场合,非常有必要切断事件继续传播,否则会造成页面特效显示出bug
    小案例:制作一个弹出层:点击按钮显示弹出层,点击网页任意地方,弹出层关闭
<button id="btn">点我弹出弹出层</button>
<div class="model" id="model"></div>

// JS代码
var oBtn = document.getElementById('btn');
var oModel = document.getElementById('model');

// 点击按钮的时候,弹出弹出层
oBtn.onclick = function(e) {
    // 阻止事件传播到document 身上
    e.stopPropagation();
    oModel.style.display = "block";
};

// 点击页面任何地方,关闭弹出层
document.onclick = function() {
    oModel.style.display = 'none';
};

// 点击弹出层内部的时候,不能关闭弹出层,所以应该阻止事件继续传播
oModel.onclick = function(e) {
    // 阻止事件继续传播到 document 身上
    e.stopPropagation();
};

(4)事件委托

问题1(批量添加): 页面上有一个无序列表 < ul >,它内部有多个< li > 元素,请批量给它们添加点击事件监听,实现效果:点击哪个 < li > 元素,哪个 < li > 元素就变红

<ul id="list">
    <li>列表项</li>
    <li>列表项</li>
    <li>列表项</li>
    <li>列表项</li>
    <li>列表项</li>
    <li>列表项</li>
    <li>列表项</li>
    <li>列表项</li>
</ul>

// JS 代码
var oList = document.getElementById('list');
var lis = oList.getElementsByTagName('li');
// 书写循环语句,批量给元素添加监听
for(var i = 0; i < lis.length; i++) {
    lis[i].onclick = function() {
        this.style.color = 'red';
    };
}

批量事件监听的性能问题:

  • 每一个事件监听注册都会消耗一定的系统内存,而批量添加事件会导致监听数量太多,内存消耗非常大
  • 实际上,每一个批量标签(如: li 标签)的事件处理函数都是不同的函数,存放这些函数本身也会占用内存

问题2(动态绑定): 页面上有一个无序列表 < ul >,它内部没有< li > 元素,请制作一个按钮,点击这个按钮就能增加一个< li >元素,并为每个增加的< li >元素添加点击事件,实现效果是点击哪个< li > 元素,哪个< li > 元素就变红

<button id="btn">点我添加新的li列表项</button>
<ul id="list"></ul>

// JS 代码
var oBtn = document.getElementById('btn');
var oList = document.getElementById('list');

// 按钮点击事件
oBtn.onclick = function() {
    // 创建一个新的 li 列表项,孤儿节点
    var oLi = document.createElement('li');
    oLi.innerHTML = '我是列表项';
    // 上树
    oList.appendChild(oLi);
    // 给新创建的这个 li 节点添加 onclick 事件监听
    oLi.onclick = function() {
        this.style.color = 'red';
    };
};

动态绑定事件的性能问题:

  • 新增元素必须分别添加事件监听,不能自动获得事件监听
  • 大量事件监听、大量事件处理函数都会 产生大量内存消耗

事件委托:

  • 事件委托:就是 利用事件冒泡机制,将后代元素事件委托给祖先元素
  • e.target 和 e.currentTarget 属性: 事件委托通常需要结合使用 e.target 属性
  • 当有大量类似元素需要批量添加事件监听时,使用时间委托可以减少内存开销
  • 当有动态元素节点上树时,使用事件委托可以让新上树的元素具有事件监听
属性属性描述
target触发此事件的最早元素,用户真正触发的那个元素,即“事件源元素”
currentTarget事件处理程序附加到的元素

使用事件委托时需要注意的事项:

  • onmouseenter 和 onmouseover 都表示“鼠标进入”,它们的区别是:onmouseenter 不冒泡,onmouseover 冒泡
  • 使用事件委托时要注意:不能委托不冒泡的事件给祖先元素
  • 最内层元素不能再有额外的内层元素了(若最内层还存在内内层,当触发内内层时,内内层执行事件语句,而最内层不执行;而触发最内层时,最内层及最内层里面的内内层都执行事件语句)

(5)定时器和延时器

定时器:

  • setInterval() 函数可以 重复调用一个函数,在每次调用之间具有固定的时间间隔
  • 第一个参数是函数
  • 第二个参数是间隔时间,单位为:毫秒(ms),1秒 = 1000毫秒
setInterval(function() {
    // 这个函数将自动被以固定间隔时间调用
},5000);
  • setInterval() 函数可以接收第3、4、5 ……个参数,它们将按顺序传入到函数的行参列表中
  • 从第三个参数开始,表示传入函数内的参数
setInterval(function(a, b) {
    // 函数行参中 a对应33,b对应55
},3000, 33, 55);
  • 具名函数也可以传入 setInterval,具名函数当做第一个参数,注意没有圆括号
function fun() {
    // 函数体语句
}
// 具名函数当做第一个参数,注意这里没有圆括号
setInterval(fun, 2000);

清除定时器:

  • clearInterval() 函数可以清除一个定时器
// 设置定时器,并且用变量接收这个定时器
var timer = setInterval(function() {
    // 函数体语句
}, 3000);

// 点击按钮时,清除定时器
oBox.onclick = function() {
    // 清除定时器时,要传入定时器变量
    clearInterval(timer);
};
<h1 id="info"></h1>
<button id="btn1">开始</button>
<button id="btn1">暂停</button>

// JS 代码:
var oInfo = document.getElementById('info');
var oBtn1 = document.getElementById('btn1');
var oBtn2 = document.getElementById('btn2');
// 定义全局变量
var timer;
var a = 0;
// 点击开始,计时器开始运行
oBox1.onclick = function() {
    // 为了防止定时器叠加,应该在设置定时器之前先清除定时器
    clearInterval(timer);
    // 设置定时器
    timer = setInterval(function() {
        // 函数体语句
        oInfo.innerText = ++a;
    }, 1000);
};
// 点击按钮时,清除定时器
oBox2.onclick = function() {
    // 清除定时器时,要传入定时器变量
    clearInterval(timer);
};

延时器:

  • setTimeout() 函数可以设置一个延时器,当指定时间到达之后,会执行函数一次,之后不再重复执行
setTimeout(function() {
    // 这个函数会在延迟2秒后执行一次
}, 2000);
  • 清除延时器:clearTimeout() 函数可以清除延时器,和 clearInterval() 非常相似
<button id="btn1">2秒后弹出你好</button>
<button id="btn1">取消弹出</button>

// JS 代码:
var oBtn1 = document.getElementById('btn1');
var oBtn2 = document.getElementById('btn2');
// 定义全局变量
var timer;
// 点击开始,计时器开始运行
oBox1.onclick = function() {
    // 设置定时器
    timer = setTimeout(function() {
        // 函数体语句
        alert('你好');
    }, 2000);
};
// 点击按钮时,清除定时器
oBox2.onclick = function() {
    // 清除定时器时,要传入定时器变量
    clearTimeout(timer);
};

初步认识一步语句:

  • setInterval() 和 setTimeout() 是两个异步语句
  • 异步(asynchronous):不会阻塞CPU继续执行其它语句,当异步完成时,会执行“回调函数(callback)”
  • 异步语句不会阻塞程序的正常执行
setTimeout(function() {
    console.log('A');
}, 2000);

console.log('B');

// 输出结果
B A

使用定时器实现动画:

  • 使用定时器可以实现动画,利用的是“视觉暂留”原理

4、JS 和 CSS 结合实现动画

  • CSS3的 transition 过渡属性可以实现动画
  • JS 可以利用CSS3的 transition 属性实现元素动画
  • JS 和 CSS3 结合实现动画规避了定时器制作动画的缺点
<button id="btn">移动</button>
<div id="box"></div>
// CSS
#box {
    width: 100px;
    height: 100px;
    background-color: orange;
    position: absolute;
    top: 100px;
    left: 100px;
}
// JS 代码
var oBtn = document.getElementById('btn');
var oBox = document.getElementById('box');
// 标识量  0:表示在左边,1:表示在右边
var pos = 0;  
// 事件监听
oBtn.onclick = function() {
    // 加上过度
    oBox.style.transition = 'all 2s linear 0s';
    if(pos == 0) {
        // 瞬间移动,但由于有过渡,所以是动画
        oBox.style.left = '1100px';
        pos = 1;
    } else {
        oBox.style.left = '100px';
        pos = 0;
    }
};

(1)函数节流

  • 上面的示例有一个问题,那就是当用户频繁点击移动时,盒子会由于点击事件而迅速改变方向移动,也就是说,当点击间隔时间小于过渡时间时,盒子会没有完成上一次点击的动画而开始执行下一次,导致动画不完整
var lock = true;
function 需要节流的函数() {
    // 如果锁是关闭状态,则不执行
    if(!lock) return;
    // 函数核心语句

    // 关锁
    lock = false;
    // 指定毫秒数后将锁打开
    setTimeout(function() {
        lock = true;
    }, 2000);
};
  • 通过函数节流的方式可以让每次点击后的动画都能完整执行
<button id="btn">移动</button>
<div id="box"></div>
// CSS
#box {
    width: 100px;
    height: 100px;
    background-color: orange;
    position: absolute;
    top: 100px;
    left: 100px;
}
// JS 代码
var oBtn = document.getElementById('btn');
var oBox = document.getElementById('box');
// 标识量  0:表示在左边,1:表示在右边
var pos = 0;  
// 函数节流锁
var lock = true;
// 事件监听
oBtn.onclick = function() {
    // 检查锁是否关闭
    if(!lock) return;
    // 加上过度
    oBox.style.transition = 'all 2s linear 0s';
    if(pos == 0) {
        // 瞬间移动,但由于有过渡,所以是动画
        oBox.style.left = '1100px';
        pos = 1;
    } else {
        oBox.style.left = '100px';
        pos = 0;
    }
    // 关锁
    lock = false;
    // 设置指定毫秒数打开,一般设置为动画过渡时间
    setTimeout(function() {
        lock = true;
    }, 2000);
};

(2)动画效果开发1 - 无缝连续滚动特效

<div id="box" class="box">
    <ul id="list">
        <li><img src="/images/number/0.png" alt=""></li>
        <li><img src="/images/number/1.png" alt=""></li>
        <li><img src="/images/number/2.png" alt=""></li>
        <li><img src="/images/number/3.png" alt=""></li>
        <li><img src="/images/number/4.png" alt=""></li>
        <li><img src="/images/number/5.png" alt=""></li>
    </ul>
</div>
// CSS
* {
    margin: 0;
    padding: 0;
}
.box {
    width: 1000px;
    height: 130px;
    border: 1px solid #000;
    margin: 50px auto;
    overflow: hidden;
}
.box ul {
    list-style: none;
    width: 5000px;
    position: relative;
}
.box ul li {
    float: left;
    margin-right: 10px;
}

// JS 代码
var oBox = document.getElementById('box');
var oList = document.getElementById('list');

// 复制一遍所有的 li
oList.innerHTML += oList.innerHTML;
// 全局变量,表示当前 list 的 left 值
var left = 0;
var timer;
move();
function move() {
    // 防止动画累积
    clearInterval(timer);
    // 定时器
    timer = setInterval(function() {
        left -= 6;
        if(left <= -1260) {
            left = 0;
        }
        oList.style.left = left + 'px';
    },20);
};

// 添加鼠标进入ul元素,停止定时器
oBox.onmouseenter = function() {
    clearInterval(timer);
};
// 鼠标离开继续执行定时器
oBox.onmouseleave = function() {
    move();
};

(3)动画效果开发2 - 跑马灯轮播图特效

<div class="carousel">
    <ul id="list">
        <li><img src="/images/0.png" alt=""></li>
        <li><img src="/images/1.png" alt=""></li>
        <li><img src="/images/2.png" alt=""></li>
        <li><img src="/images/3.png" alt=""></li>
        <li><img src="/images/4.png" alt=""></li>
    </ul>
</div>
<a href="javascript:;" class="leftbtn" id="leftbtn"></a>
<a href="javascript:;" class="rightbtn" id="rightbtn"></a>

// CSS
* {
    margin: 0;
    padding: 0;
}
.carousel {
    width: 650px;
    height: 360px;
    border: 1px solid #000;
    margin: 50px auto;
    position: relative;
    overflow: hidden;
}
.carousel ul {
    list-style: none;
    width: 6000px;
    position: relative;
    left: 0px;
    transition: left .5s ease 0s;
}
.carousel ul li {
    float: left;
}
.carousel .leftbtn {
    position: absolute;
    left: 20px;
    top: 50%;
    margin-top: -25px;
    width: 50px;
    height: 50px;
    background-color: rgb(28,180,226);
    border-radius: 50%;
}
.carousel .rightbtn {
    position: absolute;
    right: 20px;
    top: 50%;
    margin-top: -25px;
    width: 50px;
    height: 50px;
    background-color: rgb(28,180,226);
    border-radius: 50%;
}

// JS 代码
// 得到按钮和ul,ul整体进行运动
var rightbtn = document.getElementById('rightbtn');
var leftbtn = document.getElementById('leftbtn');
var list = document.getElementById('list');

// 克隆第一张图片
var cloneli = list.firstElementChild.cloneNode(true);
// 将克隆的图片追加到最后
list.appendChild(cloneli);
// 定义全局变量,当前ul显示到第几张了,从0开始数
var idx = 0;
// 函数节流锁
var lock = true;
// 右边按钮监听
rightbtn.onclick = function() {
    // 判断锁的状态
    if(!lock) return;
    // 关锁
    lock = false;
    // 因为最后一张图片会把过渡去掉,所以需要在这给list加过渡
    list.style.transition = 'left .5s ease 0s';
    idx ++; 
    if(idx > 4) {
        // 设置一个延时器,延时器的功能是将ul瞬间拉回0的位置
        setTimeout(function() {
            // 取消过渡,因为要瞬间移动到第一张图片
            list.style.transition = 'none';
            list.style.left = 0;
            idx = 0;
        },500);
    }
    list.style.left = -idx * 650 + 'px';
    //函数节流
    setTimeout(function() {
        lock = true;
    },500);
};
// 左边按钮监听
leftbtn.onclick = function() {
    // 判断锁的状态
    if(!lock) return;
    // 关锁
    lock = false;
    // 判断是不是第0张,如果是,就要瞬间用假的替换真的
    if(idx == 0) {
        // 取消过渡
        list.style.transition = 'none';
        // 直接瞬间移动到最后的假图片上
        list.style.left = -5 * 650 + 'px';
        // 设置一个延时器,这个延时器的延时时间可以是0毫秒
        // 虽然是0毫秒,但是可以让我们过渡先是瞬间取消过渡,然后再加上
        setTimeout(function() {
            // 加过渡
            list.style.transition = 'left .5s ease 0s';
            // idx改为真正的最后一张图片的编号
            idx = 4;
            list.style.left = -idx * 650 + 'px';
        },0);
    } else {
        idx --;
        list.style.left = -idx * 650 + 'px';
    }
    // 函数节流
    setTimeout(function() {
        lock = true;
    },500);
};

(4)动画效果开发3 - 呼吸轮播图特效

<div class="carousel">
    <ul id="list">
        <li><img src="/images/0.png" alt=""></li>
        <li><img src="/images/1.png" alt=""></li>
        <li><img src="/images/2.png" alt=""></li>
        <li><img src="/images/3.png" alt=""></li>
        <li><img src="/images/4.png" alt=""></li>
    </ul>
</div>
<a href="javascript:;" class="leftbtn" id="leftbtn"></a>
<a href="javascript:;" class="rightbtn" id="rightbtn"></a>

// CSS
* {
    margin: 0;
    padding: 0;
}
.carousel {
    width: 650px;
    height: 360px;
    border: 1px solid #000;
    margin: 50px auto;
    position: relative;
}
.carousel ul {
    list-style: none;
}
.carousel ul li {
   position: absolute;
   top: 0;
   left: 0;
   /* 后面图片透明度为0 */
   opacity: 0;
   transition: opacity 1s ease 0s;
}
.carousel ul li:first-child {
    /* 第一张图片透明度为1 */
    opacity: 1;
}
.carousel .leftbtn {
    position: absolute;
    left: 20px;
    top: 50%;
    margin-top: -25px;
    width: 50px;
    height: 50px;
    background-color: rgb(28,180,226);
    border-radius: 50%;
}
.carousel .rightbtn {
    position: absolute;
    right: 20px;
    top: 50%;
    margin-top: -25px;
    width: 50px;
    height: 50px;
    background-color: rgb(28,180,226);
    border-radius: 50%;
}

// JS 代码
// 得到按钮和ul,ul整体进行运动
var rightbtn = document.getElementById('rightbtn');
var leftbtn = document.getElementById('leftbtn');
var list = document.getElementById('list');
var lis = document.getElementsByTagName('li');
// 当前显示的是第几张图
var idx = 0;
// 节流
var lock = true;
// 右边按钮监听
rightbtn.onclick = function() {
    // 判断锁的状态
    if(!lock) return;
    // 关锁
    lock = false;
    // 还没有改 idx,此时的idx这个图片就是老图,老图淡出
    lis[idx].style.opacity = 0;
    idx ++; 
    if(idx > 4) idx = 0;
    // 改了 idx,此时的idx这个图片就是新图,新图淡入
    lis[idx].style.opacity = 1;
    //函数节流
    setTimeout(function() {
        lock = true;
    },1000);
};
// 左边按钮监听
leftbtn.onclick = function() {
    // 判断锁的状态
    if(!lock) return;
    // 关锁
    lock = false;
    // 还没有改 idx,此时的idx这个图片就是老图,老图淡出
    lis[idx].style.opacity = 0;
    idx --; 
    if(idx < 0) idx = 4;
    // 改了 idx,此时的idx这个图片就是新图,新图淡入
    lis[idx].style.opacity = 1;
    //函数节流
    setTimeout(function() {
        lock = true;
    },1000);
};

小结

  • 访问元素节点有哪些方法?
  • 节点的关系有哪些?
  • 常用的节点操作有哪些?
  • 节点的创建、移除和克隆要如何实现?
  • 事件捕获和冒泡是什么?应该如何设置?
  • 什么是事件委托?什么时候要用事件委托?
  • 使用定时器和CSS3的过渡实现动画

五、BOM

1、BOM 常用对象

(1)BOM 简介

  • BOM(Browser Object Model,浏览器对象模型)是 JS 与浏览器窗口交互的接口
  • 一些与浏览器改变尺寸、滚动条滚动相关的特效,都需要借助BOM技术

(2)window 对象

  • window对象是当前 JS脚本运行所处的窗口,而这个窗口中包含DOM结构,window.document 属性就是 document对象
  • 在有标签功能的浏览器中,每个标签都拥有自己的 window 对象;也就是说,同一个窗口的标签之间不会共享一个 window 对象
  • 全局变量会成为 window 对象的属性
var a = 10;
console.log(window.a == a); // 输出 true
  • 这就意味着,多个 JS 文件之间是共享全局作用域的,即 JS文件没有作用域隔离功能
  • 内置函数普遍是 window 的方法,如setInterval()、alert()等内置函数
console.log(window.alert == alert); // true
console.log(window.setInterval == setInterval); // true

窗口尺寸相关的参数:

  • 获得不包含滚动条的窗口宽度,要用 document.documentElement.clientWidth
  • 获得不包含滚动条的窗口高度,要用 document.documentElement.clientHeight
属性意义
innerHeight浏览器窗口的内容区域的高度,包含水平滚动条(如果有的话)
innerWidth浏览器窗口的内容区域的宽度,包含垂直滚动条(如果有的话)
outerHeight浏览器窗口的外部高度
outerWidth浏览器窗口的外部宽度

resize 事件:

  • 在窗口大小改变之后,就会触发 resize 事件,可以使用 window.onresize 或者 window.addEventListener(‘resize’) 来绑定事件处理函数

已卷动高度:

  • window.scrollY 属性表示在垂直方向已滚动的像素值
  • document.documentElement.scrollTop 属性也表示窗口卷动高度
// 为了更好的支持浏览器兼容性,使用下面这种方式
var scrollTop = window.scrollY || document.documentElement.scrollTop;
  • document.documentElement.scrollTop 不是只读的,而 window.scrollY 是只读的

scroll 事件:

  • 在窗口被卷动之后,就会触发 scroll 事件,可以利用 window.onscroll 或者 window.addEventListener(‘scroll’) 来绑定事件处理函数

(3)Navigator 对象

  • navigator 有导航的意思
  • window.navigator 属性可以检索 navigator 对象,它内部含有用户此次活动的 浏览器的相关属性和标识
属性意义
appName浏览器官方名称
appVersion浏览器版本
userAgent浏览器的用户代理(含有内核信息和封装壳信息)
platform用户操作系统

(4)History 对象

  • window.history 对象提供了 操作浏览器会话历史 的接口
  • 常用操作就是模拟浏览器回退按钮
history.back();  // 等同于点击浏览器的回退按钮
history.go(-1);  // 等同于 history.back();
// 这是第一个页面
<h1>我是temp网页</h1>
<a href="test.html">点我进入第二个页面</a>

// 我是第二个页面
<h1>我是test.html网页</h1>
<button id="btn">回退</button>

// JS代码
var btn = document.getElementById('btn');
btn.onclick = function() {
    // history.back();
    history.go(-1);
}

(5)Location 对象

  • window.location 标识当前所在网址,可以通过给这个属性赋值命令浏览器进行页面跳转
window.location = 'http://www.baidu.com';
window.location.href = 'http://www.baidu.com';

重新加载当前页面:

  • 可以调用 location 的 reload 方法以 重新加载当前页面,参数 true 表示强制从服务器加载
window.location.reload(true);

GET 请求的查询参数:

  • window.location.search 属性即为当前浏览器的 GET 请求的查询参数
比如网址:https://www.baidu.com/?a=1&b=2

console.log(window.location.search); //得到 ?a=1&b=2

2、BOM 特效开发

(1)返回顶部按钮制作

  • 返回顶部的原理:改变 document.documentElement.scrollTop 属性,通过定时器逐步改变此值,则将用动画形式返回顶部
body {
    height: 5000px;
}
.backtotop {
    width: 60px;
    height: 60px;
    background-color: rgba(233,233,233,.6);
    position: fixed;
    bottom: 100px;
    right: 100px;
    /* 小手状 */
    cursor: pointer;
}

<div class="backtotop" id="backtotopbtn">返回顶部</div>

var backtotopbtn = document.getElementById('backtotopbtn');
// 设置全局变量
var timer;

backtotopbtn.onclick = function() {
    // 设表先关
    clearInterval(timer);
    // 设置定时器
    timer = setInterval(function() {
        // 不断让 scrollTop减少
        document.documentElement.scrollTop -= 200;
        // 停止定时器
        if(document.documentElement.scrollTop <= 0) {
            clearInterval(timer);
        }
    },20);
};

(2)楼层导航效果制作

  • DOM 元素都有 offsetTop 属性,表示 此元素到定位祖先元素的垂直距离
  • 定位祖先元素:在祖先中,离自己最近的且拥有定位属性的元素
  • 使用这个属性时,所有祖先元素不要有定位,有定位就不好使用了
* {
    margin: 0;
    padding: 0;
}
#content-part {
    width: 1000px;
    margin:0px auto;
    margin-bottom: 30px;
    background-color: #ccc;
    font-size: 50px;
}
.floornav {
    position: fixed;
    right: 40px;
    top: 50%;
    margin-top: -100px;
    width: 120px;
    height: 200px;
    background-color: orange;
}
.floornav ul {
    list-style: none;
}
.floornav ul li {
    width: 120px;
    height: 40px;
    line-height: 40px;
    text-align: center;
    font-size: 26px;
    /* 小手指针 */
    cursor: pointer;
}
.floornav ul li.current {
    background: purple;
    color: white;
}
// html
<div class="floornav">
    <ul id="list">
        <li data-n="科技"  class="current">科技</li>
        <li data-n="体育">体育</li>
        <li data-n="新闻">新闻</li>
        <li data-n="娱乐">娱乐</li>
        <li data-n="视频">视频</li>
    </ul>
</div>
<section class="content-part" style="height:674px;" data-n="科技">科技栏目</section>
<section class="content-part" style="height:654px;" data-n="体育">体育栏目</section>
<section class="content-part" style="height:622px;" data-n="新闻">新闻栏目</section>
<section class="content-part" style="height:432px;" data-n="娱乐">娱乐栏目</section>
<section class="content-part" style="height:526px;" data-n="视频">视频栏目</section>
// JS 代码
var list = document.getElementById('list');
var contentParts = document.querySelectorAll('.content-part');
var lis = document.querySelectAll('#list li');

// 使用事件委托给 li添加监听
list.onclick = function(e) {
    if(e.target.targetName.toLowerCase() == 'li') {
        // getAttribute表示得到标签上的某个属性值
        var n = e.target.getAttribute('data-n');
        // 可以使用属性选择器(就是方括号选择器)来寻找带有相同data-n的content-part
        var contentPart = document.querySelector('.content-part[data-n=' + n + ']');
        // 让页面的卷动自动成为这个盒子的offsetTop值
        docuemnt.documentElement.scrollTop = contentPart.offsetTop;
    }
};
// 在页面加载好后,将所有的content-part盒子的offsetTop值推入数组
var offsetTopArr = [];
// 遍历所有的 contentPart,将他们的净位置推入数组
for(var i = 0; i < contentParts.length; i++) {
    offsetTopArr.push(contentParts[i].offsetTop);
}
// 为了最后一项可以方便比较,我们可以推入一个无穷大
offsetTopArr.push(Infinity);
// 当前所在楼层
var nowfloor = -1;
// 窗口的卷动
window.onscroll = function() {
    var scrollTop = document.documentElement.scrollTop;
    // 遍历offsetTopArr数组,看看当前的scrollTop 值在哪两个楼层之间
    for(var i = 0; i < offsetTopArr.length; i++) {
        if(scrollTop > offsetTopArr[i] && scrollTop < offsetTopArr[i + 1]) {
            break;
        }
    }
    // 退出循环的时候,i是几就表示当前楼层是几
    // 如果当前所在楼层不是i,表示换楼了
    if(nowfloor != i) {
        // 让全局变量改变为这个楼层号 
        nowfloor = i;
        // 设置下标为i的项有cur
        for(var j= 0; j < lis.length; j++) {
            if(j == i) {
                lis[j].className = 'current';
            } else {
                lis[j].className = '';
            }
        }
    }
};

小结

  • 窗口相关属性有哪些?
  • 窗口卷动事件是什么?如何得到卷动值?
  • 要会使用 Navigator 对象、History 对象、Location 对象 常用属性和方法

六、JS 面向对象

1、认识对象

(1)认识对象

  • 对象(Object)是 “键(key)值(value)对”的集合,表示 属性和值的映射关系
  • JS 中,大括号表示对象
var zhangsan = {
    name: '张三',
    age: 12,
    sex: '男',
    hobbies: ['玩游戏','编程','踢足球']
};
  • k 和 v 之间用冒号(“:”)分隔,每组 k:v 之间用逗号分隔,最后一个 k:v 对后可以不书写逗号
  • 属性是否加引号:如果对象的属性键名不符合 JS 标识符命名规范(不能是关键字或保留字),则这个键名必须用引号包裹
var zhangsan = {
    name: '张三',
    // 属性名中有短横,不符合JS标识符命名规范,属性名必须加引号包裹
    'favorite-book': '三体'
};

属性的访问:

  • 可以用“点语法”访问对象中指定的值,例:zhangsan.name;
  • 如果属性名不符合JS标识符命名规范,则必须用方括号的写法来访问,例:zhangsan[‘favorite-book’];
  • 如果属性名以变量形式存储,则必须使用方括号形式访问
var obj = {
    a: 1,
    b: 3
};
// 属性名用变量存储
var key = 'b';
console.log(obj.key);   // undefined
console.log(obj[key]);  // 3

属性的更改:

  • 直接使用赋值运算符重新对某属性赋值即可更改属性
var obj = {
    a: 10
};
obj.a = 30;
console.log(obj.a);  // 30

属性的新增(创建):

  • 如果对象本身没有某个属性值,则用点语法赋值时,这个属性会被创建出来
var obj = {
    a: 10
};
obj.b = 30;

属性的删除:

  • 如果要删除某个对象的属性,需要使用 delete 操作符
var obj = {
    a: 10,
    b: 20
};
delete obj.b;

(2)对象方法

  • 如果 某个属性值是函数,则它也被称为对象的 方法
var xiaoming = {
    name: '小明',
    age: 18,
    sex: '男',
    sayHello: function() {
        console.log('小明的sayHello方法!')
    }
};
  • 使用“点语法”可以调用对象的方法,例:xiaoming.sayHello();
  • 方法和函数的关系: 方法也是函数,只不过是作为对象的属性存在,它需要用对象打点调用

(3)对象遍历

  • 和遍历数组类似,对象也可以被遍历,遍历对象需要使用 for … in … 循环
  • 使用 for … in … 循环可以遍历对象的每一个
// k:循环变量,它会依次成为对象的每一个键
// obj:要遍历的对象
for(var k in obj) {
    console.log('属性' + k + '的值是:' + obj[k]);
}

(4)对象的深克隆与浅克隆

举例当 var a = b 变量传值时当用 == 比较时
基本类型数字、字符串、布尔型、undefined、null内存中产生新的副本比较值是否相等
引用类型对象、数组等(还包括函数、正则表达式)内存中不产生新的副本,而是让新的变量指向同一个对象比较内存地址是否相同,即比较是否是同一个对象

对象是引用类型值,这意味着:

  • 不能用 var obj1 = obj2; 这样的语法克隆一个对象
  • 使用 == 或 === 进行对象的比较时,比较的是它们是否为内存中的同一个对象,而不是比较值是否相同

对象浅克隆:

  • 只克隆对象的“表层”,如果对象的某些属性又是引用类型值,则不进一步克隆它们,只是传递它们的引用
var obj1 ={
    a: 1,
    b: 2,
    c: [11,22,33]
};
var obj2 = {};
// 本质上 obj1 和 obj2 的 c属性还是同一块内存中的数组,并没有实现克隆
for(var k in obj1) {
    // 每遍历一个 k 属性,就给 obj2 页添加一个同名的 k属性
    // 且每个 k属性对应的值也和 obj1的相同
    obj2[k] = obj1[k];
}

console.log(obj1.c == obj2.c);  // true

对象的深克隆:

  • 克隆对象的“全貌”,不论对象的属性值是否是引用类型值,都能实现全部克隆
  • 和数组的深克隆类似,对象的深克隆需要使用递归
  • 数组的 typeof 也是 object,所以先检测数组
var obj1 ={
    a: 1,
    b: 2,
    c: [11,22,33,{
        m: 22,
        n:78,
        p:[65,23,21]
    }]
};

function deepClone(obj) {
    var result;
    // 要判断 obj 是对象还是数组
    // 数组的 typeof 也是 object,所以先检测数组
    if(Array.isArray(obj)) {
        // 数组
        for(var i = 0; i < obj.length; i++) {
            result.push(deepClone(obj[i]));
        }
    } else if(typeof obj == 'object') {
        // 对象
        for(var k in obj) {
            result[k] = deepClone(obj[k]);
        }
    } else {
        // 基本类型值
        result = obj;
    }
    return result;
};
var obj2 = deepClone(obj1);
obj1.c.push(99);
console.log(obj1);
console.log(obj2);

2、认识函数上下文

(1)认识函数上下文

  • 函数中可以使用 this 关键字 表示函数的上下文
  • 与中文中“这”类似,函数中的 this 具体指代什么 必须通过调用函数时的“前言后语”来判断
  • 函数的上下文由调用方式决定,函数如果不调用,则不能确定函数的上下文
  • 将函数提出来单独存为变量,函数不能加圆括号,例:var fn = obj.fun;
var obj = {
    a: 1,
    b: 2,
    fun: function() {
        console.log(this.a + this.b);
    }
};
obj.fun();  // 输出结果是 3
var fn = obj.fun;  // 将函数提出来单独存为变量
fn();  // 输出结果是 NaN

(2)函数上下文规则-1

  • 对象打点调用它的方法函数,则函数的上下文是这个打点的对象
对象.方法();

例题1:

  • 这题的 this 指代的是 obj 这个对象
function fun() {
    console.log(this.a + this.b);
};

var obj = {
    a: 3,
    b: 5,
    fn: fun
};

obj.fn();  // 输出 8

例题2:

  • 这题的 this 指代的是 obj2 这个对象
var obj1 = {
    a: 1,
    b: 3,
    fn: function() {
        console.log(this.a + this.b);
    }
};

var obj2 = {
    a: 2,
    b: 5,
    fun: obj1.fn();
};

obj2.fun();  // 输出 7

例题3:

  • 这题的 this 指代的是 outer 这个对象
function outer() {
    var a = 2;
    var b = 3;
    return {
        a: 11,
        b: 22,
        fun: function() {
            console.log(this.a + this.b);
        }
    };
};

outer.fun();  // 输出 33

例题4:

  • 这题的 this 指代的是 c[0] 这个对象
function fun() {
    console.log(this.a + this.b);
};
var obj = {
    a: 1,
    b: 2,
    c: [{
        a: 3,
        b: 4,
        c: fun
    }]
};
var a = 5;
obj.c[0].c();  // 输出 7

(3)函数上下文规则-2

  • 圆括号直接调用函数,则函数的上下文是window 对象
函数()

例题1:

var obj = {
    a: 1,
    b: 2,
    fun: function() {
        console.log(this.a + this.b);
    }
};
var a = 5;
var b = 6;
var fn = obj.fun;
fun();  // 输出 11

例题2:

function fun() {
    return this.a + this.b;
}

var a = 5;
var b = 6;
var obj = {
    a: 3,
    b: fun(),  // 此处调用时的 this 是 window
    fun: fun   // 此处调用时的 this 是 obj
};
var result = obj.fun();  // 此处调用时的 this 是 obj
console.log(result);  // 输出 14

(4)函数上下文规则-3

  • 数组(类数组对象)枚举出函数进行调用,上下文是这个数组(类数组对象)
  • 什么是类数组对象:所有键名为自然数序列(从0开始),且有length属性的对象
  • arguments 对象是最常见的类数组对象,它是函数的实参列表
数组[下标]()

例题1:

var arr = ['A','B','C',function(){
    console.log(this[1]);
}];

arr[3]();  // 输出 B

例题2:

function fun() {
    arguments[3]();
}
// 调用函数
fun('A','B','C',function(){
    console.log(this[1]);
});  // 输出 B

(5)函数上下文规则-4

  • IIFE中的函数,上下文是 window 对象
(function() {
    
})();

题目1:

var a = 1;
var obj = {
    a: 2,
    fun: (function {
        var a = this.a;  // tnis指代window,变量 a 的值为全局变量赋值的 1
        return function() {
            console.log(a + this.a);  // 此处的this指代 obj
        }
    })()
};

obj.fun();  // 输出 3

(6)函数上下文规则-5

  • 使用定时器、延时器调用函数,上下文是 window 对象
setInterval(函数, 时间);
setTimeout(函数, 时间);

题目1:

var obj = {
    a: 2,
    b: 3,
    fun: function {
        console.log(this.a + this.b);
    }
};
var a = 5;
var b = 6;
setTimeout(obj.fun, 2000);  // 输出 11

题目2:

var obj = {
    a: 2,
    b: 3,
    fun: function {
        console.log(this.a + this.b);
    }
};
var a = 5;
var b = 6;
setTimeout(function() {
    obj.fun();  // 此处适用规则1
}, 2000);  // 输出 5

(7)函数上下文规则-6

  • 事件处理函数的上下文是绑定事件的 DOM 元素
DOM元素.onclick = function() {
    
};

案例1: 请实现效果:点击哪个盒子,哪个盒子就变红,要求使用同一个事件处理函数实现(不能使用事件委托)

<div id="box1"></div>
<div id="box2"></div>
<div id="box3"></div>

// JS 
var box1 = document,getElementById('box1');
var box2 = document,getElementById('box2');
var box3 = document,getElementById('box3');

function setColorToRed() {
    this.style.backgroundColor = 'red';
}

box1.onclick = setColorToRed;
box2.onclick = setColorToRed;
box3.onclick = setColorToRed;

案例2: 请实现效果:点击哪个盒子,哪个盒子在2000毫秒后变红,要求使用同一个事件处理函数实现(不能使用事件委托)

<div id="box1"></div>
<div id="box2"></div>
<div id="box3"></div>

// JS 
var box1 = document,getElementById('box1');
var box2 = document,getElementById('box2');
var box3 = document,getElementById('box3');

function setColorToRed() {
    // 备份上下文
    var that = this;
    setTimeout(function() {
        that.style.backgroundColor = 'red';
    }, 2000);
}

box1.onclick = setColorToRed;
box2.onclick = setColorToRed;
box3.onclick = setColorToRed;

(8)call 和 apply 能指定函数的上下文

二者的区别(体现在有参数时):

  • call 要用逗号罗列参数
  • apply 要把参数写到数组中
// 没有参数
    函数.call(指定的上下文);
或者:
    函数.apply(指定的上下文);
// 有参数
    函数.call(指定的上下文, 参数1, 参数2 ...);
或者:
    函数.apply(指定的上下文, [参数1, 参数2 ...]);

示例:

function sum() {
    console.log(this.a + this.b + this.c);
};

var obj = {
   a: 100,
   b: 70,
   c: 90,
   // sum: sum
};

// 方法1: 可以在obj 中使用 sum: sum 指定上下文
// obj.sum();
// 方法2:可以使用 call 或 apply 的方式指定上下文(将 sum: sum 注释掉)
sum.call(obj);
// 或者 sum.apply(obj);

函数上下文小结

规则上下文
对象.函数()对象
函数()window
数组[下标] ()数组
IIFEwindow
定时器window
DOM事件处理函数绑定DOM的元素
call 和 apply任意指定
用 new 调用函数创建出对象

3、构造函数

(1)用 new 调用函数的 4 步骤

new 函数()

JS 规定,使用 new 操作符调用函数会进行“四步走”

  • 函数体内会自动创建一个空白对象
  • 函数的上下文(this)会指向这个对象
  • 函数体内的语句会执行
  • 函数会自动返回上下文对象,即使函数没有 return 语句

(2)构造函数

  • 用 new 调用一个函数,这个函数就被称为 “构造函数”,任何函数都可以是构造函数,只需要用 new 调用它
  • 构造函数用来“构造新对象”,它内部的语句将为新对象添加若干属性和方法,完成对象的初始化
  • 构造函数必须用 new 关键字调用,否则不能正常工作,正因为如此,开发者约定,构造函数命名时首字母要大写
  • 一个函数是不是构造函数,要看它是否用 new 调用,而至于名称首字母大写,完全是开发者的习惯约定
function People(name, age, sex) {
    this.name = name;
    this.age = age;
    this.sex = sex;
}

var xiaoming = new People('小明',12,'男');
var xiaoqiang = new People('小强',12,'男');
var xiaolan = new People('小兰',12,'女');
  • 如果不加 new 直接加圆括号调用,this 指代的是 window 对象
function People(name, age, sex) {
    this.name = name;
    this.age = age;
    this.sex = sex;
}

People('小明',12,'男');
People('小强',12,'男');
People('小兰',12,'女');
  • 构造函数中的 this 不是函数本身,而是创建后赋给的变量
function People(name, age, sex) {
    this.name = name;
    this.age = age;
    this.sex = sex;
    this.sayHello = function() {
        console.log('我是 '+ this.name + ',我今年 '+ this.age + '岁了');
    };
}

var xiaoming = new People('小明',12,'男');
var xiaoqiang = new People('小强',14,'男');
var xiaolan = new People('小兰',18,'女');
xiaoming.sayHello();
xiaoqiang.sayHello();
xiaolan.sayHello();

(3)类与实例

  • 类只描述对象会拥有哪些属性和方法,但并不具体指明属性的值
  • 实例是具体的对象
  • Java、C++等是“面向对象(OO,object-oriented)”语言
  • JavaScript是“基于对象(OB,object-based)”语言
  • JavaScript中的构造函数可以类比于OO语言中的“类”,写法类似,但和真正OO语言还是有本质不同

4、原型和原型链

(1)prototype 和 原型链查找

什么是 prototype:

  • 任何函数都有 prototype 属性,prototype 是“原型”的意思
  • prototype 属性值是个对象,它默认拥有 constructor 属性,该属性指回函数
  • 对普通函数来说,prototype 属性没有任何作用,而 构造函数的 prototype属性非常有用
  • 构造函数的 prototype 属性是它的实例的原型
  • 构造函数的实例可以调用 “双下划线proto双下划线”属性来查看实例的原型
function sum(a,b) {
    return a + b;
}
console.log(sum.prototype);  //sum的原型是函数,{constructor: function}
console.log(typeof sum.prototype);  // object
console.log(sum.prototype.constructor === sum);  //true

原型链查找:

  • JavaScript规定:实例可以打点访问它的原型的属性和方法,这称为“原型链查找”
function People(name, age, sex) {
    this.name = name;
    this.age = age;
    this.sex = sex;
}
// 在构造函数的 prototype 上添加 nationality 属性
People.prototype.nationality = '中国';
var xiaoming = new People('小明',12,'男');
// 实例可以打点访问原型的属性和方法
console.log(xiaoming.nationality);  // 输出:中国
  • 如果某个实例本身具有某个属性,就不会去原型上寻找该属性了;如果某个实例和它的原型都有某个属性,且属性值不同,则该实例本身的属性值会遮蔽原型上的值(是遮蔽不是替换,类似局部变量遮蔽同名的全局变量)
function People(name, age, sex) {
    this.name = name;
    this.age = age;
    this.sex = sex;
}
// 在构造函数的 prototype 上添加 nationality 属性
People.prototype.nationality = '中国';
var xiaoming = new People('小明',12,'男');
var tom = new People('汤姆',13,'男');

// 实例可以打点访问原型的属性和方法
console.log(xiaoming.nationality);  // 输出:中国
// 这里输出中国的原因是tom实例没有nationality属性,只能到原型上查找
console.log(tom.nationality);  // 输出:中国
// tom 实例添加nationality属性
tom.nationality = '美国';
console.log(tom.nationality);  // 输出:美国

hasOwnProperty方法:

  • hasOwnProperty 方法可以 检查对象是否真正“自己拥有”某个属性或者方法(区别于原型上的)
function People(name, age, sex) {
    this.name = name;
    this.age = age;
    this.sex = sex;
}
// 在构造函数的 prototype 上添加 nationality 属性
People.prototype.nationality = '中国';
var xiaoming = new People('小明',12,'男');

console.log(xiaoming.hasOwnProperty('name'));  // true
console.log(xiaoming.hasOwnProperty('age'));  // true
console.log(xiaoming.hasOwnProperty('sex'));  // true
console.log(xiaoming.hasOwnProperty('nationality'));  // false

in 运算符:

  • in 运算符只能 检查某个属性或方法是否可以被对象访问,不能检查是否是自己的属性或方法
// 接上例
console.log('name' in xiaoming);  // true
console.log('age' in xiaoming);   // true
console.log('sex' in xiaoming);   // true
console.log('nationality' in xiaoming);   // true

(2)在 prototype 上添加方法

  • 把方法直接添加到实例身上的缺点:每个实例和每个实例的方法函数 都是内存中不同的函数,造成了内存的浪费
  • 解决方法:将方法写到 prototype 上
  • 把方法写到原型上,并不会引起调用的紊乱,而且会使内存的开销更小
function People(name, age, sex) {
    this.name = name;
    this.age = age;
    this.sex = sex;
}
// 在构造函数的 prototype 属性上添加 方法
People.prototype.sayHello = function() {
    console.log('我是'+ this.name + ',今年' + this.age + '岁了,我是一个' + this.sex + '生');
}
People.prototype.growup = function() {
    this.age ++;
}
var xiaoming = new People('小明',12,'男');
var xiaohong = new People('小红',10,'女');

console.log(xiaoming.sayHello === xiaohong.sayHello); // true,表明两个实例调用的是同一个方法
xiaoming.sayHello(); // 我是小明,今年12岁了,我是一个男生
xiaohong.sayHello(); // 我是小红,今年10岁了,我是一个女生
xiaoming.growup();
xiaoming.sayHello(); // 我是小明,今年13岁了,我是一个男生
xiaohong.sayHello(); // 我是小红,今年10岁了,我是一个女生

(3)原型链的终点

  • 原型链的终点是 Object.prototype

(4)继承

  • 继承描述了两个类之间的“is a kind of”关系,比如学生“是一种”人,所以学生类和人类之间就构成继承关系
  • People是“父类”(或“超类”、“基类”);Student 是“子类”(或“派生类”)
  • 子类丰富了父类,让类的描述更具体、更细化

JavaScript中如何实现继承:

  • 实现继承的关键在于:子类必须拥有父类全部的属性和方法,同时子类还应该能定义自己特有的属性和方法
  • 使用JavaScript特有的原型链来实现继承,是普遍的做法
  • 在ES6中,还将介绍新的实现继承的方法
  • 子类可以重写父类的同名方法
// 父类
function People(name,age,sex) {
    this.name = name;
    this.age = age;
    this.sex = sex;
};
People.prototype.sayHello = function() {
    console.log('你好,我是'+ this.name + ',我今年'+ this.age + '岁了');
};
People.prototype.sleep = function() {
    console.log(this.name + '正在睡觉,zzzzz');
};

// 子类
function Student(name,age,sex,school,studentNumber) {
    this.name = name;
    this.age = age;
    this.sex = sex;
    this.school = school;
    this.studentNumber = studentNumber;
};

// 实现继承的关键语句:子类的prototype属性指向父类的实例
// 这句关键语句必须写在子类实例原型之前
Student.prototype = new People();

// 子类的方法
Student.prototype.study = function() {
    console.log(this.name + '正在学习');
};
Student.prototype.exam = function() {
    console.log(this.name + '在考试');
};

// 实例化
var xiaoming = new Student('小明',10,'男','xxx小学',12306);
xiaoming.study();
xiaoming.sayHello();
xiaoming.sleep();  // 小明正在睡觉,zzzzz
// 重写(override)父类的sleep方法
Student.prototype.sleep = function() {
    console.log(this.name + '正在睡觉,zzzzz,请不要打扰!');
};
xiaoming.sleep();  // 小明正在睡觉,zzzzz,请不要打扰!

5、JS 的内置对象

(1)包装类

  • Number()、String()、和 Boolean()分别是数字、字符串、布尔值的“包装类”
  • 包装类的目的就是为了让基本类型值可以从它们的构造函数的 prototype 上获得方法
  • Number()、String()和Boolean()的实例都是object类型,它们的PrimitiveValue属性存储它们的本身值
  • new 出来的基本类型值可以正常参与运算
var a = new Number(123);
var b = new String('Hello');
var c = new Boolean(true);

console.log(a);          // 123
console.log(typeof a);   // object
console.log(b);          // Hello
console.log(typeof b);   // object
console.log(c);          // true
console.log(typeof c);   // object

console.log(5 + a);      // 128
var d = 2;
console.log(d.__proto__ == Number.prototype);  // true

(2)Math(数学)对象

  • 幂和开方:Math.pow()、Math.sqrt()
  • 向上取整和向下取整:Math.ceil()、Math.floor()
  • 将一个数字四舍五入为整数:Math.round()
    如何将一个数对后几位小数四舍五入:先乘,然后对得到的数四舍五入,再然后除以先前乘的数
  • 得到参数列表的最大和最小值:Math.max()、Math.min()
console.log(Math.max(6,2,3,5));  // 6
console.log(Math.min(6,2,3,5));  // 2
  • 如何利用Math.max()求数组最大值
    Math.max() 要求参数必须是“罗列出来”,不能是数组
    apply方法可以指定上下文,并且以数组的形式传入“零散值”当做函数的参数
var arr = [3,6,9,2];
var max = Math.max.apply(null,arr);
console.log(max);  // 9
  • 得到0~1之间的小数:Math.random()
    为了得到 [a,b] 区间内的整数,可以使用这个公式:
parseInt(Math.random()*(b - a + 1)) + a

(3)Date(日期)对象

  • 使用 new Date() 可以得到当前的日期对象,它是object类型值
  • 使用 new Date(2021,11,1)可以得到指定日期的日期对象,注意:第二个参数表示月份,从0开始算,11表示12月;不算时区
  • 也可以是 new Date(‘2021-12-01’) 这样的写法,注意:这里的12真的是12月;算时区

日期对象的常见方法:

方法功能
getDate()得到日期1~31
getDay()得到星期0~6
getMonth()得到月份0~11
getFullYear()得到年份
getHours()得到小时数0~23
getMinutes()得到分钟数0~59
getSeconds()得到秒数0~59

时间戳:

  • 时间戳表示1970年1月1日零点整距离某时刻的毫秒数
  • 通过 getTime() 方法或者 Date.parse() 函数可以将时间对象变为时间戳
  • 通过 new Date(时间戳) 的写法,可以将时间戳变为日期对象
  • 1970年1月1日之间的时间戳表示为负的
// 日期对象
var date = new Date();

// 显示时间戳。时间戳表示1970年1月1日距离此时的毫秒数
var timestamp1 = date.getTime();  // 精确到毫秒
var timestamp2 = Date.parse(date);  //精确到毫秒,也是毫秒数,只不过最后三位一定是000

// 把时间戳变为时间对象,比如:1601536565000
var dd = new Date(1601536565000);

小案例:在页面上实时显示距离2021年高考还有多少天、多少时、多少分、多少秒

<h1>2021年高考倒计时</h1>
<h1 id="info"></h1>

// JS 代码
var info = document.getElementById('info');

setInterval(function() {
    // 当前时间
    var nowDate = new Date();
    // 目标时间,5表示6月
    var targetDate = new Date(2021,5,7);
    // 毫秒差
    var diff = nowDate - targetDate;
    
    // 将毫秒差换算为:天、时、分、秒
    // 换算为天:毫秒差除以一天的总毫秒数,就能换算为天
    var day = parseIn(diff / (1000 * 60 * 60 * 24));
    // 换算为时:毫秒差与一天的毫秒数的余数再除以一小时的毫秒数
    var hours = parseIn(diff % (1000 * 60 * 60 * 24) / (1000 * 60 * 60));
    // 换算为分:
    var minutes = parseIn(diff % (1000 * 60 * 60) / (1000 * 60));
    // 换算为秒:
    var seconds = parseIn(diff % (1000 * 60 * 60) / (1000 * 60) / 1000);

    // 显示内容
    info.innerText = day + '天' + hours + '时' + minutes + '分' + seconds + '秒';
},1000);

小结

  • 熟悉每条函数上下文this的判定规则
  • call 和 apply 的功能和区别
  • 用 new 调用函数的四个步骤
  • 什么是类和实例?面向对象编程的意义
  • prototype 和原型链查找
  • 继承的实现
  • 熟练掌握 Math、Date等JS内置对象

总结

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
JavaScript是一种脚本语言,常用于网页开发,可用于创建动态网页、交互式网页和应用程序。下面是一些JavaScript基础入门的内容: 1. 变量和数据类型 JavaScript使用变量来存储和操作数据。在JavaScript中,有许多不同的数据类型,包括字符串、数字、布尔值、数组、对象等。 ``` // 创建变量 var name = 'John'; var age = 25; var isStudent = true; var hobbies = ['reading', 'writing', 'coding']; var person = {name: 'John', age: 25, isStudent: true}; ``` 2. 运算符 JavaScript中常用的运算符有:算术运算符、比较运算符、逻辑运算符等。 ``` // 算术运算符 var a = 10; var b = 5; var c = a + b; // 15 var d = a - b; // 5 var e = a * b; // 50 var f = a / b; // 2 // 比较运算符 var x = 10; var y = 5; console.log(x > y); // true console.log(x < y); // false console.log(x == y); // false console.log(x != y); // true // 逻辑运算符 var p = true; var q = false; console.log(p && q); // false console.log(p || q); // true console.log(!p); // false ``` 3. 条件语句 条件语句用于根据特定条件执行不同的代码块。 ``` var age = 18; if (age >= 18) { console.log('成年人'); } else { console.log('未成年人'); } ``` 4. 循环语句 循环语句用于重复执行代码块,常用的循环语句有for循环和while循环。 ``` // for循环 for (var i = 0; i < 5; i++) { console.log(i); } // while循环 var i = 0; while (i < 5) { console.log(i); i++; } ``` 5. 函数 函数是一种可重复使用的代码块,用于执行特定任务。JavaScript中定义函数的语法如下: ``` function functionName(parameters) { // 函数体 } ``` 例如: ``` function add(a, b) { return a + b; } var sum = add(5, 10); console.log(sum); // 15 ``` 以上是JavaScript基础入门的一些内容,希望能对你有所帮助。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值