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中来实现自身的功能的。
-
是一种解释性脚本语言(代码不进行预编译)。
-
主要用来向HTML(标准通用标记语言下的一个应用)页面添加交互行为。
-
可以直接嵌入HTML页面,但写成单独的js文件有利于结构和行为的分离。
-
跨平台特性,在绝大多数浏览器的支持下,可以在多种平台下运行(如Windows、Linux、Mac、Android、iOS等)。
-
JavaScript脚本语言同其他语言一样,有它自身的基本数据类型,表达式和算术运算符及程序的基本程序框架。JavaScript提供了四种基本的数据类型和两种特殊数据类型用来处理数据和文字。而变量提供存放信息的地方,表达式则可以完成较复杂的信息处理。
ECMAScript它可以理解为是JavaScript的一个标准
最新版本已经到es6版本,但是大部分浏览器还只停留在支持es5代码上
开发环境与线上环境,版本不一致,需要转换工具。
2、引入方式
定义的位置会影响执行顺序
2.1、内部标签
可以写在head或者body内
2.2、外部引入
3、语法入门
在 ES6 中 JavaScript 引用到 严格检查模式。
不用严格检查模式:
当我们如上定义一个变量的时候, JavaScript会认为这是一个 全局变量, 这实际上是不合理的,想一下,一个网页中可能有很多个 js 文件,如果在 这个 js 文件中 i = 1,另一个 js 文件中i = 10,就会造成冲突。
所以,全局变量应该谨慎使用。
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"
我的网页
我的第一个段落。
2、debugger
debugger 语句停止 JavaScript 的执行,并调用(如果可用)调试函数。
使用 debugger 语句与在代码中设置断点具有相同的功能。
3、循环
① for - 多次循环代码块
② for/in - 遍历对象的属性
for/in 遍历的是key, 如果没有key,则遍历的是index。
注释:不要使用 for/in 语句循环遍历 索引顺序 很重要的数组。请改用 for 语句。
循环遍历对象的属性:
单击该按钮可循环遍历对象的属性。
单击该按钮可循环遍历对象的属性。
试一试
John Doe 25 (点击后显示)
③ for/of - 循环遍历可迭代对象的值(ES6新增)
for/of 遍历的是元素, 在Map中,一个键值对 视为 Map 的一个元素
循环遍历数组的值:
**输出:**
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 + "
";
}
注释:该函数接受 3 个参数:
- 项目值
- 项目索引
- 数组本身
上面的例子只用了 value 参数。这个例子可以重新写为:
var txt = "";
var numbers = [45, 4, 9, 16, 25];
numbers.forEach(myFunction);
function myFunction(value) {
txt = txt + value + "
";
}
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)
Set和Map类似,也是一组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可以采用下标循环,遍历Map和Set就无法使用下标。
为了统一集合类型,ES6标准引入了新的iterable类型,Array、Map和Set都属于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包括在内,但Array的length属性却不包括在内。
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
Set与Array类似,但Set没有索引,因此回调函数的前两个参数都是元素本身:
var s = new Set(['A', 'B', 'C']);
s.forEach(function (element, sameElement, set) {
console.log(element);
});
// 输出:
// A
// B
// C
Map的回调函数参数依次为value、key和map本身:
var m = new Map([[1, 'x'], [2, 'y'], [3, 'z']]);
m.forEach(function (value, key, map) {
console.log(value);
});
// 输出:
// x
// y
// z
如果对某些参数不感兴趣,由于JavaScript的函数调用不要求参数必须一致,因此可以忽略它们。例如,只需要获得Array的element:
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
为了获取除了已定义参数a、b之外的参数,我们不得不用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中叫可变长参数),从运行结果可知,传入的参数先绑定a、b,多余的参数以数组形式交给变量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、常量
由于var和let申明的是变量,如果要申明一个常量,在ES6之前是不行的,我们通常用全部大写的变量来表示“这是一个常量,不要修改它的值”(程序员之间默认要遵守的规则,即使这个定义的常量实际上是可以被修改的):
ES6之前无const:
var PI = 3.14;
PI = 3; //可以被修改,但不要这么做
PI; //3
ES6标准引入了新的关键字const来定义常量(readonly),const与let都具有块级作用域:
'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
使用场景
解构赋值在很多时候可以大大简化代码。例如,交换两个变量x和y的值,可以这么写,不再需要临时变量:
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可以拿到xiaoming的birth属性。
让我们拆开写:
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指向undefined或window,不过,我们还是可以控制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() 注意不是 |
获取四位的年(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~11,0表示一月,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的
true或false; - 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}。
我们来从左到右解读一下:
\d{3}表示匹配3个数字,例如'010';\s可以匹配一个空格(也包括Tab等空白符),所以\s+表示至少有一个空格,例如匹配' ','\t\t'等;\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类可以创建出xiaoming、xiaohong、xiaojun等多个实例,每个实例表示一个具体的学生,他们全都属于Student类型。
所以,类和实例是大多数面向对象编程语言的基本概念。
不过,在JavaScript中,这个概念需要改一改。
JavaScript不区分类和实例的概念,而是通过原型(prototype)来实现面向对象编程。
4.1、原型
构造函数、原型和实例的关系:
每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。
原型链:
图中由相互关联的原型组成的链状结构就是原型链,也就是蓝色的这条线。
原型是指当我们想要创建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继承而来,只要Student有run()方法,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的原型。如果你又创建了xiaohong、xiaojun,那么这些对象的原型与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
xiaoming和xiaohong各自的name不同,这是对的,否则我们无法区分谁是谁了。
xiaoming和xiaohong各自的hello是一个函数,但它们是两个不同的函数,虽然函数名称和代码都是相同的!
如果我们通过new Student()创建了很多对象,这些对象的hello函数实际上只需要共享同一个函数就可以了,这样可以节省很多内存。
要让创建的对象共享一个hello函数,根据对象的属性查找原则,我们只要把hello函数移动到xiaoming、xiaohong这些对象共同的原型上就可以了,也就是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构造函数不等于继承了Student,PrimaryStudent创建的对象的原型是:
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;
是不行的!如果这样的话,PrimaryStudent和Student共享一个原型对象,那还要定义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的原型继承实现方式就是:
-
定义新的构造函数,并在内部用
call()调用希望“继承”的构造函数,并绑定this;// PrimaryStudent构造函数: function PrimaryStudent(props) { Student.call(this, props); this.grade = props.grade || 1; } -
借助中间函数
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; } -
继续在新的构造函数的原型上定义新方法。
// 继续在PrimaryStudent原型(就是new F()对象)上定义方法: PrimaryStudent.prototype.getGrade = function () { return this.grade; };
4.4、class继承(ES6,越来越像java)
class继承 - 廖雪峰的官方网站 (liaoxuefeng.com)
JavaScript的对象模型是基于原型实现的,特点是简单,缺点是理解起来比传统的类-实例模型要困难,最大的缺点是继承的实现需要编写大量代码,并且需要正确实现原型链。
新的关键字class从 ES6 开始正式被引入到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需要name和grade两个参数,并且需要通过super(name)来调用父类的构造函数,否则父类的name属性无法正常初始化。
PrimaryStudent已经自动获得了父类Student的hello方法,我们又在子类中定义了新的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对象有innerWidth和innerHeight属性,可以获取浏览器窗口的内部宽度和高度。内部宽高是指除去菜单栏、工具栏、边框等占位元素后,用于显示网页的净宽高。对应的,还有一个outerWidth和outerHeight属性,可以获取浏览器窗口的整个宽高。
兼容性: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树的根节点。
document的title属性是从HTML文档中的读取的,但是可以动态改变:
document.title = '努力学习JavaScript!'; //浏览器窗口标题改变
要查找DOM树的某个节点,需要从document对象开始查找。最常用的查找是根据ID和Tag Name。
用document对象提供的getElementById()和getElementsByTagName()
//我们先准备HTML数据:
//提供的饮料有:摩卡,酸奶,果汁,
write()
向文档写 HTML 表达式 或 JavaScript 代码。
99乘法表
解决关于js报错 Cannot read property innerHTML of null
- 相信很多同学在开发过程中都会遇到
Cannot read property ‘innerHTML’ of null ——这个报错的字面含义是:不能读取空的内部HTML。 - 实际上,在页面的HTML结构中,innerHTML是有实际的值并可以在console进行获取查询到。
- 根据浏览器的渲染原理,HTML代码从上到下执行代码,当浏览器JS解析器解析到script并进行DOM操作,下面的DOM结构还没有进行搭建。那么就会提示不能正确读取内部的HTML信息。
- 解决思路:将涉及DOM读取部分的script放在DOM结构后面。标签前面。或者在前置的script标签写明:window.onload等,确保DOM树加载完毕后再执行这段script代码。
Cookie
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代码是允许的:
...
如果引入的第三方的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操作太重要了)。
Title
操作DOM对象
这是段落
节点
下面我们介绍几个关于节点的概念。
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不支持querySelector和querySelectorAll。IE8仅有限支持。
严格地讲,我们这里的DOM节点是指Element,但是DOM节点实际上是Node,在HTML中,Node包括Element、Comment、CDATA_SECTION等很多种,以及根节点Document类型,但是,绝大多数时候我们只关心Element,也就是实际控制页面结构的Node,其他类型的Node忽略即可。根节点Document已经自动绑定为全局变量document。
2、更新DOM
拿到一个DOM节点后,我们可以对它进行更新。
可以直接修改节点的文本,方法有两种:
1、innerHTML(强大,但有安全隐患)
一种是修改innerHTML属性,这个方式非常强大,不但可以修改一个DOM节点的文本内容,还可以直接通过HTML片段修改DOM节点内部的子树:
// 获取...
var p = document.getElementById('p-id');
// 设置文本为abc:
p.innerHTML = 'ABC'; // ABC
// 设置HTML:
p.innerHTML = 'ABC RED XYZ';
// ...
的内部结构已修改
用innerHTML时要注意,是否需要写入HTML。如果写入的字符串是通过网络拿到的,要注意对字符编码来避免XSS攻击。
2、innerText / innerContent (安全)
第二种是修改innerText或textContent属性,这样可以自动对字符串进行HTML编码,保证无法设置任何HTML标签:
// 获取...
var p = document.getElementById('p-id');
// 设置文本:
p.innerText = '';
// HTML被自动编码,无法设置一个