JavaScript学习笔记

JavaScript

JavaScript参考手册 (w3school.com.cn)

JavaScript的起源故事

一个合格的后端人员,必须要精通JavaScript !!!

1、简介

JavaScript = ECMAScript + JavaScript自己特有的东西(BOM+DOM)

​ JavaScript最初受Java启发而开始设计的,目的之一就是“看上去像Java”,因此语法上有类似之处,一些名称和命名规范也借自Java,但JavaScript的主要设计原则源自Self和Scheme

​ JavaScript是一种脚本语言,其源代码在发往客户端运行之前不需经过编译,而是将文本格式的字符代码发送给浏览器由浏览器解释运行。直译语言的弱点是安全性较差,而且在JavaScript中,如果一条运行不了,那么下面的语言也无法运行。而其解决办法就是于使用try{}catch(){},其中,catch()中会传入错误信息。

​ JavaScript是一种属于网络的高级脚本语言,已经被广泛用于Web应用开发,常用来为网页添加各式各样的动态功能,为用户提供更流畅美观的浏览效果。通常JavaScript脚本是通过嵌入在HTML中来实现自身的功能的。

  1. 是一种解释性脚本语言(代码不进行预编译)。

  2. 主要用来向HTML(标准通用标记语言下的一个应用)页面添加交互行为。

  3. 可以直接嵌入HTML页面,但写成单独的js文件有利于结构和行为的分离。

  4. 跨平台特性,在绝大多数浏览器的支持下,可以在多种平台下运行(如Windows、Linux、Mac、Android、iOS等)。

  5. JavaScript脚本语言同其他语言一样,有它自身的基本数据类型,表达式和算术运算符及程序的基本程序框架。JavaScript提供了四种基本的数据类型和两种特殊数据类型用来处理数据和文字。而变量提供存放信息的地方,表达式则可以完成较复杂的信息处理。

    ECMAScript它可以理解为是JavaScript的一个标准

    最新版本已经到es6版本,但是大部分浏览器还只停留在支持es5代码上

    开发环境与线上环境,版本不一致,需要转换工具。


2、引入方式

定义的位置会影响执行顺序

2.1、内部标签

可以写在head或者body

<script>
    <!--弹窗-->
    alert("Hello World!");
</script>

2.2、外部引入

<!--外部引入
        JS是用script标签引入,必须成对出现
        css是用link标签引入
        -->
    <script src="js/js.js"></script>

3、语法入门

ES6 中 JavaScript 引用到 严格检查模式

不用严格检查模式:

<script>
    i = 1;
</script>

当我们如上定义一个变量的时候, JavaScript会认为这是一个 全局变量, 这实际上是不合理的,想一下,一个网页中可能有很多个 js 文件,如果在 这个 js 文件中 i = 1,另一个 js 文件中i = 10,就会造成冲突。
所以,全局变量应该谨慎使用。

   <script>
        "use strict"
         var i = 1;
    </script>

3.1、变量声明

严格区分大小写

java中 : 定义变量 变量类型 变量名 = 变量值
JS中 : 全部都是var声明,局部变量类型推断

var javastack = "javastack";
就等于:
String javastack = "javastack";

var、let、const

在 ES2015 之前,JavaScript 是没有块作用域的。

通过 var 关键词声明的变量没有块作用域

  • { }声明的变量可以从块之外进行访问。
  • 但是函数内声明的变量在函数外不可访问。所以不同函数内可以使用相同变量名
{ 
  var x = 10; 
}
// 此处可以使用 x
function fx(){
    var xNum = 777;
}
console.log(xNum);   //此处访问失败

可以使用 let、const 关键词声明拥有块作用域的变量。建议局部变量都不要使用var定义

在块 {} 内声明的变量无法从块外访问。

{ 
  let x = 10;
}
// 此处不可以使用 x

3.2、条件控制

JavaScript 语句参考手册 (w3school.com.cn)

大部分语句基本同java,下面重点列出不同点


1、document.getElementById(" ").innerHTML (操作DOM)

在 HTML 中,JavaScript 语句是 Web 浏览器要“执行”的“指令”。

这条语句告诉浏览器在 id="demo" 的 HTML 元素中写 "Hello Kitty"

<!DOCTYPE html>
<html>
<body>

<h1>我的网页</h1>

<p id="demo">我的第一个段落。</p>

<script>
document.getElementById("demo").innerHTML = "Hello Kitty.";
</script>

</body>
</html>

2、debugger

debugger 语句停止 JavaScript 的执行,并调用(如果可用)调试函数。

使用 debugger 语句与在代码中设置断点具有相同的功能。


3、循环

① for - 多次循环代码块
② for/in - 遍历对象的属性

for/in 遍历的是key, 如果没有key,则遍历的是index。

注释:不要使用 for/in 语句循环遍历 索引顺序 很重要的数组。请改用 for 语句。

循环遍历对象的属性:

<!DOCTYPE html>
<html>
<body>

<p>单击该按钮可循环遍历对象的属性。</p>

<button onclick="myFunction()">试一试</button>

<p id="demo"></p>

<script>
function myFunction() {
  var person = {fname:"John", lname:"Doe", age:25}; 
  
  var text = "";
  var x; 	//在for/in中会自动推断为person的属性
  for (x in person) {
    text += person[x] + " ";
  }
  document.getElementById("demo").innerHTML = text;
}
</script>

</body>
</html>

单击该按钮可循环遍历对象的属性。
试一试
John Doe 25   (点击后显示)

③ for/of - 循环遍历可迭代对象的值(ES6新增)

for/of 遍历的是元素, 在Map中,一个键值对 视为 Map 的一个元素

循环遍历数组的值:

<!DOCTYPE html>
<html>
<body>

<script>
var cars = ['BMW', 'Volvo', 'Mini'];
var x;

for (x of cars) {
  document.write(x + "<br >");
}
</script>

</body>
</html>

**输出:**
BMW
Volvo
Mini

④ while - 在指定条件为真时循环代码块

⑤ do/while - 循环一次代码块,然后在指定条件为真时重复循环

4、switch

  • 在java中,switch语句可以接受的数据类型: byte,int,shot,char,枚举(JDK1.5) ,String(JDK1.7)

  • 在JS中,switch语句可以接受任意的原始数据类型:number、string、boolean、null、undefined

switch(变量){
	case 值:
        //执行语句
		break;
	case 值:
        //执行语句
		break;
	...
	
	case 值:
        //执行语句
		break;
}

3.3、运算符

1、一元算术符

只有一个 运算数 的运算符

++ -- : 自增(自减)

  • ++(--) 在前,先自增(自减),再运算
  • ++(--) 在后,先运算,再自增(自减)

+(-) :正负号

注意:在JS中,如果运算数不是运算符所要求的类型,那么js引擎会自动的将运算数进行类型转换,其他类型转number。

  • string转number:按照字面值转换。如果字面值不是数字,则转为NaN(不是数字的数字)
  • boolean转number:true转为1,false转为0

2、算术运算符

+ - * / % ...

3、赋值运算符

= += -+ ....

4、逻辑运算符

运算符 描述 例子
&& and (x < 10 && y > 1) is true
|| or (x === 5 || y === 5) is false
! not !(x === y) is true

5、比较运算符

在逻辑语句中使用比较运算符来确定变量或值之间的相等性或差异。

比较方式

  • 类型相同:直接比较

    字符串:按照字典顺序比较。按位逐一比较,直到得出大小为止。

  • 类型不同:先进行类型转换,再比较

    ===:全等于。在比较之前,先判断类型,如果类型不一样,则直接返回false

不要使用 == 比较,这和java不一样,无法比较类型。要用===

运算符 描述 比较 返回
== 等于 x == 8 false
=== 相等值和相等类型 x === "5" false
!= 不相等 x != 8 true
!== 不相等的值不相等的类型 x !== "5" true
> 大于 x > 8 false
< 小于 x < 8 true
>= 大于或等于 x >= 8 false
<= 小于或等于 x <= 8 true

特例:NaN===NaN ---->false

NaN与所有的数值都不相等,包括自己。

浮点数问题

console.log((1/3) ===(1-2/3))    //false,会有精度损失 

6、三元运算符

语法:

  • 表达式 ? 值1 : 值2;
  • 判断表达式的值,如果是true则取值1,如果是false则取值2;

7、typeof 运算符

获取变量的类型。

注意: null运算后得到的是object


3.4、数据类型

1、原始数据类型(基本数据类型)

  • number
  • string
  • boolean
  • null
  • undefined

1、number

不区分小数整数

  • 整数
  • 小数
  • NaN NaN == NaN -----> false, 当然全等===也是false

JavaScript Number 参考手册 (w3school.com.cn)

当 Number() 和运算符 new 一起作为构造函数使用时,它返回一个新创建的 Number 对象。如果不用 new 运算符,把 Number() 作为一个函数来调用,它将把自己的参数转换成一个原始的数值,并且返回这个值(如果转换失败,则返回 NaN)。


Number() 构造函数的属性

注意:这些值是构造函数 Number() 自身的属性,而不是单独的某个 Number 对象的属性。

比如这样使用属性 MAX_VALUE 是正确的:

var big = Number.MAX_VALUE

但是这样是错误的:

var n= new Number(2);
var big = n.MAX_VALUE
属性 描述
constructor 返回对创建此对象的 Number 函数的引用。
MAX_VALUE 可表示的最大的数。
MIN_VALUE 可表示的最小的数。
NaN Not a Number 非数字值。
NEGATIVE_INFINITY 负无穷大,溢出时返回该值。
POSITIVE_INFINITY 正无穷大,溢出时返回该值。
prototype 使您有能力向对象添加属性和方法。

注意:typeof NaN ----> "number"


Number 对象方法

是每个 Number 对象的方法,而不是 Number() 构造函数的方法。

在必要时,JavaScript 会自动地把原始数值转化成 Number 对象

调用 Number 方法的既可以是 Number 对象,也可以是原始数字值。

var n = 123;
var binary_value = n.toString(2);
方法 描述
toString 把数字转换为字符串,使用指定的基数。
toLocaleString 把数字转换为字符串,使用本地数字格式顺序。
toFixed 把数字转换为字符串,结果的小数点后有指定位数的数字。
toExponential 把对象的值转换为指数计数法。
toPrecision 把数字格式化为指定的长度。
valueOf 返回一个 Number 对象的基本数字值。

2、string

JavaScript String 参考手册 (w3school.com.cn)

1、正常字符串

正常字符串是文本,由双引号或单引号包围:

"Bill Gates"

'Bill Gates' 
new String(s);
String(s);
/*
当 String() 和运算符 new 一起作为构造函数使用时,它返回一个新创建的 String 对象,存放的是字符串 s 或 s 的字符串表示。

当不用 new 运算符调用 String() 时,它只把 s 转换成原始的字符串,并返回转换后的值。
*/
2、转义字符
let a = '\n';
let b = '\u4e2d';  //unicode字符
let c = '\x41';   //ascii码
3、多行字符串编写
//多行字符串编写,
let msg = `hello
world
你好呀
好呀
呀`;
4、模板字符串

Internet Explorer 不支持模板字面量。

JavaScript 字符串模板 (w3school.com.cn)

模板字面量允许字符串中的变量

let firstName = "John";
let lastName = "Doe";

let text = `Welcome ${firstName}, ${lastName}!`;

输出	text:Welcome John, Doe!

模板字面量允许字符串中的表达式

let price = 10;
let VAT = 0.25;

let total = `Total: ${(price * (1 + VAT)).toFixed(2)}`;  //保留两位小数

输出	Total: 12.50
5、字符串的可变性,不可变
6、字符串对象属性
属性 描述
constructor 对创建该对象的函数的引用
length 字符串的长度
prototype 允许您向对象添加属性和方法
7、字符串对象方法,带()
(前面几个常用)方法 描述
toString() 返回字符串。
indexOf() 检索字符串。
substr() 从起始索引号提取字符串中指定数目的字符。
substring() 提取字符串中两个指定的索引号之间的字符。左闭右开
valueOf() 返回某个字符串对象的原始值。
match() 找到一个或多个正则表达式的匹配。
toLowerCase() 把字符串转换为小写。
toUpperCase() 把字符串转换为大写。
anchor() 创建 HTML 锚。
big() 用大号字体显示字符串。
blink() 显示闪动字符串。
bold() 使用粗体显示字符串。
charAt() 返回在指定位置的字符。
charCodeAt() 返回在指定的位置的字符的 Unicode 编码。
concat() 连接字符串。
fixed() 以打字机文本显示字符串。
fontcolor() 使用指定的颜色来显示字符串。
fontsize() 使用指定的尺寸来显示字符串。
fromCharCode() 从字符编码创建一个字符串。
italics() 使用斜体显示字符串。
lastIndexOf() 从后向前搜索字符串。
link() 将字符串显示为链接。
localeCompare() 用本地特定的顺序来比较两个字符串。
replace() 替换与正则表达式匹配的子串。
search() 检索与正则表达式相匹配的值。
slice() 提取字符串的片断,并在新的字符串中返回被提取的部分。
small() 使用小字号来显示字符串。
split() 把字符串分割为字符串数组。
strike() 使用删除线来显示字符串。
sub() 把字符串显示为下标。
sup() 把字符串显示为上标。
toLocaleLowerCase() 把字符串转换为小写。
toLocaleUpperCase() 把字符串转换为大写。
toSource() 代表对象的源代码。

3、boolean

Boolean 可以有以下两个值之一:true 或 false。

其他类型转boolean:

  • number:0或NaN为假,其他为真
  • string:除了空字符串(""),其他都是true
  • null & undefined: 都是false
  • 对象:所有对象都为true

4、null

一个对象为空的占位符

typeof(null) = object


5、undefined

未定义。如果一个变量没有给初始值,则会被默认赋值为undefined


2、引用数据类型

1、Array

JavaScript 数组参考手册 (w3school.com.cn)

Java数组中必须是相同类型的对象

JavaScript 变量可以是对象。数组是特殊类型的对象。

正因如此,您可以在相同数组中存放不同类型的变量

您可以在数组保存对象。您可以在数组中保存函数。你甚至可以在数组中保存数组

数组长度可变

给arr.length赋值,arr的大小会发生变化,赋值大于原长度,多余的元素为undefine;

赋值小于原长度,会丢失后面的元素。


避免 new Array()

没有必要使用 JavaScript 的内建数组构造器 new Array()。

请使用 [ ] 取而代之!

var points = new Array(40, 100, 1, 5, 25, 10); // 差
var points = [40, 100, 1, 5, 25, 10];          // 优

new 关键词只会使代码复杂化。它还会产生某些不可预期的结果:

var points = new Array(40, 100);  // 创建包含两个元素的数组(40 和 100)

假如删除其中一个元素会怎么样?

var points = new Array(40);       // 创建包含 40 个未定义元素的数组!!!

常见的问题是:我如何知晓某个变量是否是数组

问题在于 JavaScript 运算符 typeof 返回 "object",因为 JavaScript 数组属于对象。

var fruits = ["Banana", "Orange", "Apple", "Mango"];

typeof fruits;             // 返回 object

为了解决这个问题,ECMAScript 5 定义了新方法 Array.isArray()

Array.isArray(fruits);     // 返回 true

假如对象由给定的构造器创建,则 instanceof 运算符返回 true:

var fruits = ["Banana", "Orange", "Apple", "Mango"];
 
fruits instanceof Array     // 返回 true

Array 方法

方法 描述
slice() 类似字符串的substring方法,左闭右开 选择数组的一部分,并返回新数组。
indexOf() 在数组中搜索元素并返回其位置。
toString() 将数组转换为字符串,并返回结果。
valueOf() 返回数组的原始值。
isArray() 检查对象是否为数组。
join() 将数组的所有元素连接成一个字符串
pop() 删除数组的最后一个元素,并返回该元素
shift() 删除数组的第一个元素,并返回该元素
push() 将新元素添加到数组的末尾,并返回新的长度
unshift() 将新元素添加到数组的开头,并返回新的长度
concat() 连接两个或多个数组,并返回已连接数组的副本原数组未改变
reverse() 反转数组中元素的顺序
sort() 对数组的元素进行排序
forEach() 迭代 为每个数组元素调用函数。
copyWithin() 将数组中的数组元素复制到指定位置或从指定位置复制。
entries() 返回键/值对数组迭代对象。
every() 检查数组中的每个元素是否通过测试。
fill() 用静态值填充数组中的元素。
filter() 使用数组中通过测试的每个元素创建新数组。
find() 返回数组中第一个通过测试的元素的值。
findIndex() 返回数组中通过测试的第一个元素的索引。
from() 从对象创建数组。
includes() 检查数组是否包含指定的元素。
keys() 返回 Array Iteration 对象,包含原始数组的键.
lastIndexOf() 在数组中搜索元素,从末尾开始,并返回其位置。
map() 使用为每个数组元素调用函数的结果创建新数组。
reduce() 将数组的值减为单个值(从左到右)。
reduceRight() 将数组的值减为单个值(从右到左)。
some() 检查数组中的任何元素是否通过测试。
splice() 从数组中添加/删除元素。

Array.forEach() 回调函数

JavaScript 数组迭代 (w3school.com.cn)

var txt = "";
var numbers = [45, 4, 9, 16, 25];
numbers.forEach(myFunction);

function myFunction(value, index, array) {
  txt = txt + value + "<br>"; 
}

注释:该函数接受 3 个参数:

  • 项目值
  • 项目索引
  • 数组本身

上面的例子只用了 value 参数。这个例子可以重新写为:

var txt = "";
var numbers = [45, 4, 9, 16, 25];
numbers.forEach(myFunction);

function myFunction(value) {
  txt = txt + value + "<br>"; 
}
2、Class

JavaScript 类不是对象。

它只是 JavaScript 对象的模板。

类是函数的一种,但我们不使用关键字 function 来对其初始化,而是使用关键字 class,并在 constructor() 方法中分配属性:

创建一个 Car 类,然后基于这个 Car 类创建名为 "mycar" 的对象:

class Car {  // 创建类
  constructor(brand) {  // 类构造方法
    this.carname = brand;  // 类主体、属性
  }
}
mycar = new Car("Ford");  // 创建 Car 类的对象

Class 关键字

关键字 描述
extends 扩展类(继承)。
static 为类定义静态方法。
super 引用父类。
3、对象
1、对象属性

对象也是变量。但是对象包含很多值。

这段代码把多个值(porsche, 911, white)赋给名为 car 的变量:

var car = {type:"porsche", model:"911", color:"white"};

值以键值对的方式来书写(名称和值由冒号分隔)。

Javascript中所有的键都是字符串,值是任意对象

JavaScript 对象是被命名值的容器。

键值对被称为属性

var person = {firstName:"Bill", lastName:"Gates", age:62, eyeColor:"blue"};
属性 属性值
firstName Bill
lastName Gates
age 62
eyeColor blue

注意:JavaScript规定,访问不存在的属性不报错,而是返回undefined

var xiaoming = {
    name: '小明'
};
console.log(xiaoming.name);
console.log(xiaoming.age); // undefined

由于JavaScript的对象是动态类型,你可以自由地给一个对象添加或删除属性:

var xiaoming = {
    name: '小明'
};
xiaoming.age; // undefined
xiaoming.age = 18; // 新增一个age属性
xiaoming.age; // 18
delete xiaoming.age; // 删除age属性
xiaoming.age; // undefined
delete xiaoming['name']; // 删除name属性
xiaoming.name; // undefined
delete xiaoming.school; // 删除一个不存在的school属性也不会报错

如果我们要检测xiaoming是否拥有某一属性,可以用in操作符:

var xiaoming = {
    name: '小明',
    birth: 1990,
    school: 'No.1 Middle School',
    height: 1.70,
    weight: 65,
    score: null
};
'name' in xiaoming; // true
'grade' in xiaoming; // false

不过要小心,如果in判断一个属性存在,这个属性不一定是xiaoming的,它可能是xiaoming继承得到的:

'toString' in xiaoming; // true

2、对象方法

对象也可以有方法

方法是在对象上执行的动作。

方法以 函数定义 被存储在属性中。

var person = {
  firstName: "Bill",
  lastName : "Gates",
  id       : 678,
  fullName : function() {
    return this.firstName + " " + this.lastName;
  }
};

3、this (此处结合函数方法部分理解)

JavaScript this 关键词指的是它所属的对象

它拥有不同的值,具体取决于它的使用位置:

  • 方法中,this 指的是所有者对象。
  • 单独的情况下,this 指的是全局对象。
  • 函数中,this 指的是全局对象。
  • 在函数中,严格模式下,this 是 undefined。JavaScript 严格模式不允许默认绑定。
  • 在事件中,this 指的是接收事件的元素。

call() 和 apply() 这样的方法可以将 this 引用到任何对象。具体跳到方法部分详解

显式函数绑定

call() 和 apply() 方法是预定义的 JavaScript 方法。

它们都可以用于将另一个对象作为参数调用对象方法。

在下面的例子中,当使用 person2 作为参数调用 person1.fullName 时this 将引用 person2,即使它是 person1 的方法:

var person1 = {
  fullName: function() {
    return this.firstName + " " + this.lastName;
  }
}
var person2 = {
  firstName:"Bill",
  lastName: "Gates",
}
person1.fullName.call(person2);  // 会返回 "Bill Gates"

单独的 this

在单独使用时,拥有者是全局对象,因此 this 指的是全局对象。

在浏览器窗口中,全局对象是 [object Window]


4、Map(有序的键值对,ES6新增)

JavaScript Map 对象 (w3school.com.cn)

Map 对象存有键值对,其中的键可以是任何数据类型。

Map 对象记得键的原始插入顺序。

Map 对象具有表示映射大小的属性。

基本的 Map() 方法

Method Description
new Map() 创建新的 Map 对象。
set(key,value) 为 Map 对象中的键设置值。如果key不存在,则添加新的键值对
get(key) 获取 Map 对象中键的值
entries() 返回 Map 对象中键/值对的数组。
keys() 返回 Map 对象中键的数组。
values() 返回 Map 对象中值的数组。
clear() 删除 Map 中的所有元素。
delete() 删除由键指定的元素。
has() 如果键存在,则返回 true。
forEach() 为每个键/值对调用回调。

Map() 属性

Property Description
size 获取 Map 对象中某键的值。

能够使用对象作为键是 Map 的一个重要特性

// 创建对象
const apples = {name: 'Apples'};
const bananas = {name: 'Bananas'};
const oranges = {name: 'Oranges'};

// 方法1:创建新的 Map
const fruits = new Map([
  [apples, 500],
  [bananas, 300],
  [oranges, 200]
]);

// 方法2:创建新的 Map
const fruits = new Map();

// Add new Elements to the Map
fruits.set(apples, 500);
fruits.set(bananas, 300);
fruits.set(oranges, 200);

get() 方法获取 Map 中键的值:

fruits.get(apples);    // 返回 500,对象作为键
fruits.get("apples");  // 返回 undefined

由于一个key只能对应一个value,所以,多次对一个key放入value,后面的值会把前面的值冲掉:

var m = new Map();
m.set('Adam', 67);
m.set('Adam', 88);
m.get('Adam'); // 88

5、Set(无序不重复的集合,ES6新增)

JavaScript Set 对象 (w3school.com.cn)

SetMap类似,也是一组key的集合,但不存储value。由于key不能重复,所以,在Set中,没有重复的key。

Set 是唯一值的集合。

每个值在 Set 中只能出现一次。如果添加相等的元素,则只会保存第一个元素。

一个 Set 可以容纳任何数据类型的任何值。

Set 对象的方法和属性

new Set() 创建新的 Set 对象。
add() 向 Set 添加新元素。
clear() 从 Set 中删除所有元素。
delete() 删除由其值指定的元素。
entries() 返回 Set 对象中值的数组。
has() 如果值存在则返回 true。
forEach() 为每个元素调用回调。
keys() 返回 Set 对象中值的数组。
values() 与 keys() 相同。
size 返回元素计数。
// 方法1:创建新的 Set
const letters = new Set(["a","b","c"]);
// 方法2:创建 Set
const letters = new Set();

// 向 Set 添加一些值
letters.add("a");
letters.add("b");
letters.add("c");
// 方法3:创建新的变量
const a = "a";
const b = "b";
const c = "c";

// 创建 Set
const letters = new Set();

// Add the values to the Set
letters.add(a);
letters.add(b);
letters.add(c);

重复元素Set中自动被过滤:

var s = new Set([1, 2, 3, 3, '3']);
s; // Set {1, 2, 3, "3"}

注意数字3和字符串'3'是不同的元素。

通过add(key)方法可以添加元素到Set中,可以重复添加,但不会有效果:

s.add(4);
s; // Set {1, 2, 3, 4}
s.add(4);
s; // 仍然是 Set {1, 2, 3, 4}

通过delete(key)方法可以删除元素:

var s = new Set([1, 2, 3]);
s; // Set {1, 2, 3}
s.delete(3);
s; // Set {1, 2}

对于 Set,typeof 返回对象:

typeof letters;      // 返回 object。

6、(Map/Set)iterator迭代器 (ES6新增)

遍历Array可以采用下标循环,遍历MapSet无法使用下标。

为了统一集合类型,ES6标准引入了新的iterable类型,ArrayMapSet都属于iterable类型。

具有iterable类型的集合可以通过新的for ... of循环来遍历

遍历Map/Set中的值(for/of):Map中 一个键值对 视为 Map的一个元素

for ... of循环遍历集合,用法如下

var a = ['A', 'B', 'C'];
var s = new Set(['A', 'B', 'C']);
var m = new Map([[1, 'x'], [2, 'y'], [3, 'z']]);
for (var x of a) { // 遍历Array
    console.log(x);
}
for (var x of s) { // 遍历Set
    console.log(x);
}
for (var x of m) { // 遍历Map
    console.log(x[0] + '=' + x[1]);
}

for ... of循环和for ... in循环有何区别

for ... in循环由于历史遗留问题,它遍历的实际上是对象的属性名称。一个Array数组实际上也是一个对象,它的每个元素的索引被视为一个属性

当我们手动给Array对象添加了额外的属性后,for ... in循环将带来意想不到的意外效果:

var a = ['A', 'B', 'C'];
a.name = 'Hello';
for (var x in a) {
    console.log(x); // '0', '1', '2', 'name'
}

for ... in循环将把name包括在内,Arraylength属性却不包括在内

for ... of循环则完全修复了这些问题,它只循环集合本身的元素

var a = ['A', 'B', 'C'];
a.name = 'Hello';
for (var x of a) {
    console.log(x); // 'A', 'B', 'C'
}

这就是为什么要引入新的for ... of循环。

然而,更好的方式是直接使用iterable内置的forEach方法(ES5.1标准引入),它接收一个函数,每次迭代就自动回调该函数。以Array为例:

var a = ['A', 'B', 'C'];
a.forEach(function (element, index, array) {
    // element: 指向当前元素的值
    // index: 指向当前索引
    // array: 指向Array对象本身
    console.log(element + ', index = ' + index);
});

//	输出:
//	A, index = 0
//	B, index = 1
//	C, index = 2

SetArray类似,但Set没有索引,因此回调函数的前两个参数都是元素本身:

var s = new Set(['A', 'B', 'C']);
s.forEach(function (element, sameElement, set) {
    console.log(element);
});

//    输出:
//    A
//    B
//    C

Map的回调函数参数依次为valuekeymap本身:

var m = new Map([[1, 'x'], [2, 'y'], [3, 'z']]);
m.forEach(function (value, key, map) {
    console.log(value);
});
//	输出:
//	x
//	y
//	z

如果对某些参数不感兴趣,由于JavaScript的函数调用不要求参数必须一致,因此可以忽略它们。例如,只需要获得Arrayelement

var a = ['A', 'B', 'C'];    
a.forEach(function (element) {		//默认使用第一个参数element
    console.log(element);
});

//	输出:
//  A
//	B
//	C
var m = new Map([[1, 'x'], [2, 'y'], [3, 'z']]);   
m.forEach(function (value) {		//默认使用第一个参数value
    console.log(value);
});
//	输出:
//	x
//	y
//	z

4、函数

3、函数及面向对象

类似java方法

JavaScript 函数定义 (w3school.com.cn)

所有 JavaScript 值,除了原始值,都是对象。对象是包含变量的变量

原始值指的是没有属性或方法的值

原始数据类型指的是拥有原始值的数据

JavaScript 定义了 5 种原始数据类型

  • string
  • number
  • boolean
  • null
  • undefined

1、定义函数

在JavaScript中,定义函数的方式如下:

function abs(x) {
    if (x >= 0) {
        return x;
    } else {
        return -x;
    }
}

//也可以把方法体用双引号包裹,写进参数列表的最后一个,但是不建议
function abs(x,y,z,"方法体");

上述abs()函数的定义如下:

  • function指出这是一个函数定义;
  • abs是函数的名称;
  • (x)括号内列出函数的参数,多个参数以,分隔;
  • { ... }之间的代码是函数体,可以包含若干语句,甚至可以没有任何语句。

请注意,函数体内部的语句在执行时,一旦执行到return时,函数就执行完毕,并将结果返回。因此,函数内部通过条件判断和循环可以实现非常复杂的逻辑。

如果没有return语句,函数执行完毕后也会返回结果,只是结果为undefined

由于JavaScript的函数也是一个对象,上述定义的abs()函数实际上是一个函数对象,而函数名abs可以视为指向该函数的变量。

因此,第二种定义函数的方式如下:

var abs = function (x) {
    if (x >= 0) {
        return x;
    } else {
        return -x;
    }
};

在这种方式下,function (x) { ... }是一个匿名函数,它没有函数名。但是,这个匿名函数赋值给了变量abs,所以,通过变量abs就可以调用该函数。

上述两种定义完全等价,注意第二种方式按照完整语法需要在函数体末尾加一个;,表示赋值语句结束。

特点

  • 方法定义是,形参的类型不用写,返回值类型也不写。

  • 方法是一个对象,如果定义名称相同的方法,会覆盖,所以没有函数重载,

    要实现重载效果可以在方法体中对arguments进行判断。

  • 在JS中,方法的调用只与方法的名称有关,和参数列表无关

  • 在方法声明中有一个隐藏的内置对象(数组),arguments, 封装所有的实际参数


2、调用函数

调用函数时,按顺序传入参数即可:

abs(10); // 返回10
abs(-9); // 返回9

由于JavaScript允许传入任意个参数而不影响调用,因此传入的参数比定义的参数多也没有问题,虽然函数内部并不需要这些参数

abs(10, 'blablabla'); // 返回10
abs(-9, 'haha', 'hehe', null); // 返回9

传入的参数比定义的少也没有问题:

abs(); // 返回NaN

此时abs(x)函数的参数x将收到undefined,计算结果为NaN

要避免收到undefined,可以对参数进行检查

function abs(x) {
    if (typeof x !== 'number') {
        throw 'Not a number';
    }
    if (x >= 0) {
        return x;
    } else {
        return -x;
    }
}

3、arguments (翻译:参数)

用于接收所有的参数

JavaScript还有一个免费赠送的关键字arguments,它只在函数内部起作用,并且永远指向当前函数的调用者传入的所有参数arguments类似Array但它不是一个Array

如果函数调用的参数太多(超过声明),则可以使用 arguments 对象来达到这些参数。

x = findMax(1, 123, 500, 115, 44, 88);

function findMax() {
    var i;
    var max = -Infinity;
    for (i = 0; i < arguments.length; i++) {
        if (arguments[i] > max) {
            max = arguments[i];
        }
    }
    return max;
}

利用arguments,你可以获得调用者传入的所有参数。也就是说,即使函数不定义任何参数,还是可以拿到参数的值:

function abs() {
    if (arguments.length === 0) {
        return 0;
    }
    var x = arguments[0];
    return x >= 0 ? x : -x;
}

abs(); // 0
abs(10); // 10
abs(-9); // 9

实际上arguments最常用于判断传入参数的个数。你可能会看到这样的写法:

// foo(a[, b], c)
// 接收2~3个参数,b是可选参数,如果只传2个参数,b默认为null:
function foo(a, b, c) {
    if (arguments.length === 2) {
        // 实际拿到的参数是a和b,c为undefined
        c = b; // 把b赋给c
        b = null; // b变为默认值
    }
    // ...
}

要把中间的参数b变为“可选”参数,就只能通过arguments判断,然后重新调整参数并赋值。


4、rest (翻译:剩余部分)

用于接收多余的参数

由于JavaScript函数允许接收任意个参数,于是我们就不得不用arguments来获取所有参数:

function foo(a, b) {
    var i, rest = [];
    if (arguments.length > 2) {
        for (i = 2; i<arguments.length; i++) {
            rest.push(arguments[i]);
        }
    }
    console.log('a = ' + a);
    console.log('b = ' + b);
    console.log(rest);
}

为了获取除了已定义参数ab之外的参数,我们不得不用arguments,并且循环要从索引2开始以便排除前两个参数,这种写法很别扭,只是为了获得额外的rest参数,有没有更好的方法?

ES6标准(有些浏览器不支持)引入了rest参数,上面的函数可以改写为:

function foo(a, b, ...rest) {
    console.log('a = ' + a);
    console.log('b = ' + b);
    console.log(rest);
}

foo(1, 2, 3, 4, 5);
// 结果:
// a = 1
// b = 2
// Array [ 3, 4, 5 ]

foo(1);
// 结果:
// a = 1
// b = undefined
// Array []

rest参数只能写在最后,前面用...标识java中叫可变长参数),从运行结果可知,传入的参数先绑定ab,多余的参数以数组形式交给变量rest,所以,不再需要arguments我们就获取了全部参数。

如果传入的参数连正常定义的参数都没填满,也不要紧,rest参数会接收一个空数组(注意不是undefined)。


5、return

小心你的return语句

JavaScript引擎有一个在行末自动添加分号的机制,这可能让你栽到return语句的一个大坑:

function foo() {
    return { name: 'foo' };
}

foo(); // { name: 'foo' }

如果把return语句拆成两行:

function foo() {
    return
        { name: 'foo' };
}

foo(); // undefined

由于JavaScript引擎在行末自动添加分号的机制,上面的代码实际上变成了:

function foo() {
    return; // 自动添加了分号,相当于return undefined;
        { name: 'foo' }; // 这行语句已经没法执行到了
}

所以正确的多行写法是:

function foo() {
    return { // 这里不会自动加分号,因为{表示语句尚未结束
        name: 'foo'
    };
}

5、传递方式
参数 通过 值传递

函数调用中的参数(parameter)是函数的参数(argument)。

JavaScript 参数通过传递:函数只知道值,而不是参数的位置。

如果函数改变了参数的值,它不会改变参数的原始值。

参数的改变在函数之外是不可见的。

对象 是由 引用传递 的

在 JavaScript 中,对象引用是值。

正因如此,对象的行为就像它们通过引用来传递:

如果函数改变了对象属性,它也改变了原始值。

对象属性的改变在函数之外是可见的。


6、变量的作用域

在JavaScript中,用var申明的变量实际上是有作用域的。

如果一个变量在函数体内部申明,则该变量的作用域为整个函数体,在函数体外不可引用该变量:

'use strict';

function foo() {
    var x = 1;
    x = x + 1;
}

x = x + 2; // ReferenceError! 无法在函数体外引用变量x
1、不同函数的同名变量

如果两个不同的函数各自申明了同一个变量,那么该变量只在各自的函数体内起作用。换句话说,不同函数内部的同名变量互相独立,互不影响

'use strict';

function foo() {
    var x = 1;
    x = x + 1;
}

function bar() {
    var x = 'A';
    x = x + 'B';
}

2、嵌套函数的同名变量

由于JavaScript的函数可以嵌套,此时,内部函数可以访问外部函数定义的变量,反过来则不行

'use strict';

function foo() {
    var x = 1;
    function bar() {
        var y = x + 1; // bar可以访问foo的变量x!
    }
    var z = y + 1; // ReferenceError! foo不可以访问bar的变量y!
}

JavaScript的函数在查找变量时从自身函数定义开始从“内”向“外”查找。如果内部函数定义了与外部函数重名的变量则内部函数的变量将“屏蔽”外部函数的变量

'use strict';

function foo() {
    var x = 1;
    function bar() {
        var x = 'A';
        console.log('x in bar() = ' + x); // 'A'
    }
    console.log('x in foo() = ' + x); // 1
    bar();
}

foo();  //x in foo() = 1      x in bar() = A

这说明JavaScript的函数在查找变量时从自身函数定义开始,从“内”向“外”查找。如果内部函数定义了与外部函数重名的变量,则内部函数的变量将“屏蔽”外部函数的变量。


3、变量提升

JavaScript的函数定义有个特点,它会先扫描整个函数体的语句,把所有申明的变量“提升”到函数顶部

'use strict';

function foo() {
    var x = 'Hello, ' + y;
    console.log(x);
    var y = 'Bob';
}

foo();

虽然是strict模式,但语句var x = 'Hello, ' + y; 并不报错,原因是变量y在稍后申明了。但是console.log显示Hello, undefined,说明变量y的值为undefined。这正是因为JavaScript引擎自动提升了变量y的声明,但不会提升变量y的赋值

对于上述foo()函数,JavaScript引擎看到的代码相当于:

function foo() {
    var y; // 提升变量y的申明,此时y为undefined
    var x = 'Hello, ' + y;
    console.log(x);
    y = 'Bob';
}

由于JavaScript的这一怪异的“特性”,我们在函数内部定义变量时,请严格遵守“在函数内部首先申明所有变量”这一规则。最常见的做法是用一个var申明函数内部用到的所有变量:

function foo() {
    var
        x = 1, // x初始化为1
        y = x + 1, // y初始化为2
        z, i; // z和i为undefined
    // 其他语句:
    for (i=0; i<100; i++) {
        ...
    }
}
4、全局作用域

不在任何函数内定义的变量就具有全局作用域。实际上,JavaScript默认有一个全局对象window,全局作用域的变量实际上被绑定到window的一个属性:

'use strict';

var course = 'Learn JavaScript';
alert(course); // 'Learn JavaScript'
alert(window.course); // 'Learn JavaScript'

因此,直接访问全局变量course和访问window.course是完全一样的。

你可能猜到了,由于函数定义有两种方式,以变量方式var foo = function () {}定义的函数实际上也是一个全局变量,因此,顶层函数的定义也被视为一个全局变量,并绑定到window对象:

'use strict';

function foo() {
    alert('foo');
}

foo(); // 直接调用foo()
window.foo(); // 通过window.foo()调用

我们每次直接调用的alert()函数其实也是window的一个变量:

'use strict';

window.alert('调用window.alert()');
// 把alert保存到另一个变量:
var old_alert = window.alert;
// 给alert赋一个新函数:
window.alert = function () {} 

alert('无法用alert()显示了!');  //此时alert()是个空函数,能干掉系统函数,JS太随意了!

// 恢复alert:
window.alert = old_alert;   //alert又重新指向回去
alert('又可以用alert()了!');     

这说明JavaScript实际上只有一个全局作用域。任何变量(函数也视为变量),如果没有在当前函数作用域中找到,就会继续往上查找,最后如果在全局作用域中也没有找到,则报ReferenceError错误(引用异常)。


5、名字空间(解决全局变量冲突)

全局变量会绑定到window上,不同的JavaScript文件如果使用了相同的全局变量,或者定义了相同名字的顶层函数,都会造成命名冲突,并且很难被发现。

减少冲突的一个方法是把自己的所有变量和函数全部绑定到一个全局变量中。例如:

// 唯一的全局变量MYAPP:
var MYAPP = {};

// 其他变量:
MYAPP.name = 'myapp';
MYAPP.version = 1.0;

// 其他函数:
MYAPP.foo = function () {
    return 'foo';
};

把自己的代码全部放入唯一的名字空间MYAPP中,会大大减少全局变量冲突的可能

许多著名的JavaScript库都是这么干的:jQuery,YUI,underscore等等。


6、局部作用域

由于JavaScript的变量作用域实际上是函数内部,我们在for循环等 语句块中 是无法定义具有局部作用域的变量的:

'use strict';

function fx() {
    for (var i=0; i<100; i++) { };
    console.log(i += 100); // 仍然可以引用变量i  ,i=200
}

为了解决块级作用域,ES6引入了新的关键字let,用let替代var可以申明一个块级作用域的变量:

'use strict';

function fx() {
    var sum = 0;
    for (let i=0; i<100; i++) {
        sum += i;
    }
    // SyntaxError:
    i += 1;     //此处无法调用i
}

7、常量

由于varlet申明的是变量,如果要申明一个常量,在ES6之前是不行的,我们通常用全部大写的变量来表示“这是一个常量,不要修改它的值”(程序员之间默认要遵守的规则,即使这个定义的常量实际上是可以被修改的):

ES6之前无const:

var PI = 3.14;
PI = 3; //可以被修改,但不要这么做
PI;   //3

ES6标准引入了新的关键字const来定义常量(readonly),constlet都具有块级作用域:

'use strict';

const PI = 3.14;  
PI = 3; // 某些浏览器不报错,但是无效果!
PI; // 3.14

8、解构赋值

变量作用域与解构赋值 - 廖雪峰的官方网站 (liaoxuefeng.com)

ES6开始,JavaScript引入了解构赋值,可以同时对一组变量进行赋值。目前支持解构赋值的浏览器包括Chrome,Firefox,Edge等。

什么是解构赋值?我们先看看传统的做法,如何把一个数组的元素分别赋值给几个变量:

var array = ['hello', 'JavaScript', 'ES6'];
var x = array[0];
var y = array[1];
var z = array[2];

现在,在ES6中,可以使用解构赋值,直接对多个变量同时赋值

'use strict';

// 如果浏览器支持解构赋值就不会报错:
var [x, y, z] = ['hello', 'JavaScript', 'ES6'];

注意,对数组元素进行解构赋值时,多个变量要用[...]括起来。

如果数组本身还有嵌套,也可以通过下面的形式进行解构赋值,注意嵌套层次和位置要保持一致

let [x, [y, z]] = ['hello', ['JavaScript', 'ES6']];
x; // 'hello'
y; // 'JavaScript'
z; // 'ES6'

解构赋值还可以忽略某些元素

let [, , z] = ['hello', 'JavaScript', 'ES6']; // 忽略前两个元素,只对z赋值第三个元素
z; // 'ES6'

如果需要从一个对象中取出若干属性,也可以使用解构赋值,便于快速获取对象的指定属性:

'use strict';

var person = {
    name: '小明',
    age: 20,
    gender: 'male',
    passport: 'G-12345678',
    school: 'No.4 middle school'
};
var {name, age, passport} = person;
			// name = '小明', age = 20, passport = 'G-12345678'

对一个对象进行解构赋值时,同样可以直接对嵌套的对象属性进行赋值,只要保证对应的层次是一致的:

var person = {
    name: '小明',
    age: 20,
    gender: 'male',
    passport: 'G-12345678',
    school: 'No.4 middle school',
    address: {
        city: 'Beijing',
        street: 'No.1 Road',
        zipcode: '100001'
    }
};
var {name, address: {city, zip}} = person;
name; // '小明'
city; // 'Beijing'
zip; // undefined, 因为属性名是zipcode而不是zip
// 注意: address不是变量,而是为了让city和zip获得嵌套的address对象的属性:
address; // Uncaught ReferenceError: address is not defined

如果要使用的变量名和属性名不一致,可以用下面的语法获取:

var person = {
    name: '小明',
    age: 20,
    gender: 'male',
    passport: 'G-12345678',
    school: 'No.4 middle school'
};

// 把passport属性赋值给变量id:
let {name, passport:id} = person;
name; // '小明'
id; // 'G-12345678'
// 注意: passport不是变量,而是为了让变量id获得passport属性:
passport; // Uncaught ReferenceError: passport is not defined

解构赋值还可以使用默认值,这样就避免了不存在的属性返回undefined的问题:

var person = {
    name: '小明',
    age: 20,
    gender: 'male',
    passport: 'G-12345678'
};

// 如果person对象没有single属性,默认赋值为true:
var {name, single=true} = person;
name; // '小明'
single; // true

使用场景

解构赋值在很多时候可以大大简化代码。例如,交换两个变量xy的值,可以这么写,不再需要临时变量:

var x=1, y=2;
[x, y] = [y, x]

快速获取当前页面的域名和路径:

var {hostname:domain, pathname:path} = location;

如果一个函数接收一个对象作为参数,那么,可以使用解构直接把对象的属性绑定到变量中。

例如,下面的函数可以快速创建一个Date对象:

function buildDate({year, month, day, hour=0, minute=0, second=0}) {
    return new Date(year , month - 1 , day , hour , minute ,second);
}
// year :The full year designation is required for cross-century date accuracy. If year is between 0 and 		99 is used, then year is assumed to be 1900 + year.
// month :The month as a number between 0 and 11 (January to December).
// date :The date as a number between 1 and 31.

console.log(buildDate({year: 2022, month: 1, day: 30}));
//Sun Jan 30 2022 00:00:00 GMT+0800 (中国标准时间)

此处传递参数时相当于

({year, month, day, hour=0, minute=0, second=0}={year: 2022, month: 1, day: 30});//必须要有小括号

有些时候,如果变量已经被声明了,再次赋值的时候,正确的写法也会报语法错误:

// 声明变量:
var x, y;
// 解构赋值:
{x, y} = { name: '小明', x: 100, y: 200};
// 语法错误: Uncaught SyntaxError: Unexpected token =

这是因为JavaScript引擎把{开头的语句当作了块处理,于是=不再合法。解决方法是用小括号括起来

({x, y} = { name: '小明', x: 100, y: 200});

7、方法

一个对象中绑定函数,称为这个对象的方法

如果我们给xiaoming绑定一个函数,就可以做更多的事情。比如,写个age()方法,返回xiaoming的年龄:

var xiaoming = {
    name: '小明',  //属性
    birth: 1990,  //属性
    age: function () {      //方法
        var y = new Date().getFullYear();
        return y - this.birth;
    }
};
//调方法一定要带()
xiaoming.age; // function xiaoming.age()
xiaoming.age(); // 根据当前时间动态的,今年调用是32,明年调用就变成33了,避免了每年改数据的麻烦

this(难点)

在一个方法内部this是一个特殊变量,它始终指向当前对象,也就是xiaoming这个变量。所以,this.birth可以拿到xiaomingbirth属性。

让我们拆开写:

function getAge() {
    var y = new Date().getFullYear();
    return y - this.birth;
}

var xiaoming = {
    name: '小明',
    birth: 1990,
    age: getAge
};

xiaoming.age(); // 25, 正常结果
getAge(); // NaN

单独调用函数getAge()怎么返回了NaN请注意,我们已经进入到了JavaScript的一个大坑里。

JavaScript的函数内部如果调用了this,那么这个this到底指向谁?

答案是,视情况而定!

如果以对象的方法形式调用,比如xiaoming.age(),该函数的this指向被调用的对象,也就是xiaoming,这是符合我们预期的。

如果单独调用函数,比如getAge(),此时,该函数的this指向全局对象,也就是window

var fn = xiaoming.age; // 先拿到xiaoming的age函数
fn(); // NaN    this->window

由于这是一个巨大的设计错误,要想纠正可没那么简单。ECMA决定,在strict模式下函数的this指向undefined,因此,在strict模式下,你会得到一个错误:

'use strict';

var xiaoming = {
    name: '小明',
    birth: 1990,
    age: function () {
        var y = new Date().getFullYear();
        return y - this.birth;
    }
};

var fn = xiaoming.age; //此时fn是一个函数,其中的this指向undefined
fn(); // Uncaught TypeError: Cannot read property 'birth' of undefined

这个决定只是让错误及时暴露出来,并没有解决this应该指向的正确位置。

要保证this指向正确,必须用obj.xxx()的形式调用!

有些时候,喜欢重构的你把方法重构了一下:

'use strict';

var xiaoming = {
    name: '小明',
    birth: 1990,
    age: function () {
        function getAgeFromBirth() {
            var y = new Date().getFullYear();
            return y - this.birth;
        }
        return getAgeFromBirth();
    }
};


xiaoming.age(); // Uncaught TypeError: Cannot read property 'birth' of undefined

结果又报错了!原因是this指针只在age方法的函数内指向xiaoming

在函数内部定义的函数getAgeFromBirth(),this又指向undefined了!(在非strict模式下,它重新指向全局对象window!)

修复的办法,我们用一个that变量首先捕获this

'use strict';

var xiaoming = {
    name: '小明',
    birth: 1990,
    age: function () {
        var that = this; // 在方法内部一开始就捕获this指向xiaoming
        function getAgeFromBirth() {
            var y = new Date().getFullYear();
            return y - that.birth; // 用that而不是this
        }
        return getAgeFromBirth();
    }
};

xiaoming.age(); // 25

var that = this;,你就可以放心地在方法内部定义其他函数,而不是把所有语句都堆到一个方法中。


apply()

虽然在一个独立的函数调用中,根据是否是strict模式,this指向undefinedwindow,不过,我们还是可以控制this的指向的!

要指定函数的this指向哪个对象,可以用函数本身的apply方法,它接收两个参数

第一个参数就是需要绑定的this变量

第二个参数Array,表示函数本身的参数。要写成数组形式

apply修复getAge()调用:

function getAge() {
    var y = new Date().getFullYear();
    return y - this.birth;
}

var xiaoming = {
    name: '小明',
    birth: 1990,
    age: getAge
};

xiaoming.age(); // 25
getAge.apply(xiaoming, []); // 25, this指向xiaoming, 参数为空

call()

另一个与apply()类似的方法是call(),唯一区别是:

  • apply()把参数打包成Array再传入,需要写成数组形式
  • call()把参数按顺序传入,不用写成数组形式

比如调用Math.max(3, 5, 4),分别用apply()call()实现如下:

Math.max.apply(null, [3, 5, 4]); // 5
Math.max.call(null, 3, 5, 4); // 5

对普通函数调用,我们通常把this绑定为null

参数传入null或者undefined时,函数调用还是会使用默认绑定。

但是总是传入null可能会出现一些难以追踪的bug,比如说当你在使用的第三方库中的某个函数中有this时,this会被错误的绑定到全局对象上,造成一些难以预料的后果(修改全局变量)

var a = 1;//全局变量
const Utils = {
	a: 2,
	changeA: function(a){
		this.a = a;
	}
}
Utils.changeA(3);
Utils.a //3
a //1
Utils.changeA.call(null,4);
Utils.a //3, Utils的a未被修改
a //4, 修改了全局变量a!

更安全的做法:

var o = Object.create(null);  //空对象,没有任何属性和方法
Utils.changeA.call(o,6);   //运行到this.a = a时,相当于直接给o添加了一个属性a = 6
a //1, 全局变量没有修改
o.a // 6 改的是变量o

例如:

var o = Object.create(null)
o.a = 6;
console.log("a = " + o.a );   //  a = 6

Object.create (null) 创建的对象是一个空对象,在该对象上没有继承 Object.prototype 原型链上的属性或者方法,例如:toString(), hasOwnProperty()等方法

Object.create ()方法接受两个参数: Object.create (obj, propertiesObject) ;

obj:一个对象,应该是新创建的对象的原型。

propertiesObject可选。该参数对象是一组属性与值,该对象的属性名称将是新创建的对象的属性名称,值是属性描述符(这些属性描述符的结构与Object.defineProperties()的第二个参数一样)。注意:该参数对象不能是 undefined,另外只有该对象中自身拥有的可枚举的属性才有效,也就是说该对象的原型链上属性是无效的。


装饰器

利用apply(),我们还可以动态改变函数的行为。

JavaScript的所有对象都是动态的,即使内置的函数,我们也可以重新指向新的函数。

现在假定我们想统计一下代码一共调用了多少次parseInt(),可以把所有的调用都找出来,然后手动加上count += 1,不过这样做太傻了。最佳方案是用我们自己的函数替换掉默认的parseInt()

'use strict';

var count = 0;
var oldParseInt = parseInt; // 保存原函数

window.parseInt = function () {
    count += 1;
    return oldParseInt.apply(null, arguments); // 调用原函数,
    							//arguments只在函数内部起作用,指向传入的所有参数,类似Array但它不是一个Array。
};

4、常用对象

1、Date

在JavaScript中,Date对象用来表示日期和时间。

JavaScript 将日期存储为 毫秒

JavaScript 将日期存储为自 1970 年 1 月 1 日 00:00:00 UTC(协调世界时)以来的毫秒数。

零时间是 1970 年 1 月 1 日 00:00:00 UTC。

要获取系统当前时间,用:

var now = new Date();
now; // Wed Jun 24 2015 19:49:22 GMT+0800 (CST)
方法 描述
getDate() 以数值返回天(1-31)
getDay() 以数值获取周名(0-6)
getFullYear() 注意不是getYear()这个已弃用 获取四位的年(yyyy)
getHours() 获取小时(0-23)
getMilliseconds() 获取毫秒(0-999)
getMinutes() 获取分(0-59)
getMonth() 获取月(0-11)
getSeconds() 获取秒(0-59)
getTime() 获取时间(从 1970 年 1 月 1 日至今)的毫秒数,即时间戳
parse(),静态方法,需要填入参数,符合ISO 8601格式的日期字符串 解析日期字符串并返回自 1970 年 1 月 1 日以来的毫秒数,即时间戳

注意,当前时间是浏览器从本机操作系统获取的时间,所以不一定准确,因为用户可以把当前时间设定为任何值。

如果要创建一个指定日期和时间的Date对象,可以用:

var d = new Date(2015, 5, 19, 20, 15, 30, 123);
d; // Fri Jun 19 2015 20:15:30 GMT+0800 (CST)      ,六月

一个非常非常坑爹的地方,就是JavaScript的月份范围用整数表示是0~110表示一月,1表示二月……,所以要表示6月,我们传入的是5!这绝对是JavaScript的设计者当时脑抽了一下,但是现在要修复已经不可能了。

第二种创建一个指定日期和时间的方法是解析一个符合ISO 8601格式的字符串:

var d = Date.parse('2015-06-24T19:49:22.875+08:00');
d; // 1435146562875

但它返回的不是Date对象,而是一个时间戳。不过有时间戳就可以很容易地把它转换为一个Date

new Date(milliseconds) 创建一个零时加毫秒的新日期对象

var d = new Date(1435146562875);
d; // Wed Jun 24 2015 19:49:22 GMT+0800 (CST)
d.getMonth(); // 5

Date对象表示的时间总是按浏览器所在时区显示的,不过我们既可以显示本地时间,也可以显示调整后的UTC时间:

方法 描述
toLocaleString() 使用区域设置约定将 Date 对象转换为字符串。
toUTCString() 根据世界时,将 Date 对象转换为字符串。
var d = new Date(1435146562875);
d.toLocaleString(); // '2015/6/24 下午7:49:22',本地时间(北京时区+8:00),显示的字符串与操作系统设定的格式有关
d.toUTCString(); // 'Wed, 24 Jun 2015 11:49:22 GMT',UTC时间,与本地时间相差8小时

2、Math

数学对象

方法

  • random(): 返回 0 ~ 1 之间的随机数。 含0不含1
  • ceil(x):对数进行上舍入。
  • floor(x):对数进行下舍入。
  • round(x):把数四舍五入为最接近的整数。

属性:PI


3、JSON(后端使用,前端解析)

JSON - 廖雪峰的官方网站 (liaoxuefeng.com)

JSON是什么?

JSON 是 JavaScript Object Notation的缩写,它是一种数据交换格式

在JSON出现之前,大家一直用XML来传递数据。因为XML是一种纯文本格式,所以它适合在网络上交换数据。XML本身不算复杂,但是,加上DTD、XSD、XPath、XSLT等一大堆复杂的规范以后,任何正常的软件开发人员碰到XML都会感觉头大了,最后大家发现,即使你努力钻研几个月,也未必搞得清楚XML的规范。

终于,在2002年的一天,道格拉斯·克罗克福特(Douglas Crockford)同学为了拯救深陷水深火热同时又被某几个巨型软件企业长期愚弄的软件工程师,发明了JSON这种超轻量级的数据交换格式。

道格拉斯同学长期担任雅虎的高级架构师,自然钟情于JavaScript。他设计的JSON实际上是JavaScript的一个子集。在JSON中,一共就这么几种数据类型:

  • number:和JavaScript的number完全一致;
  • boolean:就是JavaScript的truefalse
  • string:就是JavaScript的string
  • null:就是JavaScript的null
  • array:就是JavaScript的Array表示方式——[]
  • object:就是JavaScript的{ ... }表示方式。
  • 所有的键值对 都是用 key : value,且key和字符串需要用双引号

以及上面的任意组合。

并且,JSON还定死了字符集必须是UTF-8,表示多语言就没有问题了。为了统一解析,JSON的字符串规定必须用双引号""Object的键也必须用双引号""

由于JSON非常简单,很快就风靡Web世界,并且成为ECMA标准。几乎所有编程语言都有解析JSON的库,而在JavaScript中,我们可以直接使用JSON,因为JavaScript内置了JSON的解析。

把任何JavaScript对象变成JSON,就是把这个对象序列化成一个JSON格式的字符串,这样才能够通过网络传递给其他计算机。

如果我们收到一个JSON格式的字符串,只需要把它反序列化成一个JavaScript对象,就可以在JavaScript中直接使用这个对象了。


序列化

JSON.stringify (value, replacer, space) 静态方法

  • value – 要转换为 JSON 字符串的值。

  • replacer – 一个改变字符串化过程行为的函数,或一个String和Number数组,用作选择/过滤要包含在 JSON 字符串中的值对象的属性的允许列表。 如果此值为null或未提供,则对象的所有属性都包含在生成的 JSON 字符串中。

  • space – 一个String或Number对象,用于在输出 JSON 字符串中插入空格以提高可读性。

    如果这是一个Number ,它表示代替一个空格的空格数量; 此数字上限为 10(如果更大,则该值仅为10 )。 小于 1 的值表示不应使用空格。

    如果这是一个String ,则该字符串(或字符串的前 10 个字符,如果它比那个长)替代一个空格。 如果未提供此参数(或为null ),则不使用空格。

让我们先把小明这个对象序列化成JSON格式的字符串:

'use strict';

var xiaoming = {
    name: '小明',
    age: 14,
    gender: true,
    height: 1.65,
    grade: null,
    'middle-school': '\"W3C\" Middle School',
    skills: ['JavaScript', 'Java', 'Python', 'Lisp']
};

var s = JSON.stringify(xiaoming,null, '  ');
console.log(s);

输出:
{
  "name": "小明",
  "age": 14,
  "gender": true,
  "height": 1.65,
  "grade": null,
  "middle-school": "\"W3C\" Middle School",
  "skills": [
    "JavaScript",
    "Java",
    "Python",
    "Lisp"
  ]
}

反序列化

JSON.parse() 静态方法

  • text -- 要解析为 JSON 的字符串。有关 JSON 语法的描述,请参阅JSON对象。
  • reviver - 如果是一个函数,它规定了在返回之前如何转换最初由解析产生的值。

拿到一个JSON格式的字符串,我们直接用JSON.parse()把它变成一个JavaScript对象:

JSON.parse('[1,2,3,true]'); // [1, 2, 3, true]  字符串
JSON.parse('{"name":"小明","age":14}'); // Object {name: '小明', age: 14}    
JSON.parse('true'); // true  字符串
JSON.parse('123.45'); // 123.45  number

JSON.parse()还可以接收一个函数,用来转换解析出的属性:

var obj = JSON.parse('{"name":"小明","age":14}', function (key, value) {
    if (key === 'name') {
        return value + '同学';
    }
    return value;
});
console.log(JSON.stringify(obj)); // {name: '小明同学', age: 14}

JSON和JS对象的区别
var obj = {a:'hello', b:'world'};
var json = {"a":"hello", "b":"world"};

4、RegExp(正则表达式)

RegExp - 廖雪峰的官方网站 (liaoxuefeng.com)

基础

在正则表达式中,如果直接给出字符,就是精确匹配。用\d可以匹配一个数字,\w可以匹配一个字母或数字,所以:

  • '00\d'可以匹配'007',但无法匹配'00A'

  • '\d\d\d'可以匹配'010'

  • '\w\w'可以匹配'js'

  • .可以匹配任意字符,所以:

  • 'js.'可以匹配'jsp''jss''js!'等等。


要匹配变长的字符,在正则表达式中,

  • *表示任意个字符(包括0个),
  • +表示至少一个字符,
  • ?表示0个或1个字符,
  • {n}表示n个字符,
  • {m,n}表示m-n个字符,
  • {,n}表示最多n个字符
  • {m,}表示最少m个字符

来看一个复杂的例子:\d{3}\s+\d{3,8}

我们来从左到右解读一下:

  1. \d{3}表示匹配3个数字,例如'010'
  2. \s可以匹配一个空格(也包括Tab等空白符),所以\s+表示至少有一个空格,例如匹配' ''\t\t'等;
  3. \d{3,8}表示3-8个数字,例如'1234567'

综合起来,上面的正则表达式可以匹配以任意个空格隔开的带区号的电话号码。

如果要匹配'010-12345'这样的号码呢?由于'-'是特殊字符,在正则表达式中,要用'\'转义,所以,上面的正则是\d{3}\-\d{3,8}

但是,仍然无法匹配'010 - 12345',因为带有空格。所以我们需要更复杂的匹配方式。


进阶

要做更精确地匹配,可以用[]表示范围,比如:

  • [0-9a-zA-Z\_]可以匹配一个数字、字母或者下划线;
  • [0-9a-zA-Z\_]+可以匹配至少由一个数字、字母或者下划线组成的字符串,比如'a100''0_Z''js2015'等等;
  • [a-zA-Z\_\$][0-9a-zA-Z\_\$]*可以匹配由字母或下划线、$开头,后接任意个由一个数字、字母或者下划线、$组成的字符串,也就是JavaScript允许的变量名;
  • [a-zA-Z\_\$][0-9a-zA-Z\_\$]{0, 19}更精确地限制了变量的长度是1-20个字符(前面1个字符+后面最多19个字符)。

A|B可以匹配A或B,所以(J|j)ava(S|s)cript可以匹配'JavaScript''Javascript''javaScript'或者'javascript'

^表示行的开头,^\d表示必须以数字开头。

$表示行的结束,\d$表示必须以数字结束。

你可能注意到了,js也可以匹配'jsp',但是加上^js$就变成了整行匹配,就只能匹配'js'了。


JS中创建正则表达式

JavaScript有两种方式创建一个正则表达式

第一种方式是直接通过/正则表达式/写出来,

第二种方式是通过new RegExp('正则表达式')创建一个RegExp对象。

两种写法是一样的:

var re1 = /ABC\-001/;
var re2 = new RegExp('ABC\\-001');//注意,因为字符串的转义问题,字符串的两个\\实际上是一个\。

re1; // /ABC\-001/
re2; // /ABC\-001/

如何判断正则表达式是否匹配reg.test( )

var re = /^\d{3}\-\d{3,8}$/;
re.test('010-12345'); // true
re.test('010-1234x'); // false
re.test('010 12345'); // false

切分字符串

用正则表达式切分字符串比用固定的字符更灵活,请看正常的切分代码:

'a b   c'.split(' '); // ['a', 'b', '', '', 'c']

嗯,无法识别连续的空格,用正则表达式试试:

'a b   c'.split(/\s+/); // ['a', 'b', 'c']

无论多少个空格都可以正常分割。加入,试试:

'a,b, c  d'.split(/[\s\,]+/); // ['a', 'b', 'c', 'd']

再加入;试试:

'a,b;; c  d'.split(/[\s\,\;]+/); // ['a', 'b', 'c', 'd']

如果用户输入了一组标签,下次记得用正则表达式来把不规范的输入转化成正确的数组。


分组

除了简单地判断是否匹配之外,正则表达式还有提取子串的强大功能。用()表示的就是要提取的分组(Group)。比如:

^(\d{3})-(\d{3,8})$分别定义了两个组,可以直接从匹配的字符串中提取出区号和本地号码:

var re = /^(\d{3})-(\d{3,8})$/;
re.exec('010-12345'); // ['010-12345', '010', '12345']
re.exec('010 12345'); // null

如果正则表达式中定义了组,就可以在RegExp对象上用exec()方法提取出子串来。

exec()方法在匹配成功后,会返回一个Array,第一个元素是正则表达式匹配到的整个字符串,后面的字符串表示匹配成功的子串。

exec()方法在匹配失败时返回null

提取子串非常有用。来看一个更凶残的例子:

var re = /^(0[0-9]|1[0-9]|2[0-3]|[0-9])\:(0[0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9]|[0-9])\:(0[0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9]|[0-9])$/;
re.exec('19:05:30'); // ['19:05:30', '19', '05', '30']

这个正则表达式可以直接识别合法的时间。但是有些时候,用正则表达式也无法做到完全验证,比如识别日期:

var re = /^(0[1-9]|1[0-2]|[0-9])-(0[1-9]|1[0-9]|2[0-9]|3[0-1]|[0-9])$/;

对于'2-30''4-31'这样的非法日期,用正则还是识别不了,或者说写出来非常困难,这时就需要程序配合识别了。


贪婪匹配

需要特别指出的是,正则匹配默认是贪婪匹配,也就是匹配尽可能多的字符。举例如下,匹配出数字后面的0

var re = /^(\d+)(0*)$/;
re.exec('102300'); // ['102300', '102300', '']

由于\d+采用贪婪匹配,直接把后面的0全部匹配了,结果0*只能匹配空字符串了。

必须让\d+采用非贪婪匹配(也就是尽可能少匹配),才能把后面的0匹配出来,加个?就可以让\d+采用非贪婪匹配

var re = /^(\d+?)(0*)$/;
re.exec('102300'); // ['102300', '1023', '00']

全局搜索

JavaScript的正则表达式还有几个特殊的标志,最常用的是g,表示全局匹配:

var r1 = /test/g;
// 等价于:
var r2 = new RegExp('test', 'g');

全局匹配可以多次执行exec()方法来搜索一个匹配的字符串。当我们指定g标志后,每次运行exec(),正则表达式本身会更新lastIndex属性,表示上次匹配到的最后索引:

var s = 'JavaScript, VBScript, JScript and ECMAScript';
var re=/[a-zA-Z]+Script/g;

// 使用全局匹配:
re.exec(s); // ['JavaScript']
re.lastIndex; // 10

re.exec(s); // ['VBScript']
re.lastIndex; // 20

re.exec(s); // ['JScript']
re.lastIndex; // 29

re.exec(s); // ['ECMAScript']
re.lastIndex; // 44

re.exec(s); // null,直到结束仍没有匹配到

全局匹配类似搜索,因此不能使用/^...$/,那样只会最多匹配一次。

正则表达式还可以:

  • 指定i标志,表示忽略大小写,
  • m标志,表示执行多行匹配。

练习

尝试写一个验证Email地址的正则表达式。版本一应该可以验证出类似的Email:

var re = /^[0-9a-zA-Z]+\.?[0-9a-zA-Z]*@[0-9a-zA-Z]+\.(com|org)$/;
var
        i,
        success = true,
        should_pass = ['someone@gmail.com', 'bill.gates@microsoft.com', 'tom@voyager.org', 'bob2015@163.com'],
        should_fail = ['test#gmail.com', 'bill@microsoft', 'bill%gates@ms.com', '@voyager.org'];
for (i = 0; i < should_pass.length; i++) {
  if (!re.test(should_pass[i])) {
    console.log('测试失败: ' + should_pass[i]);
    success = false;
    break;
  }
}
for (i = 0; i < should_fail.length; i++) {
  if (re.test(should_fail[i])) {
    console.log('测试失败: ' + should_fail[i]);
    success = false;
    break;
  }
}
if (success) {
  console.log('测试通过!');
}

5、Global

全局对象,这个Global中封装的方法不需要对象就可以直接调用。

JavaScript 全局参考手册 (w3school.com.cn)

函数 描述
decodeURI() 解码 URI。
decodeURIComponent() 解码 URI 组件。
encodeURI() 对 URI 进行编码。编码的字符更多
encodeURIComponent() 对 URI 组件进行编码。
parseInt() 解析字符串并返回整数。逐一判断每一个字符是否是数字,直到不是数字为止,将前边数字部分转为number
parseFloat() 解析字符串并返回浮点数。
String() 将对象的值转换为字符串。
Number() 将对象的值转换为数字。
isFinite() 确定值是否是有限的合法数。
isNaN() 确定值是否是非法数字。NaN六亲不认,连自己都不认。NaN参与的 == 比较全部问false
eval() 评估字符串并像脚本代码一样执行它。
escape() 在 1.5 版中已弃用。请使用 encodeURI()encodeURIComponent() 代替。
unescape() 在 1.5 版中已弃用。请使用 decodeURI()decodeURIComponent() 代替。

4、面向对象编程

JavaScript的面向对象编程和大多数其他语言如Java、C#的面向对象编程都不太一样。如果你熟悉Java或C#,很好,你一定明白面向对象的两个基本概念:

  • 类:类是对象的类型模板,例如,定义Student类来表示学生,类本身是一种类型,Student表示学生类型,但不表示任何具体的某个学生;
  • 实例:实例是根据类创建的对象,例如,根据Student类可以创建出xiaomingxiaohongxiaojun等多个实例,每个实例表示一个具体的学生,他们全都属于Student类型。

所以,类和实例是大多数面向对象编程语言的基本概念。

不过,在JavaScript中,这个概念需要改一改。

JavaScript不区分类和实例的概念,而是通过原型(prototype)来实现面向对象编程。


4.1、原型

构造函数、原型和实例的关系:

javascript—原型与原型链

每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。

原型链:

图中由相互关联的原型组成的链状结构就是原型链,也就是蓝色的这条线

原型是指当我们想要创建xiaoming这个具体的学生时,我们并没有一个Student类型可用。那怎么办?恰好有这么一个现成的对象:

var robot = {
    name: 'Robot',
    height: 1.6,
    run: function () {
        console.log(this.name + ' is running...');
    }
};

我们看这个robot对象有名字,有身高,还会跑,有点像小明,干脆就根据它来“创建”小明得了!

于是我们把它改名为Student,然后创建出xiaoming

var Student = {
    name: 'Robot',
    height: 1.2,
    run: function () {
        console.log(this.name + ' is running...');
    }
};

var xiaoming = {
    name: '小明'
};

xiaoming.__proto__ = Student;  //左右各两个下划线

注意最后一行代码xiaoming的原型指向了对象Student看上去xiaoming仿佛是从Student继承下来的:

xiaoming有自己的name属性,但并没有定义run()方法。不过,由于小明是从Student继承而来,只要Studentrun()方法,xiaoming也可以调用。

JavaScript的原型链和Java的Class区别就在,它没有“Class”的概念,所有对象都是实例,所谓继承关系不过是把一个对象的原型指向另一个对象而已。

如果你把xiaoming的原型指向其他对象:

var Bird = {
    fly: function () {
        console.log(this.name + ' is flying...');
    }
};

xiaoming.__proto__ = Bird;

现在xiaoming已经无法run()了,他已经变成了一只鸟:

xiaoming.fly(); // 小明 is flying...

请注意,上述代码仅用于演示目的。

在编写JavaScript代码时,不要直接用obj.__proto__去改变一个对象的原型,并且,低版本的IE也无法使用__proto__Object.create()方法可以传入一个原型对象,并创建一个基于该原型的新对象,但是新对象什么属性都没有,因此,我们可以编写一个函数来创建xiaoming

// 原型对象:
var Student = {
    name: 'Robot',
    height: 1.2,
    run: function () {
        console.log(this.name + ' is running...');
    }
};

function createStudent(name) {
    // 基于Student原型创建一个新对象:
    var s = Object.create(Student);
    // 初始化新对象:
    s.name = name;
    return s;
}

var xiaoming = createStudent('小明');
xiaoming.run(); // 小明 is running...
xiaoming.__proto__ === Student; // true

4.2、创建对象

创建对象 - 廖雪峰的官方网站 (liaoxuefeng.com)

JavaScript对每个创建的对象都会设置一个原型,指向它的原型对象。

当我们用obj.xxx访问一个对象的属性时,JavaScript引擎先在当前对象上查找该属性,如果没有找到,就到其原型对象上找,如果还没有找到,就一直上溯到Object.prototype对象,最后,如果还没有找到,就只能返回undefined

例如,创建一个Array对象:

var arr = [1, 2, 3];

其原型链是:

arr ----> Array.prototype ----> Object.prototype ----> null

Array.prototype定义了indexOf()shift()等方法,因此你可以在所有的Array对象上直接调用这些方法。

当我们创建一个函数时:

function foo() {
    return 0;
}

函数也是一个对象,它的原型链是:

foo ----> Function.prototype ----> Object.prototype ----> null

由于Function.prototype定义了apply()等方法,因此,所有函数都可以调用apply()方法。


构造函数

除了直接用{ ... }创建一个对象外,JavaScript还可以用一种构造函数的方法来创建对象。它的用法是,先定义一个构造函数:

function Student(name) {
    this.name = name;
    this.hello = function () {
        alert('Hello, ' + this.name + '!');
    }
}

这确实是一个普通函数,但是在JavaScript中,可以用关键字new来调用这个函数,并返回一个对象:

var xiaoming = new Student('小明');
xiaoming.name; // '小明'
xiaoming.hello(); // Hello, 小明!

注意,如果不写new,这就是一个普通函数,它返回undefined。但是,如果写了new,它就变成了一个构造函数,它绑定的this指向新创建的对象,并默认返回this,也就是说,不需要在最后写return this;

新创建的xiaoming的原型链是:

xiaoming ----> Student.prototype ----> Object.prototype ----> null

也就是说,xiaoming的原型指向函数Student的原型。如果你又创建了xiaohongxiaojun,那么这些对象的原型与xiaoming是一样的:

xiaoming ↘
xiaohong -→ Student.prototype ----> Object.prototype ----> null
xiaojun  ↗

new Student()创建的对象还从原型上获得了一个constructor属性,它指向函数Student本身:

xiaoming.constructor === Student.prototype.constructor; // true
Student.prototype.constructor === Student; // true

Object.getPrototypeOf(xiaoming) === Student.prototype; // true

xiaoming instanceof Student; // true

注意观察:

xiaoming.name; // '小明'
xiaohong.name; // '小红'
xiaoming.hello; // function: Student.hello()
xiaohong.hello; // function: Student.hello()
xiaoming.hello === xiaohong.hello; // false

xiaomingxiaohong各自的name不同,这是对的,否则我们无法区分谁是谁了。

xiaomingxiaohong各自的hello是一个函数,但它们是两个不同的函数,虽然函数名称和代码都是相同的!

如果我们通过new Student()创建了很多对象,这些对象的hello函数实际上只需要共享同一个函数就可以了,这样可以节省很多内存。

要让创建的对象共享一个hello函数,根据对象的属性查找原则,我们只要把hello函数移动到xiaomingxiaohong这些对象共同的原型上就可以了,也就是Student.prototype

修改代码如下:

function Student(name) {
    this.name = name;
}

Student.prototype.hello = function () {
    alert('Hello, ' + this.name + '!');
};
忘记写new怎么办

如果一个函数被定义为用于创建对象的构造函数,但是调用时忘记了写new怎么办?

在strict模式下,this.name = name将报错,因为this绑定为undefined,在非strict模式下,this.name = name不报错,因为this绑定为window,于是无意间创建了全局变量name,并且返回undefined,这个结果更糟糕。

所以,调用构造函数千万不要忘记写new

为了区分普通函数和构造函数,按照约定,构造函数首字母应当大写,而普通函数首字母应当小写,

这样,一些语法检查工具如jslint将可以帮你检测到漏写的new

最后,我们还可以编写一个createStudent()函数,在内部封装所有的new操作。一个常用的编程模式像这样:

function Student(props) {
    this.name = props.name || '匿名'; // 默认值为'匿名'
    this.grade = props.grade || 1; // 默认值为1
}

Student.prototype.hello = function () {
    alert('Hello, ' + this.name + '!');
};

function createStudent(props) {
    return new Student(props || {})
}

这个createStudent()函数有几个巨大的优点:一是不需要new来调用,二是参数非常灵活,可以不传,也可以这么传:

var xiaoming = createStudent({           //传入参数对象
    name: '小明'
});

xiaoming.grade; // 1

如果创建的对象有很多属性,我们只需要传递需要的某些属性,剩下的属性可以用默认值。由于参数是一个Object,我们无需记忆参数的顺序。如果恰好从JSON拿到了一个对象,就可以直接创建出xiaoming


4.3、原型继承(ES6之前,了解)

原型继承 - 廖雪峰的官方网站 (liaoxuefeng.com)

在传统的基于Class的语言如Java、C++中,继承的本质是扩展一个已有的Class,并生成新的Subclass。

由于这类语言严格区分类和实例,继承实际上是类型的扩展。但是,JavaScript由于采用原型继承,我们无法直接扩展一个Class,因为根本不存在Class这种类型。

function Student(props) {
    this.name = props.name || 'Unnamed';
}

Student.prototype.hello = function () {
    alert('Hello, ' + this.name + '!');
}

我们要基于Student扩展出PrimaryStudent,可以先定义出PrimaryStudent

function PrimaryStudent(props) {
    // 调用Student构造函数,绑定this变量:
    Student.call(this, props);
    this.grade = props.grade || 1;
}

但是,调用了Student构造函数不等于继承了StudentPrimaryStudent创建的对象的原型是:

new PrimaryStudent() ----> PrimaryStudent.prototype ----> Object.prototype ----> null

必须想办法把原型链修改为:

new PrimaryStudent() ----> PrimaryStudent.prototype ----> Student.prototype ----> Object.prototype ----> null

这样,原型链对了,继承关系就对了。新的基于PrimaryStudent创建的对象不但能调用PrimaryStudent.prototype定义的方法,也可以调用Student.prototype定义的方法。

如果你想用最简单粗暴的方法这么干:

PrimaryStudent.prototype = Student.prototype;

是不行的!如果这样的话,PrimaryStudentStudent共享一个原型对象,那还要定义PrimaryStudent干啥?

我们必须借助一个中间对象来实现正确的原型链,这个中间对象的原型要指向Student.prototype。为了实现这一点,参考道爷(就是发明JSON的那个道格拉斯)的代码,中间对象可以用一个空函数F来实现

// PrimaryStudent构造函数:
function PrimaryStudent(props) {
    Student.call(this, props);
    this.grade = props.grade || 1;
}

// 空函数F:
function F() {
}

// 把F的原型指向Student.prototype:
F.prototype = Student.prototype;

// 把PrimaryStudent的原型指向一个新的F对象,F对象的原型正好指向Student.prototype:
PrimaryStudent.prototype = new F();

// 把PrimaryStudent原型的构造函数修复为PrimaryStudent:
PrimaryStudent.prototype.constructor = PrimaryStudent;

// 继续在PrimaryStudent原型(就是new F()对象)上定义方法:
PrimaryStudent.prototype.getGrade = function () {
    return this.grade;
};

// 创建xiaoming:
var xiaoming = new PrimaryStudent({
    name: '小明',
    grade: 2
});
xiaoming.name; // '小明'
xiaoming.grade; // 2

// 验证原型:
xiaoming.__proto__ === PrimaryStudent.prototype; // true
xiaoming.__proto__.__proto__ === Student.prototype; // true

// 验证继承关系:
xiaoming instanceof PrimaryStudent; // true
xiaoming instanceof Student; // true

注意,函数F仅用于桥接,我们仅创建了一个new F()实例,而且,没有改变原有的Student定义的原型链。

如果把继承这个动作用一个inherits()函数封装起来,还可以隐藏F的定义,并简化代码:

function inherits(Child, Parent) {
    var F = function () {};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.prototype.constructor = Child;
}

这个inherits()函数可以复用:

function Student(props) {
    this.name = props.name || 'Unnamed';
}

Student.prototype.hello = function () {
    alert('Hello, ' + this.name + '!');
}

function PrimaryStudent(props) {
    Student.call(this, props); //将Student中的this绑定为PrimaryStudent的this,来达到继承属性和方法的效果
    this.grade = props.grade || 1;
}

// 实现原型继承链:
inherits(PrimaryStudent, Student);

// 绑定其他方法到PrimaryStudent原型:
PrimaryStudent.prototype.getGrade = function () {
    return this.grade;
};
小结

JavaScript的原型继承实现方式就是:

  1. 定义新的构造函数,并在内部用call()调用希望“继承”的构造函数,并绑定this

    // PrimaryStudent构造函数:
    function PrimaryStudent(props) {
        Student.call(this, props);
        this.grade = props.grade || 1;
    }
    
  2. 借助中间函数F实现原型链继承,最好通过封装的inherits函数完成;

    // 空函数F:
    function F() {
    }
    
    // 把PrimaryStudent的原型指向一个新的F对象,F对象的原型正好指向Student.prototype:
    PrimaryStudent.prototype = new F();
    
    // 把PrimaryStudent原型的构造函数修复为PrimaryStudent:
    PrimaryStudent.prototype.constructor = PrimaryStudent;
    

    把继承这个动作用一个inherits()函数封装起来,还可以隐藏F的定义,并简化代码:

    function inherits(Child, Parent) {
        var F = function () {};
        F.prototype = Parent.prototype;
        Child.prototype = new F();
        Child.prototype.constructor = Child;
    }
    
  3. 继续在新的构造函数的原型上定义新方法

    // 继续在PrimaryStudent原型(就是new F()对象)上定义方法:
    PrimaryStudent.prototype.getGrade = function () {
        return this.grade;
    };
    

4.4、class继承(ES6,越来越像java)

class继承 - 廖雪峰的官方网站 (liaoxuefeng.com)

JavaScript的对象模型是基于原型实现的,特点是简单,缺点是理解起来比传统的类-实例模型要困难,最大的缺点是继承的实现需要编写大量代码,并且需要正确实现原型链。

新的关键字classES6 开始正式被引入到JavaScript中。class的目的就是让定义类更简单。

我们先回顾用函数实现Student的方法:

function Student(name) {
    this.name = name;
}

Student.prototype.hello = function () {
    alert('Hello, ' + this.name + '!');
}

如果用新的class关键字来编写Student,可以这样写:

class Student {
    constructor(name) {    //构造函数
        this.name = name;
    }

    hello() {
        alert('Hello, ' + this.name + '!');
    }
}

比较一下就可以发现,class的定义包含了构造函数constructor和定义在原型对象上的函数hello()(注意没有function关键字),这样就避免了Student.prototype.hello = function () {...}这样分散的代码。

最后,创建一个Student对象代码和前面章节完全一样:

var xiaoming = new Student('小明');
xiaoming.hello();

class定义对象的另一个巨大的好处是继承更方便了。

想一想我们从Student派生一个PrimaryStudent需要编写的代码量。现在,原型继承的中间对象,原型对象的构造函数等等都不需要考虑了,直接通过extends来实现

class PrimaryStudent extends Student {
    constructor(name, grade) {
        super(name); // 记得用super调用父类的构造方法!
        this.grade = grade;
    }

    myGrade() {
        alert('I am at grade ' + this.grade);
    }
}

注意PrimaryStudent的定义也是class关键字实现的,而extends则表示原型链对象来自Student。子类的构造函数可能会与父类不太相同,例如,PrimaryStudent需要namegrade两个参数,并且需要通过super(name)来调用父类的构造函数,否则父类的name属性无法正常初始化。

PrimaryStudent已经自动获得了父类Studenthello方法,我们又在子类中定义了新的myGrade方法。

ES6 引入的class和原有的JavaScript原型继承有什么区别呢?

实际上它们没有任何区别,class的作用就是让JavaScript引擎去实现原来需要我们自己编写的原型链代码。简而言之,用class的好处就是极大地简化了原型链代码。

你一定会问,class这么好用,能不能现在就用上?

现在用还早了点,因为不是所有的主流浏览器都支持ES6的class。如果一定要现在就用上,就需要一个工具把class代码转换为传统的prototype代码,可以试试Babel这个工具。


5、操作BOM对象(重点)

浏览器对象 - 廖雪峰的官方网站 (liaoxuefeng.com)

定义:BOM (Browser Object Model) 是 指浏览器对象模型

JavaScript可以获取浏览器提供的很多对象,并进行操作。


5.1、window(重要)

Window 对象 (w3school.com.cn)

常用方法 描述
alert() 显示带有一段消息和一个确认按钮的警告框。
confirm() 显示带有一段消息以及 确认按钮(返回true) 和 取消按钮(返回false) 的对话框。
print() 打印当前窗口的内容。
prompt() 显示可提示用户输入的对话框。
open() 打开一个新的浏览器窗口或查找一个已命名的窗口。返回值是一个window对象
close() 关闭调用的window对象的窗口。
setTimeout(code, millisec) 在指定的毫秒数后调用函数或计算表达式,返回值是一个唯一的数值id。只执行 code 一次
clearTimeout(id_of_settimeout) 取消由 setTimeout() 方法设置的 timeout。
setInterval(code, millisec) 按照指定的周期(以毫秒计)来调用函数或计算表达式,返回值是一个唯一的数值id。循环执行
clearInterval(id_of_setinterval) 取消由 setInterval() 设置的 timeout。

window对象不但充当全局作用域,而且表示浏览器窗口。

window对象有innerWidthinnerHeight属性,可以获取浏览器窗口的内部宽度和高度。内部宽高是指除去菜单栏、工具栏、边框等占位元素后,用于显示网页的净宽高。对应的,还有一个outerWidthouterHeight属性,可以获取浏览器窗口的整个宽高。

兼容性:IE<=8不支持。

console.log(innerHeight)   //默认this指向window
//706

5.2、navigator(不建议使用)

navigator对象表示浏览器的信息,最常用的属性包括:

  • navigator.appName:浏览器名称;
  • navigator.appVersion:浏览器版本;
  • navigator.language:浏览器设置的语言;
  • navigator.platform:操作系统类型;
  • navigator.userAgent:浏览器设定的User-Agent字符串。
console.log('浏览器名称: '+ navigator.appName)
//浏览器名称: Netscape(网景)   

请注意navigator的信息可以很容易地被用户修改,所以JavaScript读取的值不一定是正确的。


5.3、screen

screen对象表示屏幕的信息,常用的属性有:

  • screen.width:屏幕宽度,以像素为单位;
  • screen.height:屏幕高度,以像素为单位;
  • screen.colorDepth:返回颜色位数,如8、16、24。

5.4、location(重要)

location对象表示当前页面的URL信息。例如,一个完整的URL:

location.href; //查询当前页面URL
http://www.example.com:8080/path/index.html?a=1&b=2#TOP

可以用location.href获取。要获得URL各个部分的值,可以这么写:

location.protocol; // 'http'
location.host; // 'www.example.com'
location.port; // '8080'
location.pathname; // '/path/index.html'
location.search; // '?a=1&b=2'
location.hash; // 'TOP'
location.reload(); //重新加载,刷新页面
location.assign('https://www.baidu.com'); //由当前页面跳转至百度

要加载一个新页面,可以调用location.assign()。如果要重新加载当前页面,调用location.reload()方法非常方便。


5.5、document(DOM)

方法 描述
close() 关闭用 document.open() 方法打开的输出流,并显示选定的数据。
getElementById() 返回对拥有指定 id 的第一个对象的引用。
getElementsByName() 返回带有指定名称的对象集合。
getElementsByTagName() 返回带有指定标签名的对象集合。
open() 打开一个流,以收集来自任何 document.write() 或 document.writeln() 方法的输出。
write() 向文档写 HTML 表达式 或 JavaScript 代码。
writeln() 等同于 write() 方法,不同的是在每个表达式之后写一个换行符。

document对象表示当前页面。由于HTML在浏览器中以DOM形式表示为树形结构,document对象就是整个DOM树的根节点。

documenttitle属性是从HTML文档中的<title>xxx</title>读取的,但是可以动态改变:

document.title = '努力学习JavaScript!';   //浏览器窗口标题改变

要查找DOM树的某个节点,需要从document对象开始查找。最常用的查找是根据ID和Tag Name。

document对象提供的getElementById()getElementsByTagName()

//我们先准备HTML数据:
<body>
    
  <dl id="drink-menu" style="border:solid 1px #ccc;padding:6px;">
    <dt>摩卡</dt>
    <dd>热摩卡咖啡</dd>
    <dt>酸奶</dt>
    <dd>北京老酸奶</dd>
    <dt>果汁</dt>
    <dd>鲜榨苹果汁</dd>
  </dl>

  <script>
//可以按ID获得一个DOM节点和按Tag名称获得一组DOM节点:
    var menu = document.getElementById('drink-menu');
    var drinks = document.getElementsByTagName('dt');
    var i, s;

    s = '提供的饮料有:';
    for (i=0; i<drinks.length; i++) {
      s = s + drinks[i].innerHTML + ',';
    }
    console.log(s);

  </script>
</body>

//提供的饮料有:摩卡,酸奶,果汁,
write()

向文档写 HTML 表达式 或 JavaScript 代码。

<!-- 练习:99乘法表 -->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>99乘法表</title>
    <style>
        td {
            border: 1px solid;
        }
    </style>

    <script>
        document.write("<table  align='center'>");
        //1.完成基本的for循环嵌套,展示乘法表
        for (var i = 1; i <= 9; i++) {
            document.write("<tr>");
            for (var j = 1; j <= i; j++) {
                document.write("<td>");
                //输出  i * j 
                document.write(i + " * " + j + " = " + (i * j) + "&nbsp;&nbsp;&nbsp;");
                document.write("</td>");
            }
            document.write("</tr>");
        }
        //2.完成表格嵌套
        document.write("</table>");
    </script>
</head>
<body>

</body>
</html>

解决关于js报错 Cannot read property innerHTML of null

  1. 相信很多同学在开发过程中都会遇到
    Cannot read property ‘innerHTML’ of null ——这个报错的字面含义是:不能读取空的内部HTML
  2. 实际上,在页面的HTML结构中,innerHTML是有实际的值并可以在console进行获取查询到。
  3. 根据浏览器的渲染原理,HTML代码从上到下执行代码,当浏览器JS解析器解析到script并进行DOM操作,下面的DOM结构还没有进行搭建。那么就会提示不能正确读取内部的HTML信息。
  4. 解决思路:将涉及DOM读取部分的script放在DOM结构后面。标签前面。或者在前置的script标签写明:window.onload等,确保DOM树加载完毕后再执行这段script代码。

document对象还有一个cookie属性,可以获取当前页面的Cookie。

Cookie 是由服务器发送的key-value标示符。因为HTTP协议是无状态的,但是服务器要区分到底是哪个用户发过来的请求,就可以用Cookie来区分。当一个用户成功登录后,服务器发送一个Cookie给浏览器,例如user=ABC123XYZ(加密的字符串)...,此后,浏览器访问该网站时,会在请求头附上这个Cookie,服务器根据Cookie即可区分出用户。

Cookie还可以存储网站的一些设置,例如,页面显示的语言等等。

JavaScript可以通过document.cookie读取到当前页面的Cookie:

document.cookie; // 'v=123; remember=true; prefer=zh'

由于JavaScript能读取到页面的Cookie,而用户的登录信息通常也存在Cookie中,这就造成了巨大的安全隐患,这是因为在HTML页面中引入第三方的JavaScript代码是允许的:

<!-- 当前页面在www.example.com -->
<html>
    <head>
        <script src="http://www.foo.com/jquery.js"></script>
    </head>
    ...
</html>

如果引入的第三方的JavaScript中存在恶意代码,则www.foo.com网站将直接获取到www.example.com网站的用户登录信息。

为了解决这个问题,服务器在设置Cookie时可以使用httpOnly,设定了httpOnly的Cookie将不能被JavaScript读取。这个行为由浏览器实现,主流浏览器均支持httpOnly选项,IE从IE6 SP1开始支持。

为了确保安全,服务器端在设置Cookie时,应该始终坚持使用httpOnly


history(不建议使用)

history对象保存了浏览器的历史记录,JavaScript可以调用history对象的back()forward ()

相当于用户点击了浏览器的“后退”或“前进”按钮。

这个对象属于历史遗留对象,对于现代Web页面来说,由于大量使用 AJAX 和页面交互,简单粗暴地调用history.back()可能会让用户感到非常愤怒。

新手开始设计Web页面时喜欢在登录页登录成功时调用history.back(),试图回到登录前的页面。这是一种错误的方法。

任何情况,你都不应该使用history这个对象了。


6、操作DOM对象(重要)

操作DOM - 廖雪峰的官方网站 (liaoxuefeng.com)

HTML DOM Element 对象 (w3school.com.cn)

6.1、DOM对象

DOM,全称“Document Object Model(文档对象模型)”,它是由W3C组织定义的一个标准。

在前端开发时,我们往往需要在页面某个地方添加一个元素或者删除元素,这种添加元素、删除元素的操作就是通过DOM来实现的。

说白了,DOM就是一个接口,我们可以通过DOM来操作页面中各种元素,例如添加元素、删除元素、替换元素等。

W3C DOM 标准被分为 3 个不同的部分:

  • Core DOM - 所有文档类型的标准模型
  • XML DOM - XML 文档的标准模型
  • HTML DOM - HTML 文档的标准模型

6.2、DOM结构

DOM采用树形结构作为分层结构,以树节点形式表示页面中各种元素或内容。

在DOM中,每一个元素看成一个节点,而每一个节点就是一个“对象”。也就是我们在操作元素时,把每一个元素节点看成一个对象,然后使用这个对象的属性和方法进行相关操作。(这句话对理解DOM操作太重要了)。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
  <h1>操作DOM对象</h1>
  <p>
    这是段落
  </p>
</body>
</html>
节点

下面我们介绍几个关于节点的概念。

1、根节点

在HTML文档中,html就是根节点

2、父节点

一个节点之上的节点就是该节点的父节点,例如h1的父节点就是body,body的父节点就是html。

3、子节点

一个节点之下的节点就是该节点的子节点,例如h1就是body的子节点。

4、兄弟节点

如果多个节点在同一层次,并拥有相同的父节点,那么这几个节点就是兄弟节点。

例如h1和p就是兄弟节点,因为他们拥有相同的父节点body。


6.3、操作DOM

由于HTML文档被浏览器解析后就是一棵DOM树,要改变HTML的结构,就需要通过JavaScript来操作DOM。

始终记住DOM是一个树形结构。操作一个DOM节点实际上就是这么几个操作:

  • 更新:更新该DOM节点的内容,相当于更新了该DOM节点表示的HTML的内容;
  • 遍历:遍历该DOM节点下的子节点,以便进行进一步操作;
  • 添加:在该DOM节点下新增一个子节点,相当于动态增加了一个HTML节点;
  • 删除:将该节点从HTML中删除,相当于删掉了该DOM节点的内容以及它包含的所有子节点。

1、查询DOM
第一种方法(原生):

在操作一个DOM节点前,我们需要通过各种方式先拿到这个DOM节点。

最常用的方法是document.getElementById()document.getElementsByTagName()

以及CSS选择器document.getElementsByClassName()

  • 由于ID在HTML文档中是唯一的,所以document.getElementById()可以直接定位唯一的一个DOM节点

  • document.getElementsByTagName()document.getElementsByClassName()总是返回一组DOM节点数组

要精确地选择DOM,可以先定位父节点,再从父节点开始选择,以缩小范围。(类似CSS选择器

// 返回ID为'test'的节点:
var test = document.getElementById('test');

// 先定位ID为'test-table'的节点,再返回其内部所有tr节点:
var trs = document.getElementById('test-table').getElementsByTagName('tr');

// 先定位ID为'test-div'的节点,再返回其内部所有class包含red的节点:
var reds = document.getElementById('test-div').getElementsByClassName('red');

// 获取节点test下的所有直属子节点:
var cs = test.children;

// 获取节点test下第一个、最后一个子节点:
var first = test.firstElementChild;
var last = test.lastElementChild;

第二种方法:

是使用querySelector()querySelectorAll(),需要了解selector语法,然后使用条件来获取节点,更加方便:

// 通过querySelector获取ID为q1的节点:
var q1 = document.querySelector('#q1');

// 通过querySelectorAll获取q1节点内的符合条件的所有节点:
var ps = q1.querySelectorAll('div.highlighted > p');

注意:低版本的IE<8不支持querySelectorquerySelectorAll。IE8仅有限支持。

严格地讲,我们这里的DOM节点是指Element,但是DOM节点实际上是Node,在HTML中,Node包括ElementCommentCDATA_SECTION等很多种,以及根节点Document类型,但是,绝大多数时候我们只关心Element,也就是实际控制页面结构的Node,其他类型的Node忽略即可。根节点Document已经自动绑定为全局变量document


2、更新DOM

拿到一个DOM节点后,我们可以对它进行更新。

可以直接修改节点的文本,方法有两种:

1、innerHTML(强大,但有安全隐患)

一种是修改innerHTML属性,这个方式非常强大,不但可以修改一个DOM节点的文本内容,还可以直接通过HTML片段修改DOM节点内部的子树:

// 获取<p id="p-id">...</p>
var p = document.getElementById('p-id');
// 设置文本为abc:
p.innerHTML = 'ABC'; // <p id="p-id">ABC</p>
// 设置HTML:
p.innerHTML = 'ABC <span style="color:red">RED</span> XYZ';
// <p>...</p>的内部结构已修改

innerHTML时要注意,是否需要写入HTML。如果写入的字符串是通过网络拿到的,要注意对字符编码来避免XSS攻击。


2、innerText / innerContent (安全)

第二种是修改innerTexttextContent属性,这样可以自动对字符串进行HTML编码保证无法设置任何HTML标签

// 获取<p id="p-id">...</p>
var p = document.getElementById('p-id');
// 设置文本:
p.innerText = '<script>alert("Hi")</script>';
// HTML被自动编码,无法设置一个<script>节点:
// <p id="p-id">&lt;script&gt;alert("Hi")&lt;/script&gt;</p>

两者的区别在于读取属性时,innerText不返回隐藏元素的文本,而textContent返回所有文本。

另外注意 IE<9 不支持textContent


3、修改CSS

修改CSS也是经常需要的操作。DOM节点的style属性对应所有的CSS,可以直接获取或设置。因为CSS允许font-size这样的名称,但它并非JavaScript有效的属性名,所以需要在JavaScript中改写为驼峰式命名fontSize

  • 属性使用 字符串 包裹
  • CSS中带 - 的命名要改成 驼峰命名
// 获取<p id="p-id">...</p>
var p = document.getElementById('p-id');
// 设置CSS:
p.style.color = 'yellow';
p.style.fontSize = '20px';
p.style.paddingTop = '2em';
4、修改属性
属性 / 方法 描述
element.getAttribute("属性名") 返回元素节点的指定属性值。
element.setAttribute("属性名","属性值") 把指定属性设置或更改为指定值。
element.removeAttribute("属性名") 从元素中移除指定属性。

3、插入DOM

当我们获得了某个DOM节点,想在这个DOM节点内插入新的DOM,应该如何做?

如果这个DOM节点是空的,例如,<div></div>,那么,直接使用innerHTML = '<span>child</span>'就可以修改DOM节点的内容,相当于“插入”了新的DOM节点。

如果这个DOM节点不是空的,那就不能这么做,因为innerHTML会直接替换掉原来的所有子节点

1、appendChild (添加到末尾)

一个是使用appendChild,把一个子节点添加到父节点的最后一个子节点。例如:

<!-- HTML结构 -->
<p id="js">JavaScript</p>      //这个标签移动到div里面后会消失
<div id="list">
    <p id="java">Java</p>
    <p id="python">Python</p>
    <p id="scheme">Scheme</p>
</div>

<p id="js">JavaScript</p>添加到<div id="list">的最后一项:

var
    js = document.getElementById('js'),
    list = document.getElementById('list');
list.appendChild(js);

现在,HTML结构变成了这样:

<!-- HTML结构 -->
<div id="list">
    <p id="java">Java</p>
    <p id="python">Python</p>
    <p id="scheme">Scheme</p>
    <p id="js">JavaScript</p>
</div>

因为我们插入的js节点已经存在于当前的文档树,因此这个节点首先会从原先的位置删除,再插入到新的位置。

更多的时候我们会从零创建一个新的节点,然后插入到指定位置:

var
    list = document.getElementById('list'),
    haskell = document.createElement('p');
haskell.id = 'haskell';
haskell.innerText = 'Haskell';
list.appendChild(haskell);

这样我们就动态添加了一个新的节点:

<!-- HTML结构 -->
<div id="list">
    <p id="java">Java</p>
    <p id="python">Python</p>
    <p id="scheme">Scheme</p>
    <p id="haskell">Haskell</p>
</div>

动态创建一个节点然后添加到DOM树中,可以实现很多功能。举个例子,下面的代码动态创建了一个<style>节点,然后把它添加到<head>节点的末尾,这样就动态地给文档添加了新的CSS定义:

var d = document.createElement('style');  //创建一个<style>节点
d.setAttribute('type', 'text/css');    //下面有这个函数的解释
d.innerHTML = 'p { color: red }';
document.getElementsByTagName('head')[0].appendChild(d); //虽然head标签只有一个,但是还是要用数组的调用方式[0]

getElementsByTagName() 返回的是一个数组,调用它的元素需要使用下标,即使只有一个元素

**setAttribute(name, value) **

设置指定元素的属性值。 如果属性(attribute)已经存在,则更新值; 否则将添加具有指定名称和值的新属性。

要获取属性的当前值,请使用getAttribute() ; 要删除属性,请调用removeAttribute() 。

参数:

  • name – 一个DOMString ,指定要设置其值的属性的名称。 在 HTML 文档中的 HTML 元素上调用setAttribute()时,属性名称会自动转换为全部小写。
  • value – 一个DOMString ,包含要分配给属性的值。 指定的任何非字符串值都会自动转换为字符串。

2、insertBefore(插入到指定位置)

如果我们要把子节点插入到指定的位置怎么办?可以使用parentElement.insertBefore(newElement, referenceElement);,子节点会插入到referenceElement之前。

还是以上面的HTML为例,假定我们要把Haskell插入到Python之前:

<!-- HTML结构 -->
<div id="list">
    <p id="java">Java</p>
    <p id="python">Python</p>
    <p id="scheme">Scheme</p>
</div>

可以这么写:

var
    list = document.getElementById('list'),
    ref = document.getElementById('python'),
    haskell = document.createElement('p');   //创建一个新的p标签
haskell.id = 'haskell';
haskell.innerText = 'Haskell';
list.insertBefore(haskell, ref);

新的HTML结构如下:

<!-- HTML结构 -->
<div id="list">
    <p id="java">Java</p>
    <p id="haskell">Haskell</p>
    <p id="python">Python</p>
    <p id="scheme">Scheme</p>
</div>

可见,使用insertBefore重点是要拿到一个“参考子节点”的引用。很多时候,需要循环一个父节点的所有子节点,可以通过迭代children属性实现:

var
    i, c,
    list = document.getElementById('list');
for (i = 0; i < list.children.length; i++) {
    c = list.children[i]; // 拿到第i个子节点
}
4、删除DOM

删除一个DOM节点就比插入要容易得多。

要删除一个节点,

  • 首先要获得该节点本身以及它的父节点
  • 然后,调用父节点的removeChild把自己删掉:
// 拿到待删除节点:
var self = document.getElementById('to-be-removed');
// 拿到父节点:
var parent = self.parentElement;
// 删除:
var removed = parent.removeChild(self);
removed === self; // true

注意 到删除后的节点虽然不在文档树中了,但其实它还在内存中,可以随时再次被添加到别的位置。


当你遍历一个父节点的子节点并进行删除操作时,要注意,children属性是一个只读属性,并且它在子节点变化时会实时更新。

例如,对于如下HTML结构:

<div id="parent">
    <p>First</p>
    <p>Second</p>
</div>

当我们用如下代码删除子节点时:

var parent = document.getElementById('parent');
parent.removeChild(parent.children[0]);
parent.removeChild(parent.children[1]); // <-- 浏览器报错

浏览器报错:parent.children[1]不是一个有效的节点。

原因就在于,当<p>First</p>节点被删除后,parent.children的节点数量已经从2变为了1,索引[1]已经不存在了。

因此,删除多个节点时,要注意children属性时刻都在变化。


6.4、事件监听机制

HTML DOM 事件

JavaScript HTML DOM 事件监听程序 (w3school.com.cn)

1、概念

某些组件被执行了某些操作后,触发某些代码的执行。

  • 事件:某些操作。如: 单击,双击,键盘按下了,鼠标移动了
  • 事件源:组件。如: 按钮 文本输入框...
  • 监听器:代码。
  • 注册监听:将事件,事件源,监听器结合在一起。 当事件源上发生了某个事件,则触发执行某个监听器代码。
2、常见的事件
1、MouseEvent
事件 描述
onclick 当用户单击元素时发生此事件。
oncontextmenu 当用户右键单击某个元素以打开上下文菜单时,发生此事件。
ondblclick 当用户双击元素时发生此事件。
onmousedown 当用户在元素上按下鼠标按钮时,发生此事件。
onmouseenter 当指针移动到元素上时,发生此事件。
onmouseleave 当指针从元素上移出时,发生此事件。
onmousemove 当指针在元素上方移动时,发生此事件。
onmouseout 当用户将鼠标指针移出元素或其中的子元素时,发生此事件。
onmouseover 当指针移动到元素或其中的子元素上时,发生此事件。
onmouseup 当用户在元素上释放鼠标按钮时,发生此事件。

当事件为onmousedown时,可以监听用户按下了鼠标哪个键。

event.button返回值,数字值,表示鼠标事件发生时按下的鼠标按钮。

<body>
<input type="button" id="button">
<script>
        document.getElementById("button").onmousedown = function (event) {
            alert(event.button);
        }
</script>
<!--这里也可以放script标签-->
</body>

可能的值:

  • 0:鼠标左键
  • 1:滚轮按钮或中间按钮(如果有)
  • 2:鼠标右键

注释:Internet Explorer 8 及更早版本有不同的返回值:

  • 1:鼠标左键
  • 2:鼠标右键
  • 4:滚轮按钮或中间按钮(如果有)

注释:对于左手配置的鼠标,返回值是相反的。


2、FocusEvent
事件 描述
onblur 当元素失去焦点时发生此事件。
onfocus 当元素获得焦点时发生此事件。
onfocusin 当元素即将获得焦点时发生此事件。
onfocusout 当元素即将失去焦点时发生此事件。

3、加载事件
事件 描述
onload 一张页面或一幅图像完成加载。

定义和用法

  • onload 事件在对象被加载后发生。
  • onload 最常用于 <body> 元素中,用于在网页完全加载所有内容(包括图像、脚本文件、CSS 文件等)后执行脚本。
  • onload 事件可用于检查访问者的浏览器类型和浏览器版本,并根据这些信息加载网页的正确版本。
  • onload 事件也可用于处理 cookie .
<body onload="checkCookies()">

4、KeyboardEvent
事件 描述
onkeydown 当用户正在按下键时,发生此事件。
onkeypress 当用户按了某个键时,发生此事件。
onkeyup 当用户松开键时,发生此事件。

当事件为onkeydown时,可以监听用户按下了鼠标哪个键。

KeyboardEvent key 属性 (w3school.com.cn)

<body>
<input id="button">
<script>
        document.getElementById("button").onkeydown = function (event) {
            alert(event.key);
        }
</script>
<!--这里也可以放script标签-->
</body>

定义和用法

key 属性返回发生按键事件时按下的键的标识符。

按键标识符是标识键盘按钮的字符串。此属性的返回值可以是以下字符串:

  • 单个字符(比如 "a", "W", "4", "+" 或 "$")
  • 多字符(比如 "F1", "Enter", "HOME" 或 "CAPS LOCK")

注释:该属性只读。

提示:如果您想知道在发生按键事件时是否按下了 "ALT"、"CTRL"、"META" 或 "SHIFT" 键,请使用 altKeyctrlKeymetaKeyshiftKey 属性,返回boolean值


5、ClipboardEvent
事件 描述
oncopy 当用户复制元素的内容时发生此事件。
oncut 当用户剪切元素的内容时发生此事件。
onpaste 当用户将内容粘贴到元素中时,发生此事件。

6、选择和改变

7、表单事件
  • onsubmit:确认按钮被点击。

    如果返回值为false,则表单会被阻止提交

  • onreset:重置按钮被点击。


7、操作表单(重要)

用JavaScript操作表单和操作DOM是类似的,因为表单本身也是DOM树。

不过表单的输入框、下拉框等可以接收用户输入,所以用JavaScript来操作表单,可以获得用户输入的内容,或者对一个输入框设置新的内容。

HTML表单的输入控件主要有以下几种:

  • 文本框,对应的<input type="text">,用于输入文本;
  • 口令框,对应的<input type="password">,用于输入口令;
  • 单选框,对应的<input type="radio">,用于选择一项;
  • 复选框,对应的<input type="checkbox">,用于选择多项;
  • 下拉框,对应的<select>,用于选择一项;
  • 隐藏文本,对应的<input type="hidden">,用户不可见,但表单提交时会把隐藏文本发送到服务器。

7.1、获取值

如果我们获得了一个<input>节点的引用,就可以直接调用value获得对应的用户输入值:

// <input type="text" id="email">
var input = document.getElementById('email');
input.value; // '用户输入的值'

这种方式可以应用于textpasswordhidden以及select

但是,对于单选框和复选框value属性返回的永远是HTML预设的值,

而我们需要获得的实际是用户是否“勾上了”选项,所以应该用checked判断:

// <label><input type="radio" name="weekday" id="monday" value="1"> Monday</label>   预设值为1
// <label><input type="radio" name="weekday" id="tuesday" value="2"> Tuesday</label> 预设值为2
var mon = document.getElementById('monday');
var tue = document.getElementById('tuesday');
mon.value; // '1'
tue.value; // '2'
mon.checked; // true或者false
tue.checked; // true或者false

7.2、设置值

设置值和获取值类似,对于textpasswordhidden以及select,直接设置value就可以:

// <input type="text" id="email">
var input = document.getElementById('email');
input.value = 'test@example.com'; // 文本框的内容已更新

对于单选框和复选框,设置checkedtruefalse即可。


7.3、HTML5控件

HTML5新增了大量标准控件,常用的包括datedatetimedatetime-localcolor等,它们都使用<input>标签:

<input type="date" value="2021-12-02">
<input type="datetime-local" value="2021-12-02T20:21:12">
<input type="color" value="#ff0000">

不支持HTML5的浏览器无法识别新的控件,会把它们当做type="text"来显示。

支持HTML5的浏览器将获得格式化的字符串。

例如,type="date"类型的inputvalue将保证是一个有效的YYYY-MM-DD格式的日期,或者空字符串。


7.4、提交表单

JavaScript可以以两种方式来处理表单的提交(AJAX方式在后面章节介绍)。

方式一:

通过<form>元素的submit()方法提交一个表单,例如,响应一个<button>click事件,在JavaScript代码中提交表单:

<!-- HTML -->
<form id="test-form" action="#" method="post">     //#代表提交到本页面
    <input type="text" name="test">

    <button type="button" onclick="doSubmitForm()">Submit</button>
</form>

<script>
    function doSubmitForm() {
        let form = document.getElementById('test-form');
        // 可以在此修改form的input...
        let first = form.getElementsByTagName('input');
        first[0].value = '自动提交';
        //alert('1')   测试是否成功绑定事件
        // 提交form:
        form.submit();    //需要绑定一个click事件
    }
</script>

这种方式的缺点是扰乱了浏览器对form的正常提交。浏览器默认点击<button type="submit">时提交表单,或者用户在最后一个输入框按回车键。


方式二:

因此,第二种方式是响应<form>本身的onsubmit事件,在提交form时作修改:

<!-- HTML -->
<form id="test-form" onsubmit="return checkForm()">
    <input type="text" name="test">
    <button type="submit">Submit</button>
</form>

<script>
function checkForm() {
    var form = document.getElementById('test-form');
    // 可以在此修改form的input...
    let first = form.getElementsByTagName('input');
    first[0].value = '自动提交';
    // 继续下一步:
    return true;     //需要返回值
}
</script>

注意要return true来告诉浏览器继续提交,如果return false,浏览器将不会继续提交form,这种情况通常对应用户输入有误,提示用户错误信息后终止提交form。

在检查和修改<input>时,要充分利用<input type="hidden">来传递数据。

例如,很多登录表单希望用户输入用户名和口令,但是,安全考虑,提交表单时不传输明文口令,而是口令的MD5。普通JavaScript开发人员会直接修改<input>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>

    <!--导入MD5工具类-->
	<script src="js/md5.js"></script>
    <!-- 几种加密方式:
    1)  hex_md5(value)     //16进制MD5加密
    2)  b64_md5(value)
    3)  str_md5(value)
    4)  hex_hmac_md5(key, data)
    5)  b64_hmac_md5(key, data)
    6)  str_hmac_md5(key, data)
    -->

</head>
<body>
<form id="login-form" method="post" onsubmit="return checkForm()" action="#">
    <p>
        <span>账号: </span> <input type="text" id="username" name="username">
    </p>
    <p>
        <span>密码:</span> <input type="password" id="password" name="password">
    </p>
    <button type="submit">Submit</button>
</form>

<script>
    function checkForm() {
        var pwd = document.getElementById('password');
        // 把用户输入的明文变为MD5: 需要导入MD5工具类
        pwd.value = hex_md5(pwd.value);
        // 继续下一步:
        return true;
    }
</script>
</body>
</html>

这个做法看上去没啥问题,但用户输入了口令提交时,口令框的显示会突然从几个*变成32个*(因为MD5有32个字符)。

要想不改变用户的输入,可以利用<input type="hidden">实现:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>

    <!--导入MD5工具类-->
	<script src="js/md5.js"></script>
    <!-- 几种加密方式:
    1)  hex_md5(value)		//16进制MD5加密
    2)  b64_md5(value)
    3)  str_md5(value)
    4)  hex_hmac_md5(key, data)
    5)  b64_hmac_md5(key, data)
    6)  str_hmac_md5(key, data)
    -->

</head>
<body>
<form id="login-form" method="post" onsubmit="return checkForm()" action="#">
    <p>
        <span>账号: </span> <input type="text" id="username" name="username">
    </p>
    <p>
        <span>密码:</span> <input type="password" id="input_password">
    </p>
    <input type="hidden" id="md5_password" name="password">
    <button type="submit">Submit</button>
</form>

<script>
    function checkForm() {
        let input_pwd = document.getElementById('input_password');
        let md5_pwd = document.getElementById('md5_password');
        // 把用户输入的明文变为MD5: 需要导入MD5工具类
        md5_pwd.value = hex_md5(input_pwd.value);  //16进制MD5加密
        // 继续下一步:
        return true;
    }
</script>
</body>
</html>

注意到idmd5-password<input>标记了name="password",而用户输入的idinput-password<input>没有name属性。没有name属性的<input>的数据不会被提交。


5、JQuery基础

jQuery API 中文文档

5.1、简介

jQuery是JavaScript世界中使用最广泛的一个库。全世界大约有80~90%的网站直接或间接地使用了jQuery。

jQuery这么流行,肯定是因为它解决了一些很重要的问题。实际上,jQuery能帮我们干这些事情:

  • 消除浏览器差异:你不需要自己写冗长的代码来针对不同的浏览器来绑定事件,编写AJAX等代码;
  • 简洁的操作DOM的方法:写$('#test')肯定比document.getElementById('test')来得简洁;
  • 轻松实现动画、修改CSS等各种操作。

**jQuery的理念 ** “Write Less, Do More“,让你写更少的代码,完成更多的工作!


jQuery版本

目前jQuery有三个大版本:
1.x:兼容ie678,使用最为广泛的,官方只做BUG维护,
功能不再新增。因此一般项目来说,使用1.x版本就可以了,
最终版本:1.12.4 (2016年5月20日)
2.x:不兼容ie678,很少有人使用,官方只做BUG维护,
功能不再新增。如果不考虑兼容低版本的浏览器可以使用2.x,
最终版本:2.2.4 (2016年5月20日)
3.x:不兼容ie678,只支持最新的浏览器。除非特殊要求,
一般不会使用3.x版本的,很多老的jQuery插件不支持这个版本。
目前该版本是官方主要更新维护的版本。最新版本:3.6.0(2022年2月15日)

jQuery官网可以下载最新版本。jQuery只是一个jquery-xxx.js文件,但你会看到有compressed(已压缩)和uncompressed(未压缩)两种版本,使用时完全一样但如果你想深入研究jQuery源码,那就用uncompressed版本。


使用jQuery

使用jQuery只需要在页面的<head>引入jQuery文件即可

<html>
<head>
    <script src="//code.jquery.com/jquery-1.11.3.min.js"></script>
	...
</head>
<body>
    ...
</body>
</html>

js对象和jquery对象的区别

  • JS对象,是一个名值对的无序集合。

  • jquery对象,是jquery特有的对象,只有调用jquery框架才存在。其实jquery对象,也是一种js对象。

  • jquery对象和js对象可以相互转换,例如 $("#div").get(),即可以把一个jquery对象转换为js对象。

  • 两者相互转换

    • jq -- > js : jq对象[索引] 或者 jq对象.get(索引)
    • js -- > jq : $(js对象)

最主要的区别:

  • js对象上的方法,不能直接用在jquery对象上,如果一定要给jquery对象使用js对象的方法,必须把jquery对象转换为js对象。
  • jquery对象,则可以随意使用jquery定义的方法。

$符号

基本用法: $(选择器).事件()

$是著名的jQuery符号。实际上,jQuery把所有功能全部封装在一个全局变量jQuery中,而$也是一个合法的变量名,它是变量jQuery的别名:

window.jQuery; // jQuery(selector, context)
window.$; // jQuery(selector, context)
$ === jQuery; // true
typeof($); // 'function'

$本质上就是一个函数,但是函数也是对象,于是$除了可以直接调用外,也可以有很多其他属性。

注意,你看到的$函数名可能不是jQuery(selector, context),因为很多JavaScript压缩工具可以对函数名和参数改名,所以压缩过的jQuery源码$函数可能变成a(b, c)

绝大多数时候,我们都直接用$(因为写起来更简单嘛)。但是,如果$这个变量不幸地被占用了,而且还不能改,那我们就只能让jQuery$变量交出来,然后就只能使用jQuery这个变量:

$; // jQuery(selector, context)
jQuery.noConflict();
$; // undefined
jQuery; // jQuery(selector, context)

这种黑魔法的原理是jQuery在占用$之前,先在内部保存了原来的$,调用jQuery.noConflict()时会把原来保存的变量还原。


5.2、选择器(与CSS语法一样)

选择器是jQuery的核心。

一个选择器写出来类似$('#dom-id')

为什么jQuery要发明选择器?回顾一下DOM操作中我们经常使用的代码:

// 按ID查找:
var a = document.getElementById('dom-id');

// 按tag查找:
var divs = document.getElementsByTagName('div');

// 查找<p class="red">:
var ps = document.getElementsByTagName('p');
// 过滤出class="red":
// TODO:

// 查找<table class="green">里面的所有<tr>:
var table = ...
for (var i=0; i<table.children; i++) {
    // TODO: 过滤出<tr>
}

这些代码实在太繁琐了,并且,在层级关系中,例如,查找<table class="green">里面的所有<tr>,一层循环实际上是错的,因为<table>的标准写法是:

<table>
    <tbody>
        <tr>...</tr>
        <tr>...</tr>
    </tbody>
</table>

很多时候,需要递归查找所有子节点。

jQuery的选择器就是帮助我们快速定位到一个或多个DOM节点。

示例:

<p>
    <a href="" id="test-jQuery">点我</a>     //点击后出现hello,jquery的弹窗
</p>
<script>
    $('#test-jQuery').click(function () {     //按ID查找
        alert('hello,jquery')
    })
</script>

1、基本选择器

1、按ID查找

如果某个DOM节点有id属性,利用jQuery查找如下:

// 查找<div id="abc">:
var div = $('#abc');

注意#abc#开头。返回的对象是jQuery对象

什么是jQuery对象?jQuery对象类似数组,它的每个元素都是一个引用了DOM节点的对象。

以上面的查找为例,如果idabc<div>存在,返回的jQuery对象如下:

[<div id="abc">...</div>]

如果idabc<div>不存在,返回的jQuery对象如下:

[]

总之jQuery的选择器不会返回undefined或者null,这样的好处是你不必在下一行判断if (div === undefined)

jQuery对象和DOM对象之间可以互相转化:

var div = $('#abc'); // jQuery对象
var divDom = div.get(0); // 假设存在div,获取第1个DOM元素
var another = $(divDom); // 重新把DOM包装为jQuery对象

通常情况下你不需要获取DOM对象,直接使用jQuery对象更加方便

如果你拿到了一个DOM对象,那可以简单地调用$(aDomObject)把它变成jQuery对象,这样就可以方便地使用jQuery的API了。


2、按tag查找

按tag查找只需要写上tag名称就可以了:

var ps = $('p'); // 返回所有<p>节点
ps.length; // 数一数页面有多少个<p>节点

3、按class查找

按class查找注意在class名称前加一个.

var a = $('.red'); // 所有节点包含`class="red"`都将返回
// 例如:
// <div class="red">...</div>
// <p class="green red">...</p>

通常很多节点有多个class,我们可以查找同时包含redgreen的节点:

var a = $('.red.green'); // 注意没有空格!
// 符合条件的节点:
// <div class="red green">...</div>
// <div class="blue green red">...</div>

4、按属性查找

一个DOM节点除了idclass外还可以有很多属性,很多时候按属性查找会非常方便,比如在一个表单中按属性来查找:

var email = $('[name=email]'); // 找出<??? name="email">
var passwordInput = $('[type=password]'); // 找出<??? type="password">
var a = $('[items="A B"]'); // 找出<??? items="A B">
var email = $('[name!=email]'); // 找出name值不等于email的所有标签

当属性的值包含空格等特殊字符时,需要用双引号括起来。

正则匹配:

按属性查找还可以使用前缀查找或者后缀查找:

var icons = $('[name^=icon]'); // 找出所有name属性值以icon开头的DOM
// 例如: name="icon-1", name="icon-2"
var names = $('[name$=with]'); // 找出所有name属性值以with结尾的DOM
// 例如: name="startswith", name="endswith"

这个方法尤其适合通过class属性查找,且不受class包含多个名称的影响:

var icons = $('[class^="icon-"]'); // 找出所有class包含至少一个以`icon-`开头的DOM
// 例如: class="icon-clock", class="abc icon-home"

5、组合查找

组合查找就是把上述简单选择器组合起来使用。如果我们查找$('[name=email]'),很可能把表单外的<div name="email">也找出来,但我们只希望查找<input>,就可以这么写:

var emailInput = $('input[name=email]'); // 不会找出<div name="email">

同样的,根据tag和class来组合查找也很常见:

var tr = $('tr.red'); // 找出<tr class="red ...">...</tr>

6、多项选择器

多项选择器就是把多个选择器用,组合起来一块选:

$('p,div'); // 把<p>和<div>都选出来
$('p.red,p.green'); // 把<p class="red">和<p class="green">都选出来

要注意的是,选出来的元素是按照它们在HTML中出现的顺序排列的,而且不会有重复元素。例如,<p class="red green">不会被上面的$('p.red,p.green')选择两次。


2、高级选择器

1、后代选择器 $('ancestor descendant')

(Descendant Selector) 可以在所有后代中选择

如果两个DOM元素具有层级关系,就可以用$('ancestor descendant')来选择,层级之间用空格隔开。例如:

<!-- HTML结构 -->
<div class="testing">
    <ul class="lang">
        <li class="lang-javascript">JavaScript</li>
        <li class="lang-python">Python</li>
        <li class="lang-lua">Lua</li>
    </ul>
</div>

要选出JavaScript,可以用层级选择器:

$('ul.lang li.lang-javascript'); // [<li class="lang-javascript">JavaScript</li>]
$('div.testing li.lang-javascript'); // [<li class="lang-javascript">JavaScript</li>]

因为<div><ul>都是<li>的祖先节点,所以上面两种方式都可以选出相应的<li>节点。

要选择所有的<li>节点,用:

$('ul.lang li');

这种层级选择器相比单个的选择器好处在于,它缩小了选择范围,因为首先要定位父节点,才能选择相应的子节点,这样避免了页面其他不相关的元素。

例如:

$('form[name=upload] input');

就把选择范围限定在name属性为upload的表单里。如果页面有很多表单,其他表单的<input>不会被选择。

多层选择也是允许的:

$('form.test p input'); // 在form表单选择被<p>包含的<input>

2、子代选择器 $('parent>child')

(Child Selector) 只能选择子代

子选择器$('parent>child')类似层级选择器,但是限定了层级关系必须是父子关系,就是<child>节点必须是<parent>节点的直属子节点。

<!-- HTML结构 -->
<div class="testing">
    <ul class="lang">
        <li class="lang-javascript">JavaScript</li>
        <li class="lang-python">Python</li>
        <li class="lang-lua">Lua</li>
    </ul>
</div>
$('ul.lang>li.lang-javascript'); // 可以选出[<li class="lang-javascript">JavaScript</li>]
$('div.testing>li.lang-javascript'); // [], 无法选出,因为<div>和<li>不构成父子关系

3、过滤器 带冒号过滤

(Filter)类似CSS结构伪类选择器

  1. 首元素选择器
    • 语法: :first 获得选择的元素中的第一个元素
  2. 尾元素选择器
    • 语法: :last 获得选择的元素中的最后一个元素
  3. 非元素选择器
    • 语法: :not(selector) 不包括指定内容的元素
  4. 偶数选择器
    • 语法: :even 偶数,从 0 开始计数
  5. 奇数选择器
    • 语法: :odd 奇数,从 0 开始计数
  6. 等于索引选择器
    • 语法: :eq(index) 指定索引元素
  7. 大于索引选择器
    • 语法: :gt(index) 大于指定索引元素
  8. 小于索引选择器
    • 语法: :lt(index) 小于指定索引元素
  9. 标题选择器
    • 语法: :header 获得全部标题(h1~h6)元素,固定写法,冒号前面不需要写东西

过滤器一般不单独使用,它通常附加在选择器上,帮助我们更精确地定位元素。观察过滤器的效果:

$('ul.lang li'); // 选出JavaScript、Python和Lua 3个节点

$('ul.lang li:first-child'); // 仅选出JavaScript
$('ul.lang li:last-child'); // 仅选出Lua
$('ul.lang li:nth-child(2)'); // 选出第N个元素,N从1开始
$('ul.lang li:nth-child(even)'); // 选出序号为偶数的元素
$('ul.lang li:nth-child(odd)'); // 选出序号为奇数的元素

4、表单相关

针对表单元素,jQuery还有一组特殊的选择器:

  • :input:可以选择<input><textarea><select><button>
  • :file:可以选择<input type="file">,和input[type=file]一样;
  • :checkbox:可以选择复选框,和input[type=checkbox]一样;
  • :radio:可以选择单选框,和input[type=radio]一样;
  • :focus:可以选择当前输入焦点的元素,例如把光标放到一个<input>上,用$('input:focus')就可以选出;
  • :checked:选择当前勾上的单选框和复选框,用这个选择器可以立刻获得用户选择的项目,如$('input[type=radio]:checked')
  • :enabled:可以选择可以正常输入的<input><select> 等,也就是没有灰掉的输入;
  • :disabled:和:enabled正好相反,选择那些不能输入的。

此外,jQuery还有很多有用的选择器,例如,选出可见的或隐藏的元素:

$('div:visible'); // 所有可见的div
$('div:hidden'); // 所有隐藏的div

5.3、查找和过滤

1、find()

在子节点中查找

通常情况下选择器可以直接定位到我们想要的元素,但是,当我们拿到一个jQuery对象后,还可以以这个对象为基准,进行查找和过滤。

最常见的查找是在某个节点的所有子节点中查找,使用find()方法,它本身又接收一个任意的选择器。例如如下的HTML结构:

  • JavaScript
  • Python
  • Swift
  • Scheme
  • Haskell
<!-- HTML结构 -->
<ul class="lang">
    <li class="js dy">JavaScript</li>
    <li class="dy">Python</li>
    <li id="swift">Swift</li>
    <li class="dy">Scheme</li>
    <li name="haskell">Haskell</li>
</ul>

find()查找:

var ul = $('ul.lang'); // 获得<ul>
var dy = ul.find('.dy'); // 获得JavaScript, Python, Scheme
var swf = ul.find('#swift'); // 获得Swift
var hsk = ul.find('[name=haskell]'); // 获得Haskell

如果要从当前节点开始向上查找,使用parent()方法:

var swf = $('#swift'); // 获得Swift
var parent = swf.parent(); // 获得Swift的上层节点<ul>
var a = swf.parent('.red'); // 获得Swift的上层节点<ul>,同时传入过滤条件。如果ul不符合条件,返回空jQuery对象

对于位于同一层级的节点,可以通过next()prev()方法,例如:

当我们已经拿到Swift节点后:

var swift = $('#swift');

swift.next(); // Scheme
swift.next('[name=haskell]'); // 空的jQuery对象,因为Swift的下一个元素Scheme不符合条件[name=haskell]

swift.prev(); // Python
swift.prev('.dy'); // Python,因为Python同时符合过滤器条件.dy

2、filter()

在接收的Array中过滤

和函数式编程的map、filter类似,jQuery对象也有类似的方法。

<!-- HTML结构 -->
<ul class="lang">
    <li class="js dy">JavaScript</li>
    <li class="dy">Python</li>
    <li id="swift">Swift</li>
    <li class="dy">Scheme</li>
    <li name="haskell">Haskell</li>
</ul>

filter()方法可以过滤掉不符合选择器条件的节点:

var langs = $('ul.lang li'); // 拿到JavaScript, Python, Swift, Scheme和Haskell
var a = langs.filter('.dy'); // 拿到JavaScript, Python, Scheme

或者传入一个函数,要特别注意函数内部的this被绑定为DOM对象,不是jQuery对象

var langs = $('ul.lang li'); // 拿到JavaScript, Python, Swift, Scheme和Haskell
langs.filter(function () {
    return this.innerHTML.indexOf('S') === 0; // 返回S开头的节点
}); // 拿到Swift, Scheme

map()方法把一个jQuery对象包含的若干DOM节点转化为其他对象:

var langs = $('ul.lang li'); // 拿到JavaScript, Python, Swift, Scheme和Haskell
var arr = langs.map(function () {
    return this.innerHTML;
}).get(); // 用get()拿到包含string的Array:['JavaScript', 'Python', 'Swift', 'Scheme', 'Haskell']

此外,一个jQuery对象如果包含了不止一个DOM节点,first()last()slice()方法可以返回一个新的jQuery对象,把不需要的DOM节点去掉:

var langs = $('ul.lang li'); // 拿到JavaScript, Python, Swift, Scheme和Haskell
var js = langs.first(); // JavaScript,相当于$('ul.lang li:first-child')
var haskell = langs.last(); // Haskell, 相当于$('ul.lang li:last-child')
var sub = langs.slice(2, 4); // Swift, Scheme, 参数和数组的slice()方法一致

5.4、事件

因为JavaScript在浏览器中以单线程模式运行,页面加载后,一旦页面上所有的JavaScript代码被执行完后,就只能依赖触发事件来执行JavaScript代码。

浏览器在接收到用户的鼠标或键盘输入后,会自动在对应的DOM节点上触发相应的事件。如果该节点已经绑定了对应的JavaScript处理函数,该函数就会自动调用。

由于不同的浏览器绑定事件的代码都不太一样,所以用jQuery来写代码,就屏蔽了不同浏览器的差异,我们总是编写相同的代码。

举个例子,假设要在用户点击了超链接时弹出提示框,我们用jQuery这样绑定一个click事件:

/* HTML:
 *
 * <a id="test-link" href="#0">点我试试</a>
 *
 */

// 获取超链接的jQuery对象:
var a = $('#test-link');
a.on('click', function () {     //on方法用来绑定一个事件,我们需要传入事件类型和对应的处理函数。
    alert('Hello!');
});

另一种更简化的写法是直接调用click()方法:

a.click(function () {
    alert('Hello!');
});

两者完全等价。我们通常用后面的写法。


1、鼠标事件

  • click: 鼠标单击时触发;
  • dblclick:鼠标双击时触发;
  • mouseenter:鼠标进入时触发;
  • mouseleave:鼠标移出时触发;
  • mousemove:鼠标在DOM内部移动时触发;
  • hover:鼠标进入和退出时触发两个函数,相当于mouseenter加上mouseleave。

2、键盘事件

键盘事件仅作用在当前焦点的DOM上,通常是<input><textarea>

  • keydown:键盘按下时触发;
  • keyup:键盘松开时触发;
  • keypress:按一次键后触发。

3、其他事件

  • focus:当DOM获得焦点时触发;
  • blur:当DOM失去焦点时触发;
  • change:当<input><select><textarea>的内容改变时触发;
  • submit:当<form>提交时触发;
  • ready:当页面被载入并且DOM树完成初始化后触发。

其中,ready仅作用于document对象。由于ready事件在DOM完成初始化后触发,且只触发一次,所以非常适合用来写其他的初始化代码。假设我们想给一个<form>表单绑定submit事件,下面的代码没有预期的效果:

<html>
<head>
    <script>
        // 代码有误:
        $('#testForm').on('submit', function () {
            alert('submit!');
        });
    </script>
</head>
<body>
    <form id="testForm">
        ...
    </form>
</body>

因为JavaScript在此执行的时候,<form>尚未载入浏览器,所以$('#testForm)返回[],并没有绑定事件到任何DOM上。

所以我们自己的初始化代码必须放到document对象的ready事件中,保证DOM已完成初始化:

<html>
<head>
    <script>
        $(document).on('ready', function () {
            $('#testForm).on('submit', function () {
                alert('submit!');
            });
        });
    </script>
</head>
<body>
    <form id="testForm">
        ...
    </form>
</body>

这样写就没有问题了。因为相关代码会在DOM树初始化后再执行。

由于ready事件使用非常普遍,所以可以这样简化:

$(document).ready(function () {
    // on('submit', function)也可以简化:
    $('#testForm).submit(function () {
        alert('submit!');
    });
});

甚至还可以再简化为:

<!--入口函数-->
$(function () {
    // init...     init是初始化函数
});

上面的这种写法最为常见。如果你遇到$(function () {...})的形式,牢记这是document对象的ready事件处理函数。

  • $(function () {...})可以反复绑定事件处理函数,它们会依次执行
  • 但是window.onload只能定义一次,如果定义多次,后边的会将前边的覆盖
$(function () {
    console.log('init A...');
});
$(function () {
    console.log('init B...');
});
$(function () {
    console.log('init C...');
});

4、事件参数

有些事件,如mousemovekeypress,我们需要获取鼠标位置和按键的值,否则监听这些事件就没什么意义了。所有事件都会传入Event对象作为参数,可以从Event对象上获取到更多的信息:

<p>
        mousemove:<span id="testMouseMoveSpan"></span>
    </p>

    <div id="testMouseMoveDiv" style="display: block; width: 300px; height: 120px; border: 1px solid #ccc;">
        在此区域移动鼠标试试
    </div>

    <script>
        $(function () {
            $('#testMouseMoveDiv').mousemove(function (e) {
                $('#testMouseMoveSpan').text('pageX = ' + e.pageX + ', pageY = ' + e.pageY);
            });
        });
    </script>

5、取消绑定

一个已被绑定的事件可以解除绑定,通过off('click', function)实现:

function hello() {
    alert('hello!');
}

a.click(hello); // 绑定事件

// 10秒钟后解除绑定:
setTimeout(function () {
    a.off('click', hello);
}, 10000);

需要特别注意的是,下面这种写法是无效的:

// 绑定事件:
a.click(function () {      //匿名函数1
    alert('hello!');
});

// 解除绑定:
a.off('click', function () {    ///匿名函数2
    alert('hello!');
});

这是因为两个匿名函数虽然长得一模一样,但是它们是两个不同的函数对象,off('click', function () {...})无法移除已绑定的第一个匿名函数。

为了实现移除效果,可以使用off('click')一次性移除已绑定的click事件的所有处理函数。

同理,无参数调用off()一次性移除已绑定的所有类型的事件处理函数。


6、事件触发条件

一个需要注意的问题是,事件的触发总是由用户操作引发的。例如,我们监控文本框的内容改动:

var input = $('#test-input');
input.change(function () {
    console.log('changed...');
});

当用户在文本框中输入时,就会触发change事件。但是,如果用JavaScript代码去改动文本框的值,将不会触发change事件:

var input = $('#test-input');
input.val('change it!'); // 无法触发change事件

有些时候,我们希望用代码触发change事件,可以直接调用无参数的change()方法来触发该事件:

var input = $('#test-input');
input.val('change it!');
input.change(); // 触发change事件

input.change()相当于input.trigger('change'),它是trigger()方法的简写。


7、浏览器安全限制

在浏览器中,有些JavaScript代码只有在用户触发下才能执行,自动弹窗将被拦截,例如,window.open()函数:

// 无法弹出新窗口,将被浏览器屏蔽:
$(function () {
    window.open('/');
});

这些“敏感代码”只能由用户操作来触发:

var button1 = $('#testPopupButton1');
var button2 = $('#testPopupButton2');

function popupTestWindow() {
    window.open('/');
}

button1.click(function () {
    popupTestWindow();
});

button2.click(function () {
    // 不立刻执行popupTestWindow(),3秒后执行:
    setTimeout(popupTestWindow, 3000);
});

当用户点击button1时,click事件被触发,由于popupTestWindow()click事件处理函数内执行,这是浏览器允许的,而button2click事件并未立刻执行popupTestWindow(),延迟执行的popupTestWindow()将被浏览器拦截 (实测Edge可以延迟弹窗,可能是由于也是绑定的click事件,是经过用户点击允许的)。


5.5、操作DOM

jQuery的选择器很强大,用起来又简单又灵活,但是搞了这么久,我拿到了jQuery对象,到底要干什么?

答案当然是操作对应的DOM节点啦!

回顾一下修改DOM的CSS、文本、设置HTML有多么麻烦,而且有的浏览器只有innerHTML,有的浏览器支持innerText,有了jQuery对象,不需要考虑浏览器差异了,全部统一操作!


  • html(): 获取/设置元素的a标签体html内容 <a><font>内容</font></a> 得到--> <font>内容</font>
  • text(): 获取/设置元素的a标签体纯文本内容 <a><font>内容</font></a> 得到--> 内容
  • val(): 获取/设置元素的value属性值

1、修改Text(显示文本)和HTML(源码)

jQuery对象的text()html()方法分别获取节点的文本和原始HTML文本,例如,如下的HTML结构:

<!-- HTML结构 -->
<ul id="test-ul">
    <li class="js">JavaScript</li>
    <li name="book">Java &amp; JavaScript</li>
</ul>

分别获取文本和HTML:

$('#test-ul li[name=book]').text(); // 'Java & JavaScript'
$('#test-ul li[name=book]').html(); // 'Java &amp; JavaScript'

如何设置文本或HTML?

jQuery的API设计非常巧妙:无参数调用text()是获取文本,传入参数就变成设置文本,HTML也是类似操作

另外:

一个jQuery对象可以包含0个或任意个DOM对象,它的方法实际上会作用在对应的每个DOM节点上。在上面的例子中试试:

$('#test-ul li').text('JS'); // 两个节点都变成了JS

所以jQuery对象的另一个好处是我们可以执行一个操作,作用在对应的一组DOM节点上。即使选择器没有返回任何DOM节点,调用jQuery对象的方法仍然不会报错:

// 如果不存在id为not-exist的节点:
$('#not-exist').text('Hello'); // 代码不报错,没有节点被设置为'Hello'

这意味着jQuery帮你免去了许多if语句。


2、修改CSS

jQuery对象有“批量操作”的特点,这用于修改CSS实在是太方便了。考虑下面的HTML结构:

<!-- HTML结构 -->
<ul id="test-css">
    <li class="lang dy"><span>JavaScript</span></li>      //文字和背景颜色会改变
    <li class="lang"><span>Java</span></li>
    <li class="lang dy"><span>Python</span></li>		  //文字和背景颜色会改变
    <li class="lang"><span>Swift</span></li>
    <li class="lang dy"><span>Scheme</span></li>		  //文字和背景颜色会改变
</ul>

要高亮显示动态语言,调用jQuery对象的css('name', 'value')方法,我们用一行语句实现:

$('#test-css li.dy>span').css('background-color', '#ffd351').css('color', 'red'); //子代选择器

注意,jQuery对象的所有方法都返回一个jQuery对象(可能是新的也可能是自身),这样我们可以进行链式调用,非常方便。

jQuery对象的css()方法可以这么用:不传入值就是获取,传入值就是设置,同text()和html()

var div = $('#test-div');
div.css('color'); // '#000033', 获取CSS属性   
div.css('color', '#336699'); // 设置CSS属性
div.css('color', ''); // 清除CSS属性

为了和JavaScript保持一致,CSS属性可以用'background-color''backgroundColor'两种格式。

css()方法将作用于DOM节点的style属性,具有最高优先级。如果要修改class属性,可以用jQuery提供的下列方法:

var div = $('#test-div');
div.hasClass('highlight'); // false, class是否包含highlight
div.addClass('highlight'); // 添加highlight这个class
div.removeClass('highlight'); // 删除highlight这个class

3、显示和隐藏DOM

要隐藏一个DOM,我们可以设置CSS的display属性为none,利用css()方法就可以实现。不过,要显示这个DOM就需要恢复原有的display属性,这就得先记下来原有的display属性到底是block还是inline还是别的值。

考虑到显示和隐藏DOM元素使用非常普遍,jQuery直接提供show()hide()方法,我们不用关心它是如何修改display属性的,总之它能正常工作:

var a = $('a[target=_blank]');
a.hide(); // 隐藏   a.css('display',none)
a.show(); // 显示   a.css('display',block/inline)  需要记住原先的属性值

注意,隐藏DOM节点并未改变DOM树的结构,它只影响DOM节点的显示。这和删除DOM节点是不同的。


4、获取DOM信息

利用jQuery对象的若干方法,我们直接可以获取DOM的高宽等信息,而无需针对不同浏览器编写特定代码:

// 浏览器可视窗口大小:
$(window).width(); // 800
$(window).height(); // 600

// HTML文档大小:
$(document).width(); // 800
$(document).height(); // 3500

// 某个div的大小:
var div = $('#test-div');
div.width(); // 600
div.height(); // 300
div.width(400); // 设置CSS属性 width: 400px,是否生效要看CSS是否有效
div.height('200px'); // 设置CSS属性 height: 200px,是否生效要看CSS是否有效

5、属性操作

1、通用属性操作
  • attr(): 获取/设置元素的自定义属性
  • removeAttr():删除自定义属性
  • prop():获取/设置元素的固有属性
  • removeProp():删除固有属性
    • attr和prop区别?
      • 如果操作的是元素的固有属性,则建议使用prop
      • 如果操作的是元素自定义的属性,则建议使用attr
2、对class属性操作
  • addClass() : 添加class属性值
  • removeClass(): 删除class属性值
  • toggleClass() : 切换class属性,相当于取反
    • toggleClass("one"):
      • 判断如果元素对象上存在class="one",则将属性值one删除掉。 如果元素对象上不存在class="one",则添加
  • css(): 样式修改
// <div id="test-div" name="Test" start="1">...</div>
var div = $('#test-div');
div.attr('data'); // undefined, 属性不存在
div.attr('name'); // 'Test'
div.attr('name', 'Hello'); // div的name属性变为'Hello'
div.removeAttr('name'); // 删除name属性
div.attr('name'); // undefined

prop()方法和attr()类似,但是HTML5规定有一种属性在DOM节点中可以没有值,只有出现与不出现两种,例如:

<input id="test-radio" type="radio" name="test" checked value="1">

等价于:

<input id="test-radio" type="radio" name="test" checked="checked" value="1">

attr()prop()对于属性checked处理有所不同:

prop() 方法设置或返回被选元素的属性和值。 property 属性

当该方法用于返回属性值时,则返回第一个匹配元素的值。

当该方法用于设置属性值时,则为匹配元素集合设置一个或多个属性/值对。

注意:prop() 方法应该用于检索属性值,例如 DOM 属性(如 selectedIndex, tagName, nodeName, nodeType, ownerDocument, defaultChecked, 和 defaultSelected)。

提示:如需检索 HTML 属性,请使用 attr() 方法代替。

提示:如需移除属性,请使用 removeProp() 方法。

var radio = $('#test-radio');
radio.attr('checked'); // 'checked'
radio.prop('checked'); // true

prop()返回值更合理一些。不过,用is()方法判断更好:

var radio = $('#test-radio');
radio.is(':checked'); // true

类似的属性还有selected,处理时最好用is(':selected')


6、CRUD操作

父子关系操作
  1. append():父元素将子元素追加到末尾
    • 对象1.append(对象2): 将对象2添加到对象1元素内部,并且在末尾
  2. prepend():父元素将子元素追加到开头
    • 对象1.prepend(对象2):将对象2添加到对象1元素内部,并且在开头
  3. appendTo(): 可以实现和append()一样的功能
    • 对象1.appendTo(对象2):将对象1添加到对象2内部,并且在末尾
  4. prependTo():可以实现和prepend()一样的功能
    • 对象1.prependTo(对象2):将对象1添加到对象2内部,并且在开头
兄弟关系操作
  1. after():添加元素到元素后边
    • 对象1.after(对象2): 将对象2添加到对象1后边。对象1和对象2是兄弟关系
  2. before():添加元素到元素前边
    • 对象1.before(对象2): 将对象2添加到对象1前边。对象1和对象2是兄弟关系
  3. insertAfter()
    • 对象1.insertAfter(对象2):将对象2添加到对象1后边。对象1和对象2是兄弟关系
  4. insertBefore()
    • 对象1.insertBefore(对象2): 将对象2添加到对象1前边。对象1和对象2是兄弟关系
自身操作
  1. remove():移除元素
    • 对象.remove(): 将对象删除掉,自杀
  2. clone():复制一个对象用于使用,会保留原来的对象,常配合添加、追加操作使用。
  3. empty():清空元素的所有后代元素。
    • 对象.empty():将对象的后代元素全部清空,但是保留当前对象以及其属性节点

7、操作表单

对于表单元素,jQuery对象统一提供val()方法获取和设置对应的value属性:

    <input id="test-input" name="email" value="test">
    <select id="test-select" name="city">               //下拉框
        <option value="BJ" selected>Beijing</option>    //默认值为BJ
        <option value="SH">Shanghai</option>
        <option value="SZ">Shenzhen</option>
    </select>
    <textarea id="test-textarea">Hello</textarea>

<script>
    var
        input = $('#test-input'),
        select = $('#test-select'),
        textarea = $('#test-textarea');

    input.val(); // 'test'
    input.val('abc@example.com'); // 文本框的内容已变为abc@example.com

    select.val(); // 'BJ'
    select.val('SH'); // 选择框已变为Shanghai

    textarea.val(); // 'Hello'
    textarea.val('Hi'); // 文本区域已更新为'Hi'
</script>

可见,一个val()就统一了各种输入框的取值和赋值的问题。


6、JQuery 高级

1、动画

三种方式显示和隐藏元素

1、默认显示和隐藏方式

  1. show([speed,[easing],[fn]])

    • 参数,所有方法的参数都是这三个:
      1. speed:动画的速度。三个预定义的值("slow","normal", "fast")或表示动画时长的毫秒数值(如:1000)
      2. easing:用来指定切换效果,默认是"swing",可用参数"linear"
        • swing:动画执行时效果是 先慢,中间快,最后又慢
        • linear:动画执行时速度是匀速的
      3. fn:在动画完成时执行的函数,每个元素执行一次。
  2. hide([speed,[easing],[fn]])

  3. toggle([speed],[easing],[fn]): 在显示和隐藏之间切换

2、滑动显示和隐藏方式

  1. slideDown([speed],[easing],[fn])
  2. slideUp([speed,[easing],[fn]])
  3. slideToggle([speed],[easing],[fn])

3、淡入淡出显示和隐藏方式

  1. fadeIn([speed],[easing],[fn])
  2. fadeOut([speed],[easing],[fn])
  3. fadeToggle([speed,[easing],[fn]])

2、遍历

1、js的遍历方式

  • for(初始化值;循环结束条件;步长)
//2.遍历li
for (var i = 0; i < citys.length; i++) {
    if("上海" == citys[i].innerHTML){
        //break; 结束循环
        //continue; //结束本次循环,继续下次循环
    }
    //获取内容
    alert(i+":"+citys[i].innerHTML);
}

2、jq的遍历方式

  1. jq对象.each(callback)

    1. 语法:
      jquery对象.each(function(index,element){});

      • index:就是当前元素在集合中的索引

      • element:遍历集合中的当前元素对象

      • this:遍历时,当前元素对象

    2. 回调函数返回值:

      • true: 如果当前function返回为true,“还需要循环吗? true”, 则结束本次循环,继续下次循环(continue)
      • false: 如果当前function返回为false,“还需要循环吗? false” ,则结束循环(break)。
  2. $.each(object, [callback])

  3. for..of : jquery 3.0 版本之后提供的方式
    for(元素对象 of 容器对象)

    //2. jq对象.each(callback)
    citys.each(function (index,element) {
        //3.1 获取li对象 第一种方式 this
        //alert(this.innerHTML);
        //alert($(this).html());
        //3.2 获取li对象 第二种方式 在回调函数中定义参数   index(索引) element(元素对象)
        //alert(index+":"+element.innerHTML);
        //alert(index+":"+$(element).html());

        //判断如果是上海,则结束循环
        if("上海" == $(element).html()){
            //如果当前function返回为false,则结束循环(break)。
            //如果返回为true,则结束本次循环,继续下次循环(continue)
            return true;
        }
        alert(index+":"+$(element).html());
    });

    //3 $.each(object, [callback])
    $.each(citys,function () {
        alert($(this).html());
    });

    //4. for ... of:jquery 3.0 版本之后提供的方式

    for(li of citys){
        alert($(li).html());
    }

3、事件绑定

1、jquery标准的绑定方式

  • jq对象.事件方法(回调函数)
  • 注:如果调用事件方法,不传递回调函数,则会触发浏览器默认行为。
    • 表单对象.submit();//让表单提交
$(function () {
           //1.获取name对象,绑定click事件
           $("#name").click(function () {
               alert("我被点击了...")
           });

           //给name绑定鼠标移动到元素之上事件。绑定鼠标移出事件
            /*$("#name").mouseover(function () {
               alert("鼠标来了...")
            });

            $("#name").mouseout(function () {
                alert("鼠标走了...")
            });*/

            //简化操作,链式编程
            $("#name").mouseover(function () {
                alert("鼠标来了...")
            }).mouseout(function () {
                alert("鼠标走了...")
            });
    
            alert("我要获得焦点了...")
            //$("#name").focus();//让文本输入框获得焦点
            //表单对象.submit();//让表单提交
        });

2、on绑定事件/off解除绑定

  • jq对象.on("事件名称",回调函数)
  • jq对象.off("事件名称")
    • 如果off方法不传递任何参数,则将组件上的所有事件全部解绑
$(function () {
           //1.使用on给按钮绑定单击事件  click
           $("#btn").on("click",function () {
               alert("我被点击了。。。")
           }) ;

           //2. 使用off解除btn按钮的单击事件
            $("#btn2").click(function () {
                //解除btn按钮的单击事件
                //$("#btn").off("click");
                $("#btn").off();//将组件上的所有事件全部解绑
            });
        });

3、事件切换:toggle

  • jq对象.toggle(fn1,fn2...)

    • 当单击jq对象对应的组件后,会执行fn1,第二次点击会执行fn2.....
  • 注意:1.9版本 .toggle() 方法删除,jQuery Migrate(迁移)插件可以恢复此功能。

    <script src="../js/jquery-migrate-1.0.0.js" type="text/javascript" charset="utf-8"></script>
    
    $(function () {
               //获取按钮,调用toggle方法
               $("#btn").toggle(function () {
                   //改变div背景色backgroundColor 颜色为 green
                   $("#myDiv").css("backgroundColor","green");
               },function () {
                   //改变div背景色backgroundColor 颜色为 pink
                   $("#myDiv").css("backgroundColor","pink");
               });
            });
    

4、案例

案例1

广告显示和隐藏

需求:
    1. 当页面加载完,3秒后。自动显示广告
    2. 广告显示5秒后,自动消失。

分析:
    1. 使用定时器来完成。setTimeout (执行一次定时器)
    2. 分析发现JQuery的显示和隐藏动画效果其实就是控制display
    3. 使用  show/hide方法来完成广告的显示
 $(function () {
           //定义定时器,调用adShow方法 3秒后执行一次
           setTimeout(adShow,3000);
           //定义定时器,调用adHide方法,8秒后执行一次
            setTimeout(adHide,8000);
        });
        //显示广告
        function adShow() {
            //获取广告div,调用显示方法
            $("#ad").show("slow");
        }
        //隐藏广告
        function adHide() {
            //获取广告div,调用隐藏方法
            $("#ad").hide("slow");
        }

案例2

抽奖

分析:
	1. 给开始按钮绑定单击事件
		1.1 定义循环定时器
		1.2 切换小相框的src属性
		* 定义数组,存放图片资源路径
		* 生成随机数。数组索引


	2. 给结束按钮绑定单击事件
		1.1 停止定时器
		1.2 给大相框设置src属性
var imgs = ["../img/man00.jpg",
                    "../img/man01.jpg",
                    "../img/man02.jpg",
                    "../img/man03.jpg",
                    "../img/man04.jpg",
                    "../img/man05.jpg",
                    "../img/man06.jpg",
                    ];
        var startId;//开始定时器 的id
        var index;//随机角标

//入口函数
$(function () {
            //处理按钮是否可以使用的效果
            $("#startID").prop("disabled",false);
            $("#stopID").prop("disabled",true);


           //1. 给开始按钮绑定单击事件
            $("#startID").click(function () {
                
                 //处理按钮是否可以使用的效果,不要写进循环定时器里面
                    $("#startID").prop("disabled",true);
                    $("#stopID").prop("disabled",false);
                
                // 1.1 定义循环定时器 20毫秒执行一次 设置该定时器的id为startId
                startId = setInterval(function () {
                   
                    //1.2生成随机角标 0-6
                    index = Math.floor(Math.random() * 7);//0.000--0.999 --> * 7 --> 0.0-----6.9999
                    //1.3设置小相框的src属性
                    $("#img1ID").prop("src",imgs[index]);

                },20);
            });


            //2. 给结束按钮绑定单击事件
            $("#stopID").click(function () {
                //处理按钮是否可以使用的效果
                $("#startID").prop("disabled",false);
                $("#stopID").prop("disabled",true);


               // 1.1 停止定时器
                clearInterval(startId);
               // 1.2 给大相框设置src属性,先不显示,在之后的一秒之内完成显示,链式调用
                $("#img2ID").prop("src",imgs[index]).hide().show(1000);
            });
        });

5、插件(自定义方法)

增强JQuery的功能

实现方式:

  1. $.fn.extend(object)
    • 为Jquery的所有对象定义可以调用的方法 $("#id")
  2. $.extend(object)
    • 为JQeury对象自身定义全局方法 , $或者jQuery

$.fn.extend(object) 案例

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>01-jQuery对象进行方法扩展</title>
    <script src="../js/jQuery-3.6.0.js" type="text/javascript" charset="utf-8"></script>
    <script type="text/javascript">
       //使用jquery插件 给jq对象添加2个方法 check()选中所有复选框,uncheck()取消选中所有复选框

        //1.定义jquery的对象插件
        $.fn.extend({
            //定义了一个check()方法。所有的jq对象都可以调用该方法
            check:function () {
               //让复选框选中

                //this:调用该方法的jq对象
                this.prop("checked",true);
            },
            uncheck:function () {
                //让复选框不选中

                this.prop("checked",false);
            }
            
        });

        $(function () {
           // 获取按钮,并绑定事件
            //$("#btn-check").check();
            //复选框对象.check();

            $("#btn-check").click(function () {
                //获取复选框对象
                $("input[type='checkbox']").check();

            });

            $("#btn-uncheck").click(function () {
                //获取复选框对象
                $("input[type='checkbox']").uncheck();

            });
        });
    </script>
</head>
<body>
<input id="btn-check" type="button" value="点击选中复选框">
<input id="btn-uncheck" type="button" value="点击取消复选框选中">
<br/>
<input type="checkbox" value="football">足球
<input type="checkbox" value="basketball">篮球
<input type="checkbox" value="volleyball">排球

</body>
</html>

$.extend(object)案例

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>01-jQuery对象进行方法扩展</title>
    <script src="../js/jQuery-3.6.0.js" type="text/javascript" charset="utf-8"></script>
    <script type="text/javascript">
        //对全局方法扩展2个方法,扩展min方法:求2个值的最小值;扩展max方法:求2个值最大值
        
        $.extend({
            max:function (a,b) {
                //返回两数中的较大值
                return a >= b ? a:b;
            },
            min:function (a,b) {
                //返回两数中的较小值
                return a <= b ? a:b;
            }
        });

        //调用全局方法
        var max = $.max(4,3);
        //alert(max);

        var min = $.min(1,2);
        alert(min);
    </script>
</head>
<body>
</body>
</html>

posted @ 2022-02-04 18:52  猫的心情  阅读(111)  评论(0)    收藏  举报