一、简介
JavaScript负责页面中的的行为,它是一门运行在浏览器端的脚本语言。
二、JS的编写的位置
1.可以编写到标签的指定属性中
<button onclick="alert('hello');">我是按钮</button>
<a href="javascript:alert('aaa');">超链接</a>
2.可以编写到script标签中
<script type="text/javascript">
//编写js代码
</script>
3.可以将代码编写到外部的js文件中,然后通过标签将其引入
script标签一旦用于引入外部文件了,就不能在编写代码了,即使编写了浏览器也会忽略 ,如果需要则可以在创建一个新的script标签用于编写内部代码
<script type="text/javascript" src="文件路径"></script>
三、输出语句
1.在浏览器窗口中弹出一个警告框
alert("要输出的内容");
2.该内容将会被写到body标签中,并在页面中显示
document.write("要输出的内容");
3.该内容会被写到开发者工具的控制台中
console.log("要输出的内容");
四、基本的语法
1.函数声明(无需;)
function functionName(arg0,arg1,arg2){
//函数声明
}
2.赋值语句(要加;分号)
var functionName=function(arg0,arg1,arg2){
//函数表达式
};(注意分号)
3.单行注释
//注释内容
4.多行注释
/*
注释内容
*/
5.其他注意事项
- 严格区分大小写
- JS中每条语句以分号(;)结尾如果不写分号,浏览器会自动添加,但是会消耗一些系统资源, 而且有些时候,浏览器会加错分号,所以在开发中分号必须写
- JS中会自动忽略多个空格和换行,所以我们可以利用空格和换行对代码进行格式化。
-
关于语句分号
- js一条语句的后面可以不加分号
- 是否加分号是编码风格问题, 没有应该不应该,只有你自己喜欢不喜欢
- 在下面2种情况下不加分号会有问题
小括号开头的前一条语句
中方括号开头的前一条语句
- 解决办法: 在行首加分号
- 知乎热议: https://www.zhihu.com/question/20298345
五、字面量和变量
1.字面量
字面量实际上就是一些固定的值,比如 1 2 3 4 true false null NaN “hello”
字面量都是不可以改变的。
2.变量
(1)变量可以用来保存字面量,并且可以保存任意的字面量
(2)一般都是通过变量来使用字面量,而不直接使用字面量,而且也可以通过变量来对字面量进行一个描述
(3)声明变量:使用var关键字来声明一个变量
1 | |
(4)为变量赋值
1 | |
(5)声明和赋值同时进行
1 | |
3.标识符
在JS中所有的可以自主命名的内容,都可以认为是一个标识符,是标识符就应该遵守标识符的规范。比如:变量名、函数名、属性名
(1)标识符中可以含有字母、数字、_、$
(2)标识符不能以数字开头
(3)标识符不能是JS中的关键字和保留字
(4)标识符一般采用驼峰命名法:xxxYyyZzz
六、数据类型
1.基本数据类型和引用数据类型
(1)基本数据类型
- String: 任意字符串
- Number: 任意的数字
- boolean: true/false
- undefined: undefined
- null: null –>使用
typeof
时返回object
- symbol (ECMAScript 2016新增)。 –>Symbol 是 基本数据类型 的一种,Symbol 对象是 Symbol原始值的封装 (en-US) 。
- bigint, –>BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数。
基本数据类型的数据,变量是直接保存的它的值。
变量与变量之间是互相独立的,修改一个变量不会影响其他的变量。
-
String 字符串
JS中的字符串需要使用引号引起来双引号或单引号都行
在字符串中使用\作为转义字符,使用typeof运算符检查字符串时,会返回”string”
\' ==> '
\" ==> "
\n ==> 换行
\t ==> 制表符
\\ ==> \
- Number 数值
- 最大能表示的值:Number.MAX_VALUE= 1.7976931348623157e+308
- 特殊的数字:能赋值给变量
- Infinity 正无穷 a = Infinity ,能赋值
- -Infinity 负无穷
- NaN 非法数字(Not A Number)
- 其他进制的数字的表示:
- 0b 开头表示二进制,但是不是所有的浏览器都支持
- 0 开头表示八进制
- 0x 开头表示十六进制
- 使用typeof检查一个Number类型的数据时,会返回”number”(包括NaN 和 Infinity)
- Boolean 布尔值
- 布尔值主要用来进行逻辑判断,布尔值只有两个
- true 逻辑的真
- false 逻辑的假
- 使用typeof检查一个布尔值时,会返回”boolean”
- Null 空值
- 空值专门用来表示为空的对象,Null类型的值只有一个:null
- 使用typeof检查一个Null类型的值时会返回”object”
- Undefined 未定义
- 如果声明一个变量但是没有为变量赋值此时变量的值就是undefined
- 该类型的值只有一个 undefined
- 使用typeof检查一个Undefined类型的值时,会返回”undefined”
(2)引用数据类型
- Object: 任意对象
- Function: 一种特别的
对象
(可以执行) –内部包含可运行的代码 - Array: 一种特别的
对象
(key
为数值下标属性, 内部数据是有序的) - 引用数据类型的数据,变量是保存的对象的引用(内存地址)。
- 如果多个变量指向的是同一个对象,此时修改一个变量的属性,会影响其他的变量。
- 比较两个变量时,对于基本数据类型,比较的就是值,
- 对于引用数据类型比较的是地址,地址相同才相同
2.判断方法
(1)typeof
typeof
操作符返回一个字符串
,表示未经计算的操作数的类型。
可以判断: undefined/ 数值 / 字符串 / 布尔值 / function
不能判断: null与object object与array
注意
: 运行console.log(typeof undefined)
时,得到的的也是一个字符串,同时为小写!!
–>'undefined'
代码示例
// typeof返回数据类型的字符串表达 var a //注意:typeof返回的是字符串 console.log(a, typeof a, typeof a==='undefined',a===undefined ) // undefined 'undefined' true true console.log(undefined === 'undefined') //false a = 4 console.log(typeof a==='number') //true a = 'hongjilin' console.log(typeof a==='string') //true console.log(typeof a==='String') //false -->注意,返回的类型为小写 a = true console.log(typeof a==='boolean') //true a = null console.log(typeof a, a===null) // 'object' true let b={} console.log(typeof b,typeof null, '-------') // 'object' 'object' -->所以Typeof不能判断null与object
(2)instanceof
(判断实例方法)
专门判断对象
的具体类型
instanceof
运算符用于检测构造函数的prototype
属性是否出现在某个实例对象的原型链上。代码示例:
var b1 = { b2: [1, 'abc', console.log], //可以简化成 b3:()=>()=> 'hongjilin' -->高阶函数相关知识 b3: function () { return () =>{ return 'hongjilin'} } } /**使用instanceof进行对象判断*/ console.log(b1 instanceof Object, b1 instanceof Array) // true false console.log(b1.b2 instanceof Array, b1.b2 instanceof Object) // true true console.log(b1.b3 instanceof Function, b1.b3 instanceof Object) // true true /**使用typeof进行对象中某属性的判断*/ console.log(typeof b1.b2, typeof null) // 'object' 'object' console.log(typeof b1.b3==='function') // true console.log(typeof b1.b2[2]==='function') //true /**调用对象与数组中某函数示例*/ b1.b2[2]('调用console.log打印hongjilin') //调用console.log打印hongjilin console.log(b1.b3()()) // hongjilin
(3)===
具体可以看 MDN的JavaScript中的相等性判断
可以判断: undefined, null
简而言之,在比较两件事情时,
双等号将执行类型转换
;三等号将进行相同的比较,而不进行类型转换
(如果类型不同, 只是总会返回 false )
3.疑难
(1)undefined与null的区别?
undefined代表定义未赋值
nulll定义并赋值了, 只是值为null
代码示例
var a console.log(a) // undefined a = null console.log(a) // null
(2) 什么时候给变量赋值为null呢?
初始赋值, 表明将要赋值为对象,
可以用做约定俗成的占位符
结束前, 让对象成为垃圾对象(被垃圾回收器回收)
代码示例
//起始,可以用做约定俗成的占位符 var b = null // 初始赋值为null, 表明将要赋值为对象 //确定对象就赋值 b = ['atguigu', 12] //最后在不使用的时候,将其引用置空,就可以释放b这个对象占用的内存 ---当没有引用指向它的对象称为垃圾对象 b = null // 让b指向的对象成为垃圾对象(被垃圾回收器回收)
(3) 严格区别变量类型与数据类型?
- 数据的类型
- 基本类型
- 对象类型
- 变量的类型(变量内存值的类型)
- 基本类型: 保存就是
基本类型
的数据- 引用类型: 保存的是地址值(对象类型)
(4)字符串对比>
、<
以及charCodeAt()
方法
Javascript字符串在进行大于(小于)比较时,会根据第一个不同的字符的ascii值码进行比较,当数字(number)与字符串(string)进行比较大小时,会强制的将数字(number)转换成字符串(string)然后再进行比较
1 2 3 4 5 6 7 (function(){ console.log('13'>'3'); // 输出:false console.log(5>'6'); // 输出: false console.log('d'>'ABDC') // 输出: true console.log(19>'ssf') // 输出 false console.log('A'>'abcdef') // 输出 false })()手动转换为ascii后相减,用正负数表示大小
1 sorter={(a:string,b:string)=> a.charCodeAt()-b.charCodeAt()}
4.类型转换
类型转换就是指将其他的数据类型,转换为String Number 或 Boolean
(1)转换为String
-
方式一(强制类型转换):
调用被转换数据的toString()方法
例子:
var a = 123;
a = a.toString();
注意:这个方法不适用于null和undefined
由于这两个类型的数据中没有方法,所以调用toString()时会报错
-
方式二(强制类型转换):
调用String()函数
例子:
var a = 123;
a = String(a);
原理:对于Number Boolean String都会调用他们的toString()方法来将其转换为字符串,对于null值,直接转换为字符串”null”。对于undefined直接转换为字符串”undefined”
-
方式三(隐式的类型转换):
为任意的数据类型 +””
例子:
var a = true;
a = a + "";
原理:和String()函数一样
(2)转换为Number
-
方式一(强制类型转换):
调用Number()函数
例子:
var s = "123";
s = Number(s);
转换的情况:
- 字符串 > 数字
如果字符串是一个合法的数字,则直接转换为对应的数字
如果字符串是一个非法的数字,则转换为NaN
如果是一个空串或纯空格的字符串,则转换为0- 布尔值 > 数字
true转换为1
false转换为0- 空值 > 数字
null转换为0- 未定义 > 数字
undefined 转换为NaN
-
方式二(强制类型转换):
调用parseInt()或parseFloat()
这两个函数专门用来将一个字符串转换为数字的
如果对非String使用parseInt()或parseFloat(),它会先将其转换为String然后在操作 parseInt()
可以将一个字符串中的有效的整数位提取出来,并转换为Number
例子:
var a = "123.456px";
a = parseInt(a); //123
如果需要可以在parseInt()中指定一个第二个参数,来指定进制parseFloat()可以将一个字符串中的有效的小数位提取出来,并转换为Number
例子:
var a = "123.456px";
a = parseFloat(a); //123.456
-
方式三(隐式的类型转换):
使用一元的+来进行隐式的类型转换
例子:
var a = "123";
a = +a;
原理:和Number()函数一样
(3)转换为布尔值
-
方式一(强制类型转换):
使用Boolean()函数
例子:
1 2 | var s = "false"; s = Boolean(s); //true |
转换的情况
字符串 > 布尔
除了空串其余全是true
数值 > 布尔
除了0和NaN其余的全是true
null、undefined > 布尔
都是false
对象 > 布尔
都是true
-
方式二(隐式类型转换):
为任意的数据类型做两次非运算,即可将其转换为布尔值
例子:
var a = "hello";
a = !!a; //true
5.数据,变量, 内存的理解
(1)数据
- 存储在内存中代表特定信息的’东西’, 本质上是0101…
- 数据的特点:
可传递
,可运算
–>let a=0;b=a 🔜体现可传递- 一切皆数据
- 内存中所有操作的目标: 数据
- 算术运算
- 逻辑运算
- 赋值
- 运行函数
(2)内存
- 内存条通电后产生的可储存数据的空间(临时的)
- 内存产生和死亡: 内存条(电路版)==>通电==>产生内存空间==>存储数据==>处理数据==>断电==>内存空间和数据都消失
- 一块小内存的2个数据
- 内部存储的数据
- 地址值
- 内存分类
- 栈: 全局变量/局部变量
- 堆: 对象
(3)变量
- 可变化的量, 由变量名和变量值组成
- 每个变量都对应的一块小内存, 变量名用来查找对应的内存, 变量值就是内存中保存的数据
ps:变量
obj.xx
–>.
相当于拿着地址找到后面对应的内存,所以只有当我变量中存的是地址,才可以用.
(4)内存,数据, 变量三者之间的关系
- 内存用来存储数据的空间
- 变量是内存的标识
(5)相关问题引出
-
关于赋值和内存的问题
let a = xxx, a内存中到底保存的是什么?
- xxx是基本数据, 保存的就是这个数据
- xxx是对象, 保存的是对象的地址值
- xxx是一个变量, 保存的xxx的内存内容(可能是基本数据, 也可能是地址值)
-
关于引用变量赋值问题
2个引用变量指向同一个对象, 通过一个变量修改对象内部数据, 另一个变量看到的是修改之后的数据
2个引用变量指向同一个对象, 让其中一个引用变量指向另一个对象, 另一引用变量依然指向前一个对象
代码示例:
let a = {age: 12} //此时是将a指向的地址值赋值给B,所以B此时也指向{age:12}这个内存 let b = a //此时重新创建了一个内存并让a指向它,所以此处a指向的是{name:'hong'},而b指向仍是刚开始的指向{age:12} a = {name: 'hong'} //此时a与b指向的内存已经不一样了,所以修改互不影响 b.age = 14 console.log(b.age, a.name, a.age) // 14 hong undefined //2个引用变量指向同一个对象, 让其中一个引用变量指向另一个对象, 另一引用变量依然指向前一个对象 -->所以 a 仍是 {name: 'hong'} const fn2=(obj) => obj = {age: 15} fn2(a) console.log(a.age) //undefined
-
在js调用函数时传递变量参数时, 是值传递还是引用传递
理解1: 都是值(基本/地址值)传递,所以实际上传进function中的参数也是拿着其存着的地址值找内存
//传进来的obj存储的是a中存的地址值,所以obj==a(因为他们地址值一致,指向一致) //2个引用变量指向同一个对象, 通过一个变量修改对象内部数据, 另一个变量看到的是修改之后的数据 -->所以被进行了修改 let a = {name: 'hong'} const fn2=(obj) => obj.age= 15 fn2(a) console.log(a.age) //15
理解2: 可能是值传递, 也可能是引用传递(地址值)
-
JS引擎如何管理内存?
- 内存生命周期
- 分配小内存空间, 得到它的使用权
- 存储数据, 可以反复进行操作
- 释放小内存空间
- 释放内存
- 局部变量: 函数执行完自动释放
- 对象: 成为垃圾对象==>垃圾回收器回收
var a = 3 var obj = {name:"hong"} obj = undefined ||null //此时,obj没有被释放,但是之前声明的`{name:"hong"}`由于没有人指向它,会在后面你某个时刻被垃圾回收器回收 >function fn () { var b = {}} fn() // b是自动释放, b所指向的对象是在后面的某个时刻由垃圾回收器回收
七、基础语法
1.运算符
运算符也称为操作符,通过运算符可以对一个或多个值进行运算或操作
(1)typeof运算符
用来检查一个变量的数据类型
语法:typeof 变量
它会返回一个用于描述类型的字符串作为结果
(2)算数运算符
+ 对两个值进行加法运算并返回结果
- 对两个值进行减法运算并返回结果
* 对两个值进行乘法运算并返回结果
/ 对两个值进行除法运算并返回结果
% 对两个值进行取余运算并返回结果
除了加法以外,对非Number类型的值进行运算时,都会先转换为Number然后在做运算。
而做加法运算时,如果是两个字符串进行相加,则会做拼串操作,将两个字符连接为一个字符串。
任何值和字符串做加法,都会先转换为字符串,然后再拼串
(3)一元运算符
一元运算符只需要一个操作数
-
一元的+
就是正号,不会对值产生任何影响,但是可以将一个非数字转换为数字
例子:
var a = true;
a = +a;
-
一元的-
就是负号,可以对一个数字进行符号位取反
例子:
var a = 10;
a = a;
-
自增(无论是++a 还是 a++都会立即使原变量自增1)
- ++a的值是变量的新值(自增后的值)
- a++的值是变量的原值(自增前的值)
-
自减(自减可以使变量在原值的基础上自减1)
- --a的值是变量的新值(自减后的值)
- a--的值是变量的原值(自减前的值)
(4)逻辑运算符
- !
非运算可以对一个布尔值进行取反,true变false false边true
当对非布尔值使用!时,会先将其转换为布尔值然后再取反
我们可以利用!来将其他的数据类型转换为布尔值
- &&
- 如果第一个值为false,则返回第一个值
- 如果第一个值为true,则返回第二个值
&&可以对符号两侧的值进行与运算
只有两端的值都为true时,才会返回true。只要有一个false就会返回false。
与是一个短路的与,如果第一个值是false,则不再检查第二个值
对于非布尔值,它会将其转换为布尔值然后做运算,并返回原值
- ||
- 如果第一个值为true,则返回第一个值
- 如果第一个值为false,则返回第二个值
||可以对符号两侧的值进行或运算
只有两端都是false时,才会返回false。只要有一个true,就会返回true。
或是一个短路的或,如果第一个值是true,则不再检查第二个值
对于非布尔值,它会将其转换为布尔值然后做运算,并返回原值
(5)赋值运算符
- =
可以将符号右侧的值赋值给左侧变量
- +=
a += 5 相当于 a = a+5
var str = "hello"; str += "world";
- -=
a -= 5 相当于 a = a-5
- *=
- /=
- %=
(6)关系运算符
关系运算符用来比较两个值之间的大小关系的
>
>=
<
<=
关系运算符的规则和数学中一致,用来比较两个值之间的关系,
- 如果关系成立则返回true,关系不成立则返回false。
- 如果比较的两个值是非数值,会将其转换为Number然后再比较。
- 如果比较的两个值都是字符串,此时会比较字符串的Unicode编码,而不会转换为Number。
(7)相等运算符
相等,判断左右两个值是否相等,如果相等返回true,如果不等返回false
相等会自动对两个值进行类型转换,如果对不同的类型进行比较,会将其转换为相同的类型然后再比较,转换后相等它也会返回true,null == undifined
- !=
不等,判断左右两个值是否不等,如果不等则返回true,如果相等则返回false
不等也会做自动的类型转换。
- ===
全等,判断左右两个值是否全等,它和相等类似,只不过它不会进行自动的类型转换,
如果两个值的类型不同,则直接返回false
- !==
不全等,和不等类似,但是它不会进行自动的类型转换,如果两个值的类型不同,它会直接返回true
- 特殊的值:
null和undefined
由于undefined衍生自null,所以null == undefined 会返回true。
但是 null === undefined 会返回false。
NaN
NaN不与任何值相等,报告它自身 NaN == NaN //false
判断一个值是否是NaN
使用isNaN()函数
(8)三元运算符:
- ?:
语法:条件表达式?语句1:语句2;
执行流程:
先对条件表达式求值判断,
如果判断结果为true,则执行语句1,并返回执行结果
如果判断结果为false,则执行语句2,并返回执行结果
优先级:
比如 先乘除 后加减 先与 后或
具体的优先级可以参考优先级的表格,在表格中越靠上的优先级越高,
优先级越高的越优先计算,优先级相同的,从左往右计算。
优先级不需要记忆,如果越到拿不准的,使用()来改变优先级。
2.流程控制语句
程序都是自上向下的顺序执行的,
通过流程控制语句可以改变程序执行的顺序,或者反复的执行某一段的程序。
(1)条件判断语句也称为if语句
- 语法一:
if(条件表达式){
语句...
}
1 2 3 4 | 执行流程: if语句执行时,会先对条件表达式进行求值判断, 如果值为true,则执行if后的语句 如果值为false,则不执行 |
- 语法二:
if(条件表达式){
语句...
}else{
语句...
}
1 2 3 4 | 执行流程: if...else语句执行时,会对条件表达式进行求值判断, 如果值为true,则执行if后的语句 如果值为false,则执行else后的语句 |
- 语法三:
if(条件表达式){
语句...
}else if(条件表达式){
语句...
}else if(条件表达式){
语句...
}else if(条件表达式){
语句...
}else{
语句...
}
1 2 3 4 5 | 执行流程 if...else if...else语句执行时,会自上至下依次对条件表达式进行求值判断, 如果判断结果为true,则执行当前if后的语句,执行完成后语句结束。 如果判断结果为false,则继续向下判断,直到找到为true的为止。 如果所有的条件表达式都是false,则执行else后的语句 |
(2)条件分支语句 switch语句
语法:
switch(条件表达式){
case 表达式:
语句...
break;
case 表达式:
语句...
break;
case 表达式:
语句...
break;
default:
语句...
break;
}
执行流程:
switch…case…语句在执行时,会依次将case后的表达式的值和switch后的表达式的值进行全等比较,
如果比较结果为false,则继续向下比较。如果比较结果为true,则从当前case处开始向下执行代码。
如果所有的case判断结果都为false,则从default处开始执行代码。
(3)循环语句
通过循环语句可以反复执行某些语句多次
- while循环
语法:
while(条件表达式){
语句...
}
执行流程:
while语句在执行时,会先对条件表达式进行求值判断,
如果判断结果为false,则终止循环
如果判断结果为true,则执行循环体
循环体执行完毕,继续对条件表达式进行求值判断,依此类推
- do…while循环
语法:
do{
语句...
}while(条件表达式)
执行流程
do…while在执行时,会先执行do后的循环体,然后在对条件表达式进行判断,
如果判断判断结果为false,则终止循环。
如果判断结果为true,则继续执行循环体,依此类推
和while的区别:
while:先判断后执行
do…while: 先执行后判断
do…while可以确保循环体至少执行一次。
- for循环
语法:
for(①初始化表达式 ; ②条件表达式 ; ④更新表达式){
③语句...
}
执行流程:
首先执行①初始化表达式,初始化一个变量,
然后对②条件表达式进行求值判断,如果为false则终止循环
如果判断结果为true,则执行③循环体
循环体执行完毕,执行④更新表达式,对变量进行更新。
更新表达式执行完毕重复②
死循环
while(true){
}
for(;;){
}
八、对象(Object)
对象是JS中的引用数据类型
对象是一种复合数据类型,在对象中可以保存多个不同数据类型的属性
使用typeof检查一个对象时,会返回object
1.对象的分类:
(1)内建对象
- 由ES标准中定义的对象,在任何的ES的实现中都可以使用
- 比如:Math String Number Boolean Function Object….
(2)宿主对象
- 由JS的运行环境提供的对象,目前来讲主要指由浏览器提供的对象
- 比如 BOM DOM
(3)自定义对象
- 由开发人员自己创建的对象
创建对象
- 方式一:
var obj = new Object();
- 方式二:
var obj = {};
2.向对象中添加属性
语法:
(1)对象.属性名 = 属性值;
(2)对象[“属性名”] = 属性值;
- 这种方式能够使用特殊的属性名
- 属性名包含特殊字符:
-
空格
- 属性名不确定
对象的属性名没有任何要求,不需要遵守标识符的规范,但是在开发中,尽量按照标识符的要求去写。
属性值也可以任意的数据类型。
3.读取对象中的属性
语法:
(1)对象.属性名
(2)对象[“属性名”] “属性名”可以使字符串常量,也可以是字符串变量
如果读取一个对象中没有的属性,它不会报错,而是返回一个undefined
4.删除对象中的属性
语法:
delete 对象.属性名
delete 对象["属性名"]
5.遍历
使用in检查对象中是否含有指定属性
(1)语法:”属性名” in 对象
如果在对象中含有该属性,则返回true
如果没有则返回false
循环遍历对象自身的和继承的可枚举属性(不含Symbol属性).
var obj = {'0':'a','1':'b','2':'c'};
for(var i in obj) {
console.log(i,":",obj[i]);
}
6.添加属性
使用对象字面量,在创建对象时直接向对象中添加属性
语法:
var obj = {
属性名:属性值,
属性名:属性值,
属性名:属性值,
属性名:属性值
}
九、函数(Function)
函数也是一个对象,也具有普通对象的功能(能有属性)
函数中可以封装一些代码,在需要的时候可以去调用函数来执行这些代码
使用typeof检查一个函数时会返回function
1.定义函数
(1)函数声明
function 函数名([形参1,形参2...形参N]){
语句...
}
function fn1 () { console.log('fn1()' )//函数声明
(2)函数表达式
var 函数名 = function([形参1,形参2...形参N]){
语句...
};
const fn2 = ()=> console.log('fn2()') //表达式
2.调用函数
语法:函数对象([实参1,实参2…实参N]);
fun() sum() alert() Number() parseInt()
当我们调用函数时,函数中封装的代码会按照编写的顺序执行
(1)test(): 直接调用
(2)obj.test(): 通过对象调用
(3)new test(): new调用
(4)test.call/apply(obj)
: 临时让test成为obj的方法进行调用
代码示例
var obj = {}
//此处不能使用箭头函数,因为箭头函数会改变this指向
function test2 () {
this.xxx = 'hongjilin'
}
// obj.test2() 不能直接, 根本就没有
test2.call(obj) // 可以让一个函数成为指定任意对象的方法进行调用
console.log(obj.xxx)
3.立即执行函数
函数定义完,立即被调用,这种函数叫做立即执行函数
立即执行函数往往只会执行一次
(1)全称:
Immediately-Invoked Function Expression
自调用函数
(2)作用:
- 隐藏实现
- 不会污染外部(一般指全局)命名空间
- 用它来编码js模块
代码示例
(function () { //匿名函数自调用
var a = 3
console.log(a + 3)
})()
console.log(a) // a is not defined
//此处前方为何要一个`;`-->因为自调用函数外部有一个()包裹,可能与前方以()结尾的代码被一起认为是函数调用
//不加分号可能会被认为这样 console.log(a)(IIFE)
;(function () {//不会污染外部(全局)命名空间-->举例
let a = 1;
function test () { console.log(++a) } //声明一个局部函数test
window.$ = function () { return {test: test} }// 向外暴露一个全局函数
})()
test () //test is not defined
$().test() // 1. $是一个函数 2. $执行后返回的是一个对象
4.遍历对象
for(var v in obj){
document.write("property:name ="+v+"value="+obj[v]+"<br/>" );
}
5.形参和实参
(1)形参:形式参数
- 定义函数时,可以在()中定义一个或多个形参,形参之间使用,隔开
- 定义形参就相当于在函数内声明了对应的变量但是并不赋值,
- 形参会在调用时才赋值。
(2)实参:实际参数
- 调用函数时,可以在()传递实参,传递的实参会赋值给对应的形参,
- 调用函数时JS解析器不会检查实参的类型和个数,可以传递任意数据类型的值。
- 如果实参的数量大于形参,多余实参将不会赋值,
- 如果实参的数量小于形参,则没有对应实参的形参将会赋值undefined
6.返回值,就是函数执行的结果
使用return 来设置函数的返回值。
语法:return 值;
该值就会成为函数的返回值,可以通过一个变量来接收返回值
- return后边的代码都不会执行,一旦执行到return语句时,函数将会立刻退出。
- return后可以跟任意类型的值,可以是基本数据类型,也可以是一个对象。
- 如果return后不跟值,或者是不写return则函数默认返回undefined。
7.break、continue和return
- break 退出循环
- continue 跳过当次循环
- return 退出函数
8.方法(method)
可以将一个函数设置为一个对象的属性,当一个对象的属性是一个函数时,我们称这个函数是该对象的方法。
对象.方法名();
函数名()
9.函数的属性和方法
(1)call() 直接传递函数的实参
(2)apply() 将实参封装到一个数组中传递
这两个方法都是函数对象的方法需要通过函数对象来调用
通过两个方法可以直接调用函数,并且可以通过第一个实参来指定函数中this
(3)arguments
arguments和this类似,都是函数中的隐含的参数
arguments是一个类数组元素,它用来封装函数执行过程中的实参
所以即使不定义形参,也可以通过arguments来使用实参
arguments中有一个属性callee表示当前执行的函数对象
10.作用域
作用域简单来说就是一个变量的作用范围。
在JS中作用域分成两种:
(1)全局作用域
- 直接在script标签中编写的代码都运行在全局作用域中
- 全局作用域在打开页面时创建,在页面关闭时销毁。
- 全局作用域中有一个全局对象window,window对象由浏览器提供,
- 可以在页面中直接使用,它代表的是整个的浏览器的窗口。
- 在全局作用域中创建的变量都会作为window对象的属性保存
- 在全局作用域中创建的函数都会作为window对象的方法保存
- 在全局作用域中创建的变量和函数可以在页面的任意位置访问。
- 在函数作用域中也可以访问到全局作用域的变量。
- 尽量不要在全局中创建变量
(2)函数作用域
- 函数作用域是函数执行时创建的作用域,每次调用函数都会创建一个新的函数作用域。
- 函数作用域在函数执行时创建,在函数执行结束时销毁。
- 在函数作用域中创建的变量,不能在全局中访问。
- 当在函数作用域中使用一个变量时,它会先在自身作用域中寻找,
- 如果找到了则直接使用,如果没有找到则到上一级作用域中寻找,
- 如果找到了则使用,找不到则继续向上找,一直会
(3)作用域链
作用域链在函数声明时产生,在函数调用时将当前函数的变量对象放到作用域链上,完成作用域链。在局部作用域中访问变量时,从当前作用域的变量对象中沿作用域链向上访问
(4)变量提升
通过 var 声明的变量会提升到所在作用域的顶部,但是变量的赋值还在原来的位置。
a // => undefined
var a = 100
a // => 100
// 等同于
var a
a // => undefined
a = 100
a // => 100
通过 function 声明的函数(包括函数体)会提升到所在作用域的顶部,在此之后都可以调用。
先执行函数声明提升,再执行变量声明提升,如果变量名与已声明的函数名相同,变量声明失效(不影响赋值)
-
变量的声明提前
在全局作用域中,使用var关键字声明的变量会在所有的代码执行之前被声明,但是不会赋值。
所以我们可以在变量声明前使用变量。但是不使用var关键字声明的变量不会被声明提前。
在函数作用域中,也具有该特性,使用var关键字声明的变量会在函数所有的代码执行前被声明,
如果没有使用var关键字声明变量,则变量会变成全局变量
-
函数的声明提前
在全局作用域中,使用函数声明创建的函数(function fun(){}),会在所有的代码执行之前被创建,也就是我们可以在函数声明前去调用函数,在函数作用域中,使用函数声明创建的函数,会在所有的函数中的代码执行之前就被创建好了。
但是使用函数表达式(var fun = function(){})创建的函数没有该特性
11.执行上下文
执行一段 JS 代码时,会做一个预处理,我们称之为执行上下文。每执行一段代码,都会创建一个执行上下文,所以 JS 创建了一个执行上下文栈 (stack) 用来存放执行上下文。
预处理包含:
(1) 开辟一个内存空间;
(2) 确定变量对象;
(3)完成作用域链;
(4) 确定 this 指向。
(1)全局执行上下文
当程序开始解析时,将全局执行上下文 (window) 压入到执行上下文栈中,对全局变量进行预处理:
-
var 声明的全局变量赋值为 undefined,作为 window 的属性;
-
function 声明的全局函数,作为 window 的方法;
-
this 指向 window。
页面关闭时,全局执行上下文从执行上下文栈中弹出。
(2)函数执行上下文
当执行函数时,会创建一个函数执行上下文,并压入到执行上下文栈中,对局部变量进行预处理:
-
将实参赋值给形参,作为函数执行上下文的属性;
-
将实参列表赋值给 arguments,作为函数执行上下文的属性;
-
var 声明的局部变量赋值为 undefined,作为函数执行上下文的属性;
-
function 声明的局部函数,作为函数执行上下文的方法;
-
this 指向调用函数的对象。
函数执行完成时,函数执行上下文会从执行上下文栈中弹出
11.this(上下文对象)
(1)定义
- 任何函数本质上都是通过某个对象来调用的,如果没有直接指定就是window
- 所有函数内部都有一个变量this
- 它的值是
调用函数的当前对象
(2) 如何确定this的值?
我们每次调用函数时,解析器都会将一个上下文对象作为隐含的参数传递进函数。
使用this来引用上下文对象,根据函数的调用形式不同,this的值也不同。
- 以函数的形式调用时,this是window test(): window
- 以方法的形式调用时,this是调用方法的对象 p.test(): p
- 以构造函数的形式调用时,this是新建的那个对象 new test(): 新创建的对象
- 使用call和apply调用时,this是指定的那个对象 p.call(obj): obj
- 在全局作用域中this代表window
函数调用方式 | 示例 | this 指向 |
---|---|---|
直接调用 | fn() window.fn() | window |
通过对象调用 | obj.fn() | 调用它的对象 |
通过 new 调用 | new Fn() | 实例对象 |
call() apply() | fn.call(thisArg) fn.apply(thisArg) | call() apply() 的第一个参数 |
(3)代码举例详解
function Person(color) {
console.log(this)
this.color = color;
this.getColor = function () {
console.log(this)
return this.color;
};
this.setColor = function (color) {
console.log(this)
this.color = color;
};
}
Person("red"); //this是谁? window
const p = new Person("yello"); //this是谁? p
p.getColor(); //this是谁? p
const obj = {};
//调用call会改变this指向-->让我的p函数成为`obj`的临时方法进行调用
p.setColor.call(obj, "black"); //this是谁? obj
const test = p.setColor;
test(); //this是谁? window -->因为直接调用了
function fun1() {
function fun2() { console.log(this); }
fun2(); //this是谁? window
}
fun1();//调用fun1
12.回调函数
(1)什么函数才是回调函数?
- 你定义的
- 你没有调
- 但最终它执行了(在某个时刻或某个条件下)
(2)常见的回调函数?
- dom事件回调函数 ==>发生事件的dom元素
- 定时器回调函数 ===>window
- ajax请求回调函数(后面讲)
- 生命周期回调函数(后面讲)
// dom事件回调函数
document.getElementById('btn').onclick = function () {alert(this.innerHTML)}
// 定时器回调函数
setTimeout(function () { alert('到点了'+this)}, 2000)
13.闭包
(1)产生闭包的条件
-
一个函数嵌套在另一个函数中;
-
内部函数访问了外部函数中定义的变量;
-
每调用一次外部函数,就会产生一个闭包。
(2)闭包的生命周期
- 产生:外部函数执行完。
- 死亡:存放闭包的变量变成垃圾对象(赋值为 null)。
(3)常见的闭包
- 将内部函数作为外部函数的返回值。
function fn1() {
var a = 2
function fn2() {
return a++
}
return fn2
}
var f = fn1() // 调用外部函数,产生一个闭包,并赋值给一个变量存放该闭包
- 将内部函数作为实参传递给另一个函数,在外部函数中调用。
function fn(a, time) {
setTimeout(() => {
alert(a)
}, time)
}
fn("延时输出", 2000) // 调用外部函数,产生一个闭包
(4)闭包的作用
-
外部函数执行完后,自由变量仍会存在于闭包中,延长了生命周期。
-
通过闭包可以操作自由变量,该操作由内部函数决定
十、构造函数
1.定义
构造函数是专门用来创建对象的函数
一个构造函数我们也可以称为一个类
- 通过一个构造函数创建的对象,我们称该对象是这个构造函数的实例
- 通过同一个构造函数创建的对象,我们称为一类对象
- 构造函数就是一个普通的函数,只是他的调用方式不同,
- 如果直接调用,它就是一个普通函数
- 如果使用new来调用,则它就是一个构造函数
例子:
function Person(name , age , gender){
this.name = name;
this.age = age;
this.gender = gender;
this.sayName = function(){
alert(this.name);
};
}
2.构造函数的执行流程:
(1)创建一个新的对象
(2)将新的对象作为函数的上下文对象(this)
(3)执行函数中的代码
(4)将新建的对象返回
3.instanceof 关键字
- 用来检查一个对象是否是一个类的实例
- 判断一个对象的原型链中,是否存在某个构造函数
- 也就是判断一个对象是否是某个构造函数实例化的结果,或者这个对象对应的构造函数是否与某个构造函数存在继承关系。
- 语法:对象 instanceof 构造函数
- 如果该对象是构造函数的实例,则返回true,否则返回false
- Object是所有对象的祖先,所以任何对象和Object做instanceof都会返回true
const obj = {}
const arr = []
function Fn() {}
obj instanceof Fn // => false
obj instanceof Function // => false
obj instanceof Object // => true
arr instanceof Array // => true
4.for…in
枚举对象中的属性
语法:
for(var 属性名 in 对象){
}
for…in语句的循环体会执行多次,对象中有几个属性就会执行几次,
每次将一个属性名赋值给定义的变量,我们可以通过它来获取对象中的属性
十一、原型(prototype)
1.函数的prototype
属性
(1)创建一个函数以后,解析器都会默认在函数中添加一个数prototype
- prototype属性指向的是一个对象,这个对象我们称为原型对象。
- 当函数作为构造函数使用,它所创建的对象中都会有一个隐含的属性constructor指向该原型对象。这个隐含的属性可以通过对象.__proto__来访问。
-
每个函数function都有一个
prototype
,即显式
原型(属性) -
每个实例对象都有一个[
__ proto __
],可称为隐式
原型(属性) -
对象的隐式原型的值为其对应构造函数的显式原型的值
- 函数的[
prototype
]属性: 在定义函数时自动添加的, 默认值是一个空Object对象 - 对象的[
__ proto __
]属性: 创建对象时自动添加的,默认值为构造函数的prototype属性值
- 程序员能直接操作显式原型, 但不能直接操作隐式原型(ES6之前)
(2)给原型对象添加属性(一般都是方法
)
- 作用: 函数的所有实例对象自动拥有原型中的属性(方法)
// 每个函数都有一个prototype属性, 它默认指向一个Object空对象(即称为: 原型对象)
console.log(Date.prototype, typeof Date.prototype)
function Fun () { }
console.log(Fun.prototype) // 默认指向一个Object空对象(没有我们的属性)
// 原型对象中有一个属性constructor, 它指向函数对象
console.log(Date.prototype.constructor===Date)
console.log(Fun.prototype.constructor===Fun)
//给原型对象添加属性(一般是方法) ===>实例对象可以访问
Fun.prototype.test = function () { console.log('test()') }
var fun = new Fun()
fun.test()
//定义构造函数
function Fn() {
// 内部默认执行语句: this.prototype = {}
}
// 1. 每个函数function都有一个prototype,即显式原型属性, 默认指向一个空的Object对象
console.log(Fn.prototype)
// 2. 每个实例对象都有一个__proto__,可称为隐式原型
//创建实例对象
var fn = new Fn() // 内部默认执行语句: this.__proto__ = Fn.prototype
console.log(fn.__proto__)
// 3. 对象的隐式原型的值为其对应构造函数的显式原型的值
console.log(Fn.prototype===fn.__proto__) // true
//给原型添加方法
Fn.prototype.test = function () {
console.log('test()')
}
//通过实例调用原型的方法
fn.test()
3.原型链
(1)原型链
- 访问一个对象的属性时,
- 先在自身属性中查找,找到返回
- 如果没有, 再沿着[
__ proto __
]这条链向上查找, 去原型对象中寻找,找到返回 - 如果没有找到,则去原型的原型中寻找,依此类推。直到找到Object的原型为止,Object的原型的原型为null,
- 如果最终没找到, 返回undefined
- 别名: 隐式原型链
- 作用: 查找对象的属性(方法)
(2)构造函数/原型/实例对象的关系
var o1 = new Object();
var o2 = {};
function Foo(){ }
//所有函数的[__ proto __]都是一样的
(3) 属性问题
-
读取对象的属性值时: 会自动到原型链中查找
-
设置对象的属性值时: 不会查找原型链, 如果当前对象中没有此属性, 直接添加此属性并设置其值
-
方法一般定义在原型中, 属性一般通过构造函数定义在对象本身上
function Fn() { }
Fn.prototype.a = 'xxx'
var fn1 = new Fn()
console.log(fn1.a, fn1) //xxx Fn{}
var fn2 = new Fn()
fn2.a = 'yyy'
console.log(fn1.a, fn2.a, fn2) //xxx yyy Fn{a: "yyy"}
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.setName = function (name) {
this.name = name
}
var p1 = new Person('Tom', 12)
p1.setName('Bob')
console.log(p1) //Person {name: "Bob", age: 12}
var p2 = new Person('Jack', 12)
p2.setName('Cat')
console.log(p2) //Person {name: "Cat", age: 12}
4.作用
原型对象就相当于一个公共的区域,凡是通过同一个构造函数创建的对象他们通常都可以访问到相同的原型对象。
我们可以将对象中共有的属性和方法统一添加到原型对象中,
这样我们只需要添加一次,就可以使所有的对象都可以使用。
5.判断属性
-
hasOwnProperty()
这个方法可以用来检查对象自身中是否含有某个属性
语法:对象.hasOwnProperty(“属性名”)
-
in 操作符
判断某个属性是否在一个对象及其原型(链)上。
const obj = { a: 1 } Object.prototype.b = 2 "a" in obj // => true "b" in obj // => true "c" in obj // => false
十二、toString方法
当我们直接在页面中打印一个对象时,事件上是输出的对象的toString()方法的返回值
如果我们希望在输出对象时不输出[object Object],可以为对象添加一个toString()方法
//修改Person原型的toString
Person.prototype.toString = function(){
return "Person[name="+this.name+",age="+this.age+",gender="+this.gender+"]";
};
十三、垃圾回收(GC)
程序运行过程中也会产生垃圾这些垃圾积攒过多以后,会导致程序运行的速度过慢,
所以我们需要一个垃圾回收的机制,来处理程序运行过程中产生垃圾
当一个对象没有任何的变量或属性对它进行引用,此时我们将永远无法操作该对象,
此时这种对象就是一个垃圾,这种对象过多会占用大量的内存空间,导致程序运行变慢,
所以这种垃圾必须进行清理。
在JS中拥有自动的垃圾回收机制,会自动将这些垃圾对象从内存中销毁,
我们不需要也不能进行垃圾回收的操作
我们需要做的只是要将不再使用的对象设置null即可
十四、数组(Array)
1.定义
数组也是一个对象,是一个用来存储数据的对象和Object类似,但是它的存储效率比普通对象要高
数组中保存的内容我们称为元素
数组使用索引(index)来操作元素
索引指由0开始的整数
2.数组的操作:
(1)创建数组
var arr = new Array();
var arr = [];
(2)向数组中添加元素
语法:数组对象[索引] = 值;
arr[0] = 123;
arr[1] = "hello";
(3)创建数组时直接添加元素
var arr = [元素1,元素2....元素N];
例子:
var arr = [123,"hello",true,null];
(4)获取和修改数组的长度
使用length属性来操作数组的长度
- 获取长度
数组.length
length获取到的是数组的最大索引+1
对于连续的数组,length获取到的就是数组中元素的个数
- 修改数组的长度
数组.length = 新长度
如果修改后的length大于原长度,则多出的部分会空出来
如果修改后的length小于原长度,则原数组中多出的元素会被删除
- 向数组的最后添加元素
数组[数组.length] = 值;
3.数组的方法
functionName | function | usage |
---|---|---|
push() | 用来向数组的末尾添加一个或多个元素,并返回数组新的长度 | 语法:数组.push(元素1,元素2,元素N)pop() |
pop() | 用来删除数组的最后一个元素,并返回被删除的元素 | |
unshift() | 向数组的开头添加一个或多个元素,并返回数组的新的长度 | |
shift() | 删除数组的开头的一个元素,并返回被删除的元素 | |
reverse() | 可以用来反转一个数组,它会对原数组产生影响 | |
concat() | 可以连接两个或多个数组,它不会影响原数组,而是新数组作为返回值返回 |
(1)slice(sart,[end])
可以从一个数组中截取指定的元素
该方法不会影响原数组,而是将截取到的内容封装为一个新的数组并返回
参数:
1.截取开始位置的索引(包括开始位置)
2.截取结束位置的索引(不包括结束位置)
第二个参数可以省略不写,如果不写则一直截取到最后
参数可以传递一个负值,如果是负值,则从后往前数
(2)splice()
可以用来删除数组中指定元素,并使用新的元素替换
该方法会将删除的元素封装到新数组中返回
参数:
1.删除开始位置的索引
2.删除的个数
3.三个以后,都是替换的元素,这些元素将会插入到开始位置索引的前边
(3)join([splitor])
可以将一个数组转换为一个字符串
参数:
需要一个字符串作为参数,这个字符串将会作为连接符来连接数组中的元素
如果不指定连接符则默认使用,
(4)sort()
可以对一个数组中的内容进行排序,默认是按照Unicode编码进行排序
调用以后,会直接修改原数组。
可以自己指定排序的规则,需要一个回调函数作为参数:
我们可以自己来指定排序的规则
我们可以在sort()添加一个回调函数,来指定排序规则,
回调函数中需要定义两个形参,
浏览器将会分别使用数组中的元素作为实参去调用回调函数
使用哪个元素调用不确定,但是肯定的是在数组中a一定在b前边
-
浏览器会根据回调函数的返回值来决定元素的顺序,
如果返回一个大于0的值,则元素会交换位置
如果返回一个小于0的值,则元素位置不变
如果返回一个0,则认为两个元素相等,也不交换位置 -
如果需要升序排列,则返回 a-b
如果需要降序排列,则返回b-a
function(a,b){
//升序排列
return a-b;
//降序排列
return b-a;
}
4.遍历数组
遍历数组就是将数组中元素都获取到
一般情况我们都是使用for循环来遍历数组
for(var i=0 ; i<数组.length ; i++){
//数组[i]
}
使用forEach()方法来遍历数组
数组.forEach(function(value , index , obj){
//value:正在遍历的元素
//index:正在遍历元素的索引
//obj:被遍历对象
});
forEach()方法需要一个回调函数作为参数,
数组中有几个元素,回调函数就会被调用几次,
每次调用时,都会将遍历到的信息以实参的形式传递进来,
我们可以定义形参来获取这些信息。
十五、常用类和方法
1.包装类
在JS中为我们提供了三个包装类:
String() Boolean() Number()
通过这三个包装类可以创建基本数据类型的对象
例子:
var num = new Number(2);
var str = new String("hello");
var bool = new Boolean(true);
但是在实际应用中千万不要这么干。
当我们去操作一个基本数据类型的属性和方法时,
解析器会临时将其转换为对应的包装类,然后再去操作属性和方法,
操作完成以后再将这个临时对象进行销毁。
2.Date
日期的对象,在JS中通过Date对象来表示一个时间
(1)创建对象
- 创建一个当前的时间对象
1
var d = new Date();
- 创建一个指定的时间对象
var d = new Date("月/日/年 时:分:秒");
(2)方法:
name | |
---|---|
getDate() | 当前日期对象是几日(1-31) |
getDay() | 返回当前日期对象时周几(0-6) 0 周日 1 周一 。。。 |
getMonth() | 返回当前日期对象的月份(0-11) 0 一月 1 二月 。。。 |
getFullYear() | 从 Date 对象以四位数字返回年份。 |
getHours() | 返回 Date 对象的小时 (0 ~ 23)。 |
getMinutes() | 返回 Date 对象的分钟 (0 ~ 59)。 |
getSeconds() | 返回 Date 对象的秒数 (0 ~ 59)。 |
getMilliseconds() | 返回 Date 对象的毫秒(0 ~ 999)。 |
getTime() | 返回当前日期对象的时间戳 时间戳,指的是从1970年月1日 0时0分0秒,到现在时间的毫秒数 计算机底层保存时间都是以时间戳的形式保存的。 |
Date.now() | 可以获取当前代码执行时的时间戳 |
3.Math
Math属于一个工具类,它不需要我们创建对象,它里边封装了属性运算相关的常量和方法
我们可以直接使用它来进行数学运算相关的操作
方法:
- Math.PI常量,圆周率
- Math.abs()绝对值运算
- Math.ceil()向上取整
- Math.floor()向下取整
- Math.round()四舍五入取整
- Math.random()生成一个01之间的随机数
- Math.round(Math.random()*(y-x)+x);生成一个xy之间的随机数
- Math.pow(x,y)求x的y次幂
- Math.sqrt()对一个数进行开方
- Math.max()求多个数中最大值
- Math.min()求多个数中的最小值
4.字符串的相关的方法
使用ES6中的字符串新方法
- String.prototype.padStart(maxLength, fillString=’’) 或 **String.prototype.padEnd(maxLength, fillString=’’)**来填充字符串;
- length获取字符串的长度
- charAt()根据索引获取指定的字符
- charCodeAt()根据索引获取指定的字符编码
- String.fromCharCode()根据字符编码获取字符
- indexOf();lastIndexOf()
- 从一个字符串中检索指定内容,需要一个字符串作为参数,这个字符串就是要检索的内容,如果找到该内容,则会返回其第一次出现的索引,如果没有找到则返回-1。可以指定一个第二个参数,来表示开始查找的位置
- indexOf()是从前向后找
- lastIndexOf()是从后向前找
- slice(start,[end])
- 可以从一个字符串中截取指定的内容,并将截取到内容返回,不会影响原变量
- 参数:
- 第一个:截取开始的位置(包括开始)
- 第二个:截取结束的位置(不包括结束)
- 可以省略第二个参数,如果省略则一直截取到最后
- 可以传负数,如果是负数则从后往前数
- substr()和slice()基本一致,不同的是它第二个参数不是索引,而是截取的数量
- substring()和slice()基本一致,不同的是它不能接受负值作为参数,如果设置一个负值,则会自动修正为0,
- substring()中如果第二个参数小于第一个,自动调整位置
- toLowerCase()将字符串转换为小写并返回
- toUpperCase()将字符串转换为大写并返回
5.正则表达相关方法
(1)split()
根据指定内容将一个字符串拆分为一个数组
参数:
需要一个字符串作为参数,将会根据字符串去拆分数组
可以接收一个正则表达式,此时会根据正则表达式去拆分数组
(3)match()
可以根据正则表达式,从一个字符串中将符合条件的内容提取出来
默认情况下我们的match只会找到第一个符合要求的内容,找到以后就停止检索
我们可以设置正则表达式为全局匹配模式,这样就会匹配到所有的内容
可以为一个正则表达式设置多个匹配模式,且顺序无所谓
match()会将匹配到的内容封装到一个数组中返回,即使只查询到一个结果
(4)replace()
可以将字符串中指定内容替换为新的内容
参数:
- 被替换的内容,可以接受一个正则表达式作为参数
- 新的内容 空串则为删除””
默认只会替换第一个
(5)search()
可以搜索字符串中是否含有指定内容
- 如果搜索到指定内容,则会返回第一次出现的索引,如果没有搜索到返回1
- 它可以接受一个正则表达式作为参数,然后会根据正则表达式去检索字符串
- serach()只会查找第一个,即使设置全局匹配也没用
十六、正则表达式
正则用来定义一些字符串的规则,程序可以根据这些规则来判断一个字符串是否符合规则,
也可以将一个字符串中符合规则的内容提取出来。
1.创建正则表达式
var reg = new RegExp(“正则”,”匹配模式”);
注意:使用构造函数时,由于它的参数是一个字符串,而\是字符串中转义字符,如果要使用\则需要使用\来代替
var reg = /正则表达式/匹配模式 (匹配模式可以多个一起写:/gi)
2.语法:
匹配模式:
- i:忽略大小写(ignore)
- g:全局匹配模式(默认为1次)
设置匹配模式时,可以都不设置,也可以设置1个,也可以全设置,设置时没有顺序要求
正则语法
- | 或
- [] 或
- [^ ] 除了
**[x-y] x的ascii到y的ascii码之间的值**
- [a-z] 小写字母 (也可以[e-i])
- [A-Z] 大写字母
- [A-z] 任意字母,但是还包括了其他ASCII在此之中的
- [0-9] 任意数字
元符号
检查一个字符串中是否含有 .
. 表示任意字符
在正则表达式中使用\作为转义字符
. 来表示.
\ 表示\
\w
任意字母、数字、_ [A-z0-9_]
\W
除了字母、数字、_ [ ^A-z0-9_]
\d
任意的数字 [0-9]
\D
除了数字 [ ^0-9]
\s
空格
\S
除了空格
\b
单词边界
\B
除了单词边界
量词
通过量词可以设置一个内容出现的次数
量词只对它前边的一个内容起作用
{n} 正好出现n次
{m,n} 出现mn次
{m,} m次以上
+至少一个,相当于{1,}
*个或多个,相当于{0,}
? 0个或1个,相当于{0,1}
边界表达式(不要在java中用,javaScript中用)
^:正则开始
$:正则结束 :注意结束前一个才是结束匹配
1 2 | reg = /^a/; reg = /b$/; |
3.方法
test()
可以用来检查一个字符串是否符合正则表达式
如果符合返回true,否则返回false
例子
去掉两端的空格:
1 2 | var s = " f afa "; s = s.replace(/^\s*|\s*$/g,""); |
十七 、DOM
Document Object Model
文档对象模型,通过DOM可以来任意来修改网页中各个内容
1.文档
文档指的是网页,一个网页就是一个文档
2.对象
对象指将网页中的每一个节点都转换为对象
转换完对象以后,就可以以一种纯面向对象的形式来操作网页了
3.模型
模型用来表示节点和节点之间的关系,方便操作页面
4.节点(Node)
节点是构成网页的最基本的单元,网页中的每一个部分都可以称为是一个节点
虽然都是节点,但是节点的类型却是不同的
5.常用的节点
(1)文档节点 (Document),代表整个网页
(2)元素节点(Element),代表网页中的标签
(3)属性节点(Attribute),代表标签中的属性
(4)文本节点(Text),代表网页中的文本内容
十八、DOM操作
1.DOM查询
在网页中浏览器已经为我们提供了document对象,
它代表的是整个网页,它是window对象的属性,可以在页面中直接使用。
document查询方法:
(1)根据元素的id属性查询一个元素节点对象:
document.getElementById(“id属性值”);
(2)根据元素的name属性值查询一组元素节点对象:
document.getElementsByName(“name属性值”);
(3)根据标签名来查询一组元素节点对象:
document.getElementsByTagName(“标签名”);
(4)通过标签名查询当前元素的指定后代元素
子节点包括便签元素中的文本,子元素自包含标签元素
- 元素.childNodes
获取当前元素的所有子节点
会获取到空白的文本子节点
childNodes属性会获取包括文本节点在的所有节点
根据DOM标签标签间空白也会当成文本节点
注意:在IE8及以下的浏览器中,不会将空白文本当成子节点,
所以该属性在IE8中会返回4个子元素而其他浏览器是9个
- 元素.children
获取当前元素的所有子元素
- 元素.firstChild
获取当前元素的第一个子节点,会获取到空白的文本子节点
- 元素.lastChild
获取当前元素的最后一个子节点
- 元素.parentNode
获取当前元素的父元素
- 元素.previousSibling
获取当前元素的前一个兄弟节点
previousElementSibling获取前一个兄弟元素,IE8及以下不支持
- 元素.nextSibling
获取当前元素的后一个兄弟节点
- firstElementChild获取当前元素的第一个子元素
firstElementChild不支持IE8及以下的浏览器,
如果需要兼容他们尽量不要使用
2.元素的属性:
(1)读取元素的属性:
语法:元素.属性名
例子:ele.name
ele.id
ele.value
ele.className
注意:class属性不能采用这种方式,
读取class属性时需要使用 元素.className
(2)修改元素的属性:
语法:元素.属性名 = 属性值
(3)innerHTML和innerText
这两个属性并没有在DOM标准定义,但是大部分浏览器都支持这两个属性
两个属性作用类似,都可以获取到标签内部的内容,
不同是innerHTML会获取到html标签,而innerText会自动去除标签
如果使用这两个属性来设置标签内部的内容时,没有任何区别的
(4)读取标签内部的文本内容
h1中的文本内容
元素.firstChild.nodeValue
3.document对象的其他的属性和方法
(1)document.all
获取页面中的所有元素,相当于document.getElementsByTagName(“*”);
(2)document.documentElement
获取页面中html根元素
(3)document.body
获取页面中的body元素
(4)document.getElementsByClassName()
根据元素的class属性值查询一组元素节点对象
这个方法不支持IE8及以下的浏览器
(5)document.querySelector()
根据CSS选择器去页面中查询一个元素
如果匹配到的元素有多个,则它会返回查询到的第一个元素
(6)document.querySelectorAll()
根据CSS选择器去页面中查询一组元素
会将匹配到所有元素封装到一个数组中返回,即使只匹配到一个
4.DOM修改
(1)document.createElement(“TagName”)
可以用于创建一个元素节点对象,
它需要一个标签名作为参数,将会根据该标签名创建元素节点对象,
并将创建好的对象作为返回值返回
(2)document.createTextNode(“textContent”)
可以根据文本内容创建一个文本节点对象
(3)父节点.appendChild(子节点)
向父节点中添加指定的子节点
(4)父节点.insertBefore(新节点,旧节点)
将一个新的节点插入到旧节点的前边
父节点.replaceChild(新节点,旧节点)
使用一个新的节点去替换旧节点
(5)父节点.removeChild(子节点)
删除指定的子节点
推荐方式:子节点.parentNode.removeChild(子节点)
以上方法,实际就是改变了相应元素(标签)的innerHTML的值。
myClick("btn07",function(){
//向city中添加广州
var city = document.getElementById("city");
/*
* 使用innerHTML也可以完成DOM的增删改的相关操作
* 一般我们会两种方式结合使用
*/
//city.innerHTML += "<li>广州</li>";
//创建一个li
var li = document.createElement("li");
//向li中设置文本
li.innerHTML = "广州";
//将li添加到city中
city.appendChild(li);
});
5.DOM对CSS的操作
(1)读取和修改内联样式
使用style属性来操作元素的内联样式
- 读取内联样式:
语法:元素.style.样式名
例子:
元素.style.width
元素.style.height
注意:如果样式名中带有-,则需要将样式名修改为驼峰命名法将-去掉,然后后的字母改大写
比如:backgroundcolor > backgroundColor
borderwidth > borderWidth
- 修改内联样式:
语法:元素.style.样式名 = 样式值
通过style修改和读取的样式都是内联样式,由于内联样式的优先级比较高,
所以我们通过JS来修改的样式,往往会立即生效,
但是如果样式中设置了!important,则内联样式将不会生效。
(2)读取元素的当前样式
正常浏览器
使用getComputedStyle()
这个方法是window对象的方法,可以返回一个对象,这个对象中保存着当前元素生效样式
参数:
1.要获取样式的元素
2.可以传递一个伪元素,一般传null
例子:
获取元素的宽度
getComputedStyle(box , null)[“width”];
通过该方法读取到样式都是只读的不能修改
IE8
使用currentStyle
语法:
元素.currentStyle.样式名
例子:
box.currentStyle[“width”]
通过这个属性读取到的样式是只读的不能修改
实现兼容性
//对象.属性不存在,不会报错,如果直接寻找对象,(当前作用域到全局作用域)找不到会报错
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | /* * 定义一个函数,用来获取指定元素的当前的样式 * 参数: * obj 要获取样式的元素 * name 要获取的样式名 */ function getStyle(obj , name){ //对象.属性不存在,不会报错,如果直接寻找对象,(当前作用域到全局作用域)找不到会报错 if(window.getComputedStyle){ //正常浏览器的方式,具有getComputedStyle()方法 return getComputedStyle(obj , null)[name]; }else{ //IE8的方式,没有getComputedStyle()方法 return obj.currentStyle[name]; } //return window.getComputedStyle?getComputedStyle(obj , null)[name]:obj.currentStyle[name]; } |
(3)其他的样式相关的属性
注意:以下样式都是只读的,未指明偏移量都是相对于当前窗口左上角
- clientHeight元素的可见高度,包括元素的内容区和内边距的高度
- clientWidth元素的可见宽度,包括元素的内容区和内边距的宽度
- offsetHeight整个元素的高度,包括内容区、内边距、边框
- offfsetWidth整个元素的宽度,包括内容区、内边距、边框
- offsetParent当前元素的定位父元素离他最近的开启了定位的祖先元素,如果所有的元素都没有开启定位,则返回body
- offsetLeft
- offsetTop
- 当前元素和定位父元素之间的偏移量
- offsetLeft水平偏移量
- offsetTop垂直偏移量
- scrollHeight
- scrollWidth获取元素滚动区域的高度和宽度
- scrollTop
- scrollLeft
- 获取元素垂直和水平滚动条滚动的距离
- 判断滚动条是否滚动到底
- 垂直滚动条
- scrollHeight -scrollTop = clientHeight
- 水平滚动
- scrollWidth -scrollLeft = clientWidth
十九、事件(Event)
1.事件对象
事件指的是用户和浏览器之间的交互行为。比如:点击按钮、关闭窗口、鼠标移动。。。
我们可以为事件来绑定回调函数来响应事件。
(1)绑定事件的方式
- 在标签的事件属性中设置相应的JS代码
例子:
<button onclick="js代码。。。">按钮</button>
- 通过为对象的指定事件属性设置回调函数的形式来处理事件
例子:
<button id="btn">按钮</button>
<script>
var btn = document.getElementById("btn");
btn.onclick = function(){
};
</script>
(2)文档的加载
浏览器在加载一个页面时,是按照自上向下的顺序加载的,加载一行执行一行。
如果将js代码编写到页面的上边,当代码执行时,页面中的DOM对象还没有加载,
此时将会无法正常获取到DOM对象,导致DOM操作失败。
- 解决方式一:将js代码编写到body的下边
<body>
<button id="btn">按钮</button>
<script>
var btn = document.getElementById("btn");
btn.onclick = function(){
};
</script>
</body>
- 解决方式二:将js代码编写到window.onload = function(){}中
window.onload 对应的回调函数会在整个页面加载完毕以后才执行,
所以可以确保代码执行时,DOM对象已经加载完毕了
<script>
window.onload = function(){
var btn = document.getElementById("btn");
btn.onclick = function(){
};
};
</script>
2.事件的冒泡(Bubble)
事件的冒泡指的是事件向上传导,当后代元素上的事件被触发时,将会导致其祖先元素上的同类事件也会触发。
事件的冒泡大部分情况下都是有益的,如果需要取消冒泡,则需要使用事件对象来取消
可以将事件对象的cancelBubble设置为true,即可取消冒泡
例子:
元素.事件 = function(event){
event = event || window.event;
event.cancelBubble = true;
};
3.事件的委派
指将事件统一绑定给元素的共同的祖先元素,这样当后代元素上的事件触发时,会一直冒泡到祖先元素,从而通过祖先元素的响应函数来处理事件。
事件委派是利用了冒泡,通过委派可以减少事件绑定的次数,提高程序的性能
我们希望,只绑定一次事件,即可应用到多个的元素上,即使元素是后添加的
我们可以尝试将其绑定给元素的共同的祖先元素
target : event中的target表示的触发事件的对象
4.事件的绑定
(1)addEventListener()
通过这个方法也可以为元素绑定响应函数
参数:
- 事件的字符串,不要on
- 回调函数,当事件触发时该函数会被调用
- 是否在捕获阶段触发事件,需要一个布尔值,一般都传false
使用addEventListener()可以同时为一个元素的相同事件同时绑定多个响应函数,
这样当事件被触发时,响应函数将会按照函数的绑定顺序执行
这个方法不支持IE8及以下的浏览器
btn01.addEventListener("click",function(){
alert(1);
},false);
btn01.addEventListener("click",function(){
alert(2);
},false);
(2)attachEvent()
在IE8中可以使用attachEvent()来绑定事件
参数:
- 事件的字符串,要on
- 回调函数
这个方法也可以同时为一个事件绑定多个处理函数,
不同的是它是后绑定先执行,执行顺序和addEventListener()相反
btn01.attachEvent("onclick",function(){
alert(1);
});
btn01.attachEvent("onclick",function(){
alert(2);
});
//定义一个函数,用来为指定元素绑定响应函数
/*
* addEventListener()中的this,是绑定事件的对象
* attachEvent()中的this,是window
* 需要统一两个方法this
*/
/*
* 参数:
* obj 要绑定事件的对象
* eventStr 事件的字符串(不要on)
* callback 回调函数
*/
function bind(obj , eventStr , callback){
if(obj.addEventListener){
//大部分浏览器兼容的方式
obj.addEventListener(eventStr , callback , false);
}else{
/*
* this是谁由调用方式决定
* callback.call(obj)
*/
//IE8及以下
obj.attachEvent("on"+eventStr , function(){
//在匿名函数中调用回调函数
callback.call(obj);
});
}
}
5.事件的传播
关于事件的传播网景公司和微软公司有不同的理解
微软公司认为事件应该是由内向外传播,也就是当事件触发时,应该先触发当前元素上的事件,
然后再向当前元素的祖先元素上传播,也就说事件应该在冒泡阶段执行。
网景公司认为事件应该是由外向内传播的,也就是当前事件触发时,应该先触发当前元素的最外层的祖先元素的事件,
然后在向内传播给后代元素
W3C综合了两个公司的方案,将事件传播分成了三个阶段
(1)捕获阶段
在捕获阶段时从最外层的祖先元素,向目标元素进行事件的捕获,但是默认此时不会触发事件
(2)目标阶段
事件捕获到目标元素,捕获结束开始在目标元素上触发事件
(3)冒泡阶段
事件从目标元素向他的祖先元素传递,依次触发祖先元素上的事件
如果希望在捕获阶段就触发事件,可以将addEventListener()的第三个参数设置为true
一般情况下我们不会希望在捕获阶段触发事件,所以这个参数一般都是false
IE8及以下的浏览器中没有捕获阶段
6.常用事件
(1)鼠标事件
- 拖拽事件
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
<style type="text/css">
#box1{
width: 100px;
height: 100px;
background-color: red;
position: absolute;
}
#box2{
width: 100px;
height: 100px;
background-color: yellow;
position: absolute;
left: 200px;
top: 200px;
}
</style>
<script type="text/javascript">
window.onload = function(){
/*
* 拖拽box1元素
* - 拖拽的流程
* 1.当鼠标在被拖拽元素上按下时,开始拖拽 onmousedown
* 2.当鼠标移动时被拖拽元素跟随鼠标移动 onmousemove
* 3.当鼠标松开时,被拖拽元素固定在当前位置 onmouseup
*/
//获取box1
var box1 = document.getElementById("box1");
var box2 = document.getElementById("box2");
var img1 = document.getElementById("img1");
//开启box1的拖拽
drag(box1);
//开启box2的
drag(box2);
drag(img1);
};
/*
* 提取一个专门用来设置拖拽的函数
* 参数:开启拖拽的元素
*/
function drag(obj){
//当鼠标在被拖拽元素上按下时,开始拖拽 onmousedown
obj.onmousedown = function(event){
//设置box1捕获所有鼠标按下的事件
/*
* setCapture()
* - 只有IE支持,但是在火狐中调用时不会报错,
* 而如果使用chrome调用,会报错
*/
/*if(box1.setCapture){
box1.setCapture();
}*/
obj.setCapture && obj.setCapture();
event = event || window.event;
//div的偏移量 鼠标.clentX - 元素.offsetLeft
//div的偏移量 鼠标.clentY - 元素.offsetTop
var ol = event.clientX - obj.offsetLeft;
var ot = event.clientY - obj.offsetTop;
//为document绑定一个onmousemove事件
document.onmousemove = function(event){
event = event || window.event;
//当鼠标移动时被拖拽元素跟随鼠标移动 onmousemove
//获取鼠标的坐标
var left = event.clientX - ol;
var top = event.clientY - ot;
//修改box1的位置
obj.style.left = left+"px";
obj.style.top = top+"px";
};
//为document绑定一个鼠标松开事件
document.onmouseup = function(){
//当鼠标松开时,被拖拽元素固定在当前位置 onmouseup
//取消document的onmousemove事件
document.onmousemove = null;
//取消document的onmouseup事件
document.onmouseup = null;
//当鼠标松开时,取消对事件的捕获
obj.releaseCapture && obj.releaseCapture();
};
/*
* 当我们拖拽一个网页中的内容时,浏览器会默认去搜索引擎中搜索内容,
* 此时会导致拖拽功能的异常,这个是浏览器提供的默认行为,
* 如果不希望发生这个行为,则可以通过return false来取消默认行为
*
* 但是这招对IE8不起作用
*/
return false;
};
}
</script>
</head>
<body>
我是一段文字
<div id="box1"></div>
<div id="box2"></div>
<img src="img/an.jpg" id="img1" style="position: absolute;"/>
</body>
</html>
- 滚轮事件:
onwheel都支持
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
<style type="text/css">
#box1{
width: 100px;
height: 100px;
background-color: red;
}
</style>
<script type="text/javascript">
window.onload = function(){
//获取id为box1的div
var box1 = document.getElementById("box1");
//为box1绑定一个鼠标滚轮滚动的事件
/*
* onmousewheel鼠标滚轮滚动的事件,会在滚轮滚动时触发,
* 但是火狐不支持该属性
*
* 在火狐中需要使用 DOMMouseScroll 来绑定滚动事件
* 注意该事件需要通过addEventListener()函数来绑定
*/
box1.onmousewheel = function(event){
event = event || window.event;
//event.wheelDelta 可以获取鼠标滚轮滚动的方向
//向上滚 120 向下滚 -120
//wheelDelta这个值我们不看大小,只看正负
//alert(event.wheelDelta);
//wheelDelta这个属性火狐中不支持
//在火狐中使用event.detail来获取滚动的方向
//向上滚 -3 向下滚 3
//alert(event.detail);
/*
* 当鼠标滚轮向下滚动时,box1变长
* 当滚轮向上滚动时,box1变短
*/
//判断鼠标滚轮滚动的方向
if(event.wheelDelta > 0 || event.detail < 0){
//向上滚,box1变短
box1.style.height = box1.clientHeight - 10 + "px";
}else{
//向下滚,box1变长
box1.style.height = box1.clientHeight + 10 + "px";
}
/*
* 使用addEventListener()方法绑定响应函数,取消默认行为时不能使用return false
* 需要使用event来取消默认行为event.preventDefault();
* 但是IE8不支持event.preventDefault();这个玩意,如果直接调用会报错
*/
event.preventDefault && event.preventDefault();
/*
* 当滚轮滚动时,如果浏览器有滚动条,滚动条会随之滚动,
* 这是浏览器的默认行为,如果不希望发生,则可以取消默认行为
*/
return false;
};
//为火狐绑定滚轮事件
bind(box1,"DOMMouseScroll",box1.onmousewheel);
};
function bind(obj , eventStr , callback){
if(obj.addEventListener){
//大部分浏览器兼容的方式
obj.addEventListener(eventStr , callback , false);
}else{
/*
* this是谁由调用方式决定
* callback.call(obj)
*/
//IE8及以下
obj.attachEvent("on"+eventStr , function(){
//在匿名函数中调用回调函数
callback.call(obj);
});
}
}
</script>
</head>
<body style="height: 2000px;">
<div id="box1"></div>
</body>
</html>
(2)键盘事件
- onkeydown按键被按下
- 对于onkeydown来说如果一直按着某个按键不松手,则事件会一直触发
- 当onkeydown连续触发时,第一次和第二次之间会间隔稍微长一点,其他的会非常的快,这种设计是为了防止误操作的发生。
- onkeyup按键被松开
- 键盘事件一般都会绑定给一些可以获取到焦点的对象或者是document
- keyCode可以通过keyCode来获取按键的编码通过它可以判断哪个按键被按下
- 除了keyCode,事件对象中还提供了几个属性,这个三个用来判断alt ctrl 和 shift是否被按下,如果按下则返回true,否则返回false
- altKey
- ctrlKey
- shiftKey
input.onkeydown = function(event) {
event = event || window.event;
//数字 48 - 57
//使文本框中不能输入数字
if(event.keyCode >= 48 && event.keyCode <= 57) {
//在文本框中输入内容,属于onkeydown的默认行为
//如果在onkeydown中取消了默认行为,则输入的内容,不会出现在文本框中
return false;
}
};
二十、BOM
浏览器对象模型(browser object model)
BOM可以使我们通过JS来操作浏览器
在BOM中为我们提供了一组对象,用来完成对浏览器的操作
BOM对象
1.Window
代表的是整个浏览器的窗口,同时window也是网页中的全局对象
2.Navigator
代表的当前浏览器的信息,通过该对象可以来识别不同的浏览器
3.Location
代表当前浏览器的地址栏信息,通过Location可以获取地址栏信息,或者操作浏览器跳转页面
4.History
代表浏览器的历史记录,可以通过该对象来操作浏览器的历史记录
由于隐私原因,该对象不能获取到具体的历史记录,只能操作浏览器向前或向后翻页
而且该操作只在当次访问时有效
- length属性,可以获取到当成访问的链接数量
- back()可以用来回退到上一个页面,作用和浏览器的回退按钮一样
- forward()可以跳转下一个页面,作用和浏览器的前进按钮一样
- go()可以用来跳转到指定的页面
- 它需要一个整数作为参数
- 1:表示向前跳转一个页面 相当于forward()
- 2:表示向前跳转两个页面
- -1:表示向后跳转一个页面
- -2:表示向后跳转两个页面
5.Screen
代表用户的屏幕的信息,通过该对象可以获取到用户的显示器的相关的信息
这些BOM对象在浏览器中都是作为window对象的属性保存的,
可以通过window对象来使用,也可以直接使用
6.Navigator
代表的当前浏览器的信息,通过该对象可以来识别不同的浏览器
由于历史原因,Navigator对象中的大部分属性都已经不能帮助我们识别浏览器了
一般我们只会使用userAgent来判断浏览器的信息,
userAgent是一个字符串,这个字符串中包含有用来描述浏览器信息的内容,
不同的浏览器会有不同的userAgent
- 火狐的userAgent--Mozilla5.0 (Windows NT 6.1; WOW64; rv:50.0) Gecko20100101 Firefox50.0
- Chrome的userAgent--Mozilla5.0 (Windows NT 6.1; Win64; x64) AppleWebKit537.36 (KHTML, like Gecko) Chrome52.0.2743.82 Safari537.36
- IE8--Mozilla4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident7.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E)
- IE9--Mozilla5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident7.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E)
- IE10--Mozilla5.0 (compatible; MSIE 10.0; Windows NT 6.1; WOW64; Trident7.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E)
- IE11--Mozilla5.0 (Windows NT 6.1; WOW64; Trident7.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; rv:11.0) like Gecko
- 在IE11中已经将微软和IE相关的标识都已经去除了,所以我们基本已经不能通过UserAgent来识别一个浏览器是否是IE了
7.Location
该对象中封装了浏览器的地址栏的信息
如果直接打印location,则可以获取到地址栏的信息(当前页面的完整路径)
(1)alert(location);
如果直接将location属性修改为一个完整的路径,或相对路径
则我们页面会自动跳转到该路径,并且会生成相应的历史记录
location = “http:www.baidu.com“;
location = “01.BOM.html”;
(2)assign()
用来跳转到其他的页面,作用和直接修改location一样
(3)reload()
用于重新加载当前页面,作用和刷新按钮一样
如果在方法中传递一个true,作为参数,则会强制清空缓存刷新页面
location.reload(true);
(4)replace()
可以使用一个新的页面替换当前页面,调用完毕也会跳转页面
不会生成历史记录,不能使用回退按钮回退
二十一、window
1.定时器
(1)setInterval()
定时调用
可以将一个函数,每隔一段时间执行一次
- 参数:
1.回调函数,该函数会每隔一段时间被调用一次
2.每次调用间隔的时间,单位是毫秒
- 返回值:
返回一个Number类型的数据
这个数字用来作为定时器的唯一标识
clearInterval()可以用来关闭一个定时器
方法中需要一个定时器的标识作为参数,这样将关闭标识对应的定时器
(2)clearInterval()
可以接收任意参数,如果参数是一个有效的定时器的标识,则停止对应的定时器
如果参数不是一个有效的标识,则什么也不做
var num = 1;
var timer = setInterval(function() {
count.innerHTML = num++;
if(num == 11) {
//关闭定时器
clearInterval(timer);
}
}, 1000);
2.延时调用
(1)setTimeout
延时调用一个函数不马上执行,而是隔一段时间以后在执行,而且只会执行一次
延时调用和定时调用的区别,定时调用会执行多次,而延时调用只会执行一次
延时调用和定时调用实际上是可以互相代替的,在开发中可以根据自己需要去选择
var timer = setTimeout(function(){
console.log(num++);
},3000);
(2)clearTimeout(timer);
使用clearTimeout()来关闭一个延时调用
二十二、类的操作
直接修改元素的类css:
通过style属性来修改元素的样式,每修改一个样式,浏览器就需要重新渲染一次页面。 这样的执行的性能是比较差的,而且这种形式当我们要修改多个样式时,也不太方便 我希望一行代码,可以同时修改多个样式
我们可以通过修改元素的class属性来间接的修改样式.这样一来,我们只需要修改一次,即可同时修改多个样式,浏览器只需要重新渲染页面一次,性能比较好,
并且这种方式,可以使表现和行为进一步的分离
box.className += " b2"; //注意有空格,添加class属性
//定义一个函数,用来向一个元素中添加指定的class属性值
/*
* 参数:
* obj 要添加class属性的元素
* cn 要添加的class值
*
*/
function addClass(obj, cn) {
if (!hasClass(obj, cn)) {
obj.className += " " + cn;
}
}
/*
* 判断一个元素中是否含有指定的class属性值
* 如果有该class,则返回true,没有则返回false
*
*/
function hasClass(obj, cn) {
var reg = new RegExp("\\b" + cn + "\\b");
return reg.test(obj.className);
}
/*
* 删除一个元素中的指定的class属性
*/
function removeClass(obj, cn) {
//创建一个正则表达式
var reg = new RegExp("\\b" + cn + "\\b");
//删除class
obj.className = obj.className.replace(reg, "");
}
/*
* toggleClass可以用来切换一个类
* 如果元素中具有该类,则删除
* 如果元素中没有该类,则添加
*/
function toggleClass(obj , cn){
//判断obj中是否含有cn
if(hasClass(obj , cn)){
//有,则删除
removeClass(obj , cn);
}else{
//没有,则添加
addClass(obj , cn);
}
}
二十三、JSON
JavaScript Object Notation JS对象表示法
1.JSON 格式
(1)复合类型的值只能是数组或对象,不能是函数、正则表达式对象、日期对象。
(2)原始类型的值只有四种:字符串、数值(必须以十进制表示)、布尔值和null
(不能使用NaN
, Infinity
, -Infinity
和undefined
)。
(3)字符串必须使用双引号表示,不能使用单引号。
(4)对象的键名必须放在双引号里面。
(5)数组或对象最后一个成员的后面,不能加逗号。
JS中的对象只有JS自己认识,其他的语言都不认识
JSON就是一个特殊格式的字符串,这个字符串可以被任意的语言所识别,
并且可以转换为任意语言中的对象,JSON在开发中主要用来数据的交互
JSON和JS对象的格式一样,只不过JSON字符串中的属性名必须加双引号
其他的和JS语法一致
2.JSON分类
(1)对象 {}
(2)数组 []
3.JSON中允许的值:
- 字符串
- 数值
- 布尔值
- null
- 对象
- 数组
举例:
var arr = '[1,2,3,"hello",true]';
var obj2 = '{"arr":[1,2,3]}';
var arr2 ='[{"name":"孙悟空","age":18,"gender":"男"},{"name":"孙悟空","age":18,"gender":"男"}]';
4.JSON工具类
json > js对象
JSON.parse()
可以将以JSON字符串转换为js对象
它需要一个JSON字符串作为参数,会将该字符串转换为JS对象并返回
var o = JSON.parse(json);
var o2 = JSON.parse(arr);
var obj3 = {name:”猪八戒” , age:28 , gender:”男”};
JS对象 > JSON
JSON.stringify() -ify/fy,表示”使……化。
可以将一个JS对象转换为JSON字符串
需要一个js对象作为参数,会返回一个JSON字符串
var str = JSON.stringify(obj3);
console.log(str);
JSON这个对象在IE7及以下的浏览器中不支持,所以在这些浏览器中调用时会报错
二十四、其他
1.localStorage
只读的localStorage
属性允许你访问一个Document 源(origin)的对象 Storage;其存储的数据能在跨浏览器会话保留。localStorage
类似 sessionStorage,
但其区别在于:
存储在 localStorage
的数据可以长期保留;
而当页面会话结束,当页面被关闭时,存储在 sessionStorage
的数据会被清除 。
2.eval()
eval()
这个函数可以用来执行一段字符串形式的JS代码,并将执行结果返回
如果使用eval()执行的字符串中含有{},它会将{}当成是代码块
如果不希望将其当成代码块解析,则需要在字符串前后各加一个()
eval()这个函数的功能很强大,可以直接执行一个字符串中的js代码,
但是在开发中尽量不要使用,首先它的执行性能比较差,然后它还具有安全隐患
var str = '{"name":"孙悟空","age":18,"gender":"男"}';
var obj = eval("("+str+")");
编码
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
<script type="text/javascript">
/*
* 在字符串中使用转义字符输入Unicode编码
* \u四位编码
*/
console.log("\u2620");
</script>
</head>
<body>
<!--在网页中使用Unicode编码
&#编码; 这里的编码需要的是10进制
-->
<h1 style="font-size: 200px;">☠</h1>
<h1 style="font-size: 200px;">⚀</h1>
</body>
</html>
3.confirm()
用于弹出一个带有确认和取消按钮的提示框
需要一个字符串作为参数,该字符串将会作为提示文字显示出来
如果用户点击确认则会返回true,如果点击取消则返回false
var flag = confirm(“确认删除”+name+”吗?”);
二十五、原生js
1.原生js实现复制内容到剪切板
copy() {
const input = document.createElement("input");
document.body.appendChild(input);
input.setAttribute("value",this.solution.code);
input.select();
if (document.execCommand("copy")) {
document.execCommand("copy");
// console.log("复制成功");
}
document.body.removeChild(input);
}
二十六、面向对象高级
1、对象创建模式
(1)Object构造函数模式
- 套路: 先创建空Object对象, 再动态添加属性/方法
- 适用场景: 起始时不确定对象内部数据
- 问题: 语句太多
/*一个人: name:"Tom", age: 12*/
// 先创建空Object对象
var p = new Object()
p = {} //此时内部数据是不确定的
// 再动态添加属性/方法
p.name = 'Tom'
p.age = 12
p.setName = function (name) {
this.name = name
}
//测试
console.log(p.name, p.age)
p.setName('Bob')
console.log(p.name, p.age)
(2)对象字面量模式
- 套路: 使用{}创建对象, 同时指定属性/方法
- 适用场景: 起始时对象内部数据是确定的
- 问题: 如果创建多个对象, 有重复代码
//对象字面量模式
var p = {
name: 'Tom',
age: 12,
setName: function (name) {
this.name = name
}
}
//测试
console.log(p.name, p.age)
p.setName('JACK')
console.log(p.name, p.age)
var p2 = { //如果创建多个对象代码很重复
name: 'Bob',
age: 13,
setName: function (name) {
this.name = name
}
}
(3)工厂模式
- 套路: 通过工厂函数动态创建对象并返回
- 适用场景: 需要创建多个对象
- 问题:
对象没有一个具体的类型
, 都是Object类型 -
//返回一个对象的函数===>工厂函数 function createPerson(name, age) { var obj = { name: name, age: age, setName: function (name) { this.name = name } } return obj } // 创建2个人 var p1 = createPerson('Tom', 12) var p2 = createPerson('Bob', 13) // p1/p2是Object类型 function createStudent(name, price) { var obj = { name: name, price: price } return obj } var s = createStudent('张三', 12000) // s也是Object
(4)自定义构造函数模式
- 套路: 自定义构造函数, 通过new创建对象
- 适用场景: 需要创建多个
类型确定
的对象,与上方工厂模式有所对比 - 问题: 每个对象都有相同的数据, 浪费内存
>//定义类型
>function Person(name, age) {
this.name = name
this.age = age
this.setName = function (name) {
this.name = name
}
>}
>var p1 = new Person('Tom', 12)
>p1.setName('Jack')
>console.log(p1.name, p1.age)
>console.log(p1 instanceof Person)
>function Student (name, price) {
this.name = name
this.price = price
>}
>var s = new Student('Bob', 13000)
>console.log(s instanceof Student)
>var p2 = new Person('JACK', 23)
>console.log(p1, p2)
(5)构造函数+原型的组合模式
- 套路: 自定义构造函数, 属性在函数中初始化, 方法添加到原型上
- 适用场景: 需要
创建多个类型确定
的对象 - 放在原型上可以节省空间(只需要加载一遍方法)
//在构造函数中只初始化一般函数
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.setName = function (name) {
this.name = name
}
var p1 = new Person('Tom', 23)
var p2 = new Person('Jack', 24)
console.log(p1, p2)
2、继承模式
(1)原型链继承
- 套路
- - 定义父类型构造函数
- - 给父类型的原型添加方法
- - 定义子类型的构造函数
- - 创建父类型的对象赋值给子类型的原型
- - `将子类型原型的构造属性设置为子类型`-->此处有疑惑的可以看本笔记[函数高级部分的1、原型与原型链](#1、原型与原型链)
- - 给子类型原型添加方法
- - 创建子类型的对象: 可以调用父类型的方法
- 关键
- - `子类型的原型为父类型的一个实例对象`
constructor 构造函数补充
constructor 我们称为构造函数,因为它指回构造函数本身
- 其作用是让某个构造函数产生的 所有实例对象(比如f) 能够找到他的构造函数(比如Fun),用法就是f.constructor
- 此时实例对象里没有constructor 这个属性,于是沿着原型链往上找到Fun.prototype 里的constructor,并指向Fun 函数本身
- constructor本就存在于原型中,指向构造函数,成为子对象后,如果该原型链中的constructor在自身没有而是在父原型中找到,所以指向父类的构造函数
- 由于这里的继承是直接改了构造函数的prototype 的指向,所以在 sub的原型链中,Sub.prototype 没有constructor 属性,反而是看到了一个super 实例
- 这就让sub 实例的constructor 无法使用了。为了他还能用,就在那个super 实例中手动加了一个constructor 属性,且指向Sub 函数看到了一个super 实例
//父类型
function Supper() {
this.supProp = '父亲的原型链'
}
//给父类型的原型上增加一个[showSupperProp]方法,打印自身subProp
Supper.prototype.showSupperProp = function () {
console.log(this.supProp)
}
//子类型
function Sub() {
this.subProp = '儿子的原型链'
}
// 子类型的原型为父类型的一个实例对象
Sub.prototype = new Supper()
// 让子类型的原型的constructor指向子类型
// 如果不加,其构造函数找的[`new Supper()`]时从顶层Object继承来的构造函数,指向[`Supper()`]
Sub.prototype.constructor = Sub
//给子类型的原型上增加一个[showSubProp]方法,打印自身subProp
Sub.prototype.showSubProp = function () {
console.log(this.subProp)
}
var sub = new Sub()
sub.showSupperProp() //父亲的原型链
sub.showSubProp() //儿子的原型链
console.log(sub)
/**
Sub {subProp: "儿子的原型链"}
subProp: "儿子的原型链"
__proto__: Supper
constructor: ƒ Sub()
showSubProp: ƒ ()
supProp: "父亲的原型链"
__proto__: Object
*/
(2)借用构造函数继承(假的)
- 套路:
- 定义父类型构造函数
- 定义子类型构造函数
- 在子类型构造函数中调用父类型构造
- 关键:
在子类型构造函数中通用call()调用父类型构造函数
- 作用:
- 能借用父类中的构造方法,但是不灵活
function Person(name, age) {
this.name = name
this.age = age
}
function Student(name, age, price) {
//此处利用call(),将 [Student]的this传递给Person构造函数
Person.call(this, name, age) // 相当于: this.Person(name, age)
/*this.name = name
this.age = age*/
this.price = price
}
var s = new Student('Tom', 20, 14000)
console.log(s.name, s.age, s.price)
//[Person]中的this是动态变化的,在[Student]中利用[Person.call(this, name, age)]改变了其this指向,所以可以实现此效果
(3)组合继承
- 利用原型链实现对父类型对象的方法继承
- 利用super()借用父类型构建函数初始化相同属性
function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.setName = function (name) {
this.name = name
}
function Student(name, age, price) {
Person.call(this, name, age) // 为了得到属性
this.price = price
}
Student.prototype = new Person() // 为了能看到父类型的方法
Student.prototype.constructor = Student //修正constructor属性
Student.prototype.setPrice = function (price) {
this.price = price
}
var s = new Student('Tom', 24, 15000)
s.setName('Bob')
s.setPrice(16000)
console.log(s.name, s.age, s.price)
二十七、线程机制与事件机制
1、进程与线程
(1) 进程
- 程序的一次执行,它
占有一片独有的内存空间
- 可以通过windows任务管理器查看进程
- 每个程序的内存空间是相互独立的
(2)线程
- 是进程内的一个独立执行单元
- 是程序执行的一个完整流程
- 是CPU的最小的调度单元
(3)进程与线程
- 应用程序必须运行在某个进程的某个线程上
- 一个进程中至少有一个运行的线程:主线程 –>进程启动后自动创建
- 一个进程中也可以同时运行多个线程:此时我们会说这个程序是多线程运行的
- 多个进程之间的数据是不能直接共享的 –>内存相互独立(隔离)
线程池(thread pool)
:保存多个线程对象的容器,实现线程对象的反复利用
(4)问题
-
何为多进程与多线程?
- 多进程运行: 一应用程序可以同时启动多个实例运行
- 多线程: 在一个进程内, 同时有多个线程运行
-
比较单线程与多线程?
- 多线程:
- 优点:
- 能有效提升CPU的利用率
- 缺点
- 创建多线程开销
- 线程间切换开销
- 死锁与状态同步问题
- 优点:
- 单线程:
- 缺点:效率低
- 优点:顺序编程简单易懂
- 多线程:
-
JS是单线程还是多线程?
JS是单线程运行的 , 但使用H5中的 Web Workers可以多线程运行
- 只能由一个线程去操作DOM界面
-
浏览器运行是单线程还是多线程?
- 都是多线程运行的
-
浏览器运行是单进程还是多进程?
-
单进程
- firefox
- 老版IE
- 多进程
- chrome
- 新版IE
-
2、浏览器内核
支撑浏览器运行的最核心的程序
(1)不同浏览器的内核
- Chrome, Safari : webkit
- firefox : Gecko
- IE : Trident
- 360,搜狗等国内浏览器: Trident + webkit
(2)内核由什么模块组成?
- 主线程
- js引擎模块 : 负责js程序的编译与运行
- html,css文档解析模块 : 负责页面文本的解析(拆解)
- dom/css模块 : 负责dom/css在内存中的相关处理
- 布局和渲染模块 : 负责页面的布局和效果的绘制
- 布局和渲染模块 : 负责页面的布局和效果的绘制
- 分线程
- 定时器模块 : 负责定时器的管理
- 网络请求模块 : 负责服务器请求(常规/Ajax)
- 事件响应模块 : 负责事件的管理
3、定时器
<body>
<button id="btn">启动定时器</button>
<script type="text/javascript">
document.getElementById('btn').onclick = function () {
var start = Date.now()
console.log('启动定时器前...')
setTimeout(function () {
console.log('定时器执行了', Date.now()-start) //定时器并不能保证真正定时执行,一般会延迟一丁点
}, 200)
console.log('启动定时器后...')
// 做一个长时间的工作
for (var i = 0; i < 1000000000; i++) { //会造成定时器延长很长时间
...
}
}
</script>
</body>
(1)定时器真是定时执行的吗?
- 定时器并不能保证真正定时执行
- 一般会延迟一丁点(可以接受), 也有可能延迟很长时间(不能接受)
(2)定时器回调函数是在分线程执行的吗?
- 在主线程执行的, JS是单线程的
(3)定时器是如何实现的?
事件循环模型
4、JS是单线程的
(1)如何证明JS执行是单线程的
setTimeout()的回调函数是在主线程执行的
- 定时器回调函数只有在运行栈中的代码全部执行完后才有可能执行
// 如何证明JS执行是单线程的
setTimeout(function () { //4. 在将[timeout 1111]弹窗关闭后,再等一秒 执行此处
console.log('timeout 2222')
alert('22222222')
}, 2000)
setTimeout(function () { //3. 过了一秒后 打印 timeout 1111并弹窗,此处如果不将弹窗关闭,不会继续执行上方222
console.log('timeout 1111')
alert('1111111')
}, 1000)
setTimeout(function () { //2. 然后打印timeout() 00000
console.log('timeout() 00000')
}, 0)
function fn() { //1. fn()
console.log('fn()')
}
fn()
//----------------------
console.log('alert()之前')
alert('------') //暂停当前主线程的执行, 同时暂停计时, 点击确定后, 恢复程序执行和计时
console.log('alert()之后')
流程结果:
- 先打印了[
fn()
],然后马上就打印了[timeout() 00000
] - 过了一秒后 打印 timeout 1111并弹窗,此处如果不将弹窗关闭,不会继续执行上方222
- 在将[timeout 1111]弹窗关闭后,
再等一秒
执行此处
- 问:为何明明写的是2秒,却关闭上一个弹窗再过一秒就执行?
- 解:并不是关闭后再计算的,而是一起计算的,alert只是暂停了主线程执行
(2)JS引擎执行代码的基本流程与代码分类
- 代码分类:
- 初始化代码
- 回调代码
- js引擎执行代码的基本流程
- 先执行初始化代码: 包含一些特别的代码 回调函数(异步执行)
- 设置定时器
- 绑定事件监听
- 发送ajax请求
- 后面在某个时刻才会执行回调代码
- 先执行初始化代码: 包含一些特别的代码 回调函数(异步执行)
(3)为什么js要用单线程模式, 而不用多线程模式?
- JavaScript的单线程,与它的用途有关。
- 作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。
- 这决定了它只能是单线程,否则会带来很复杂的同步问题
- 举个栗子:如果我们要实现更新页面上一个dom节点然后删除,用单线程是没问题的
- 但是如果多线程,当我删除线程先删除了dom节点,更新线程要去更新的时候就会出错
5、事件循环模型(Event Loop)机制
(1)概念
我们都知道,
javascript从诞生之日起就是一门单线程的非阻塞的脚本语言
。这是由其最初的用途来决定的:与浏览器交互
。单线程意味着,javascript代码在执行的任何时候,都只有一个主线程来处理所有的任务。
非阻塞
:而非阻塞则是当代码需要进行一项异步任务(无法立刻返回结果,需要花一定时间才能返回的任务,如I/O事件)的时候,主线程会挂起(pending)这个任务,然后在异步任务返回结果的时候再根据一定规则去执行相应的回调。
单线程是必要的
:也是javascript这门语言的基石,原因之一在其最初也是最主要的执行环境——浏览器中,我们需要进行各种各样的dom操作。试想一下 如果javascript是多线程的,那么当两个线程同时对dom进行一项操作,例如一个向其添加事件,而另一个删除了这个dom,此时该如何处理呢?因此,为了保证不会 发生类似于这个例子中的情景,javascript选择只用一个主线程来执行代码,这样就保证了程序执行的一致性。
当然,现如今人们也意识到,单线程在保证了执行顺序的同时也限制了javascript的效率,因此开发出了
web workers
技术。这项技术号称可以让javaScript成为一门多线程语言。然而,使用web workers技术开的多线程有着诸多限制,例如:
所有新线程都受主线程的完全控制,不能独立执行
。这意味着这些“线程” 实际上应属于主线程的子线程。另外,这些子线程并没有执行I/O操作的权限,只能为主线程分担一些诸如计算等任务。所以严格来讲这些线程并没有完整的功能,也因此这项技术并非改变了javascript语言的单线程本质。可以预见,未来的javascript也会一直是一门单线程的语言。
话说回来,前面提到javascript的另一个特点是“
非阻塞
”,那么javascript引擎到底是如何实现的这一点呢?答案就是——event loop(事件循环)。
注:
虽然nodejs中的也存在与传统浏览器环境下的相似的事件循环。然而两者间却有着诸多不同,故把两者分开,单独解释
。
(2)浏览器环境下JS引擎的事件循环机制
-
执行栈概念
执行上下文栈详情可以看上方笔记 –>函数高级的2、执行上下文与执行上下文栈,此处继续进行一次概述加深理解
当javascript代码执行的时候会将不同的变量存于内存中的不同位置:
堆(heap)
和栈(stack)
中来加以区分。其中,堆里存放着一些对象。而栈中则存放着一些基础类型变量以及对象的指针。但是我们这里说的执行栈和上面这个栈的意义却有些不同
。
执行栈
:当我们调用一个方法的时候,js会生成一个与这个方法对应的执行环境(context),又叫
执行上下文
。这个执行环境中存在着这个方法的私有作用域、上层作用域的指向、方法的参数,这个作用域中定义的变量以及这个作用域的this对象。 而当一系列方法被依次调用的时候,因为js是单线程的,同一时间只能执行一个方法,于是这些方法被排队在一个单独的地方。这个地方被称为执行栈。当一个脚本第一次执行的时候,js引擎会解析这段代码,并将其中的同步代码按照执行顺序加入执行栈中,然后从头开始执行。如果当前执行的是一个方法,那么js会向执行栈中添加这个方法的执行环境,然后进入这个执行环境继续执行其中的代码。
当这个执行环境中的代码 执行完毕并返回结果后,js会退出这个执行环境并把这个执行环境销毁,回到上一个方法的执行环境
。这个过程反复进行,直到执行栈中的代码全部执行完毕。一个方法执行会向执行栈中加入这个方法的执行环境,在这个执行环境中还可以调用其他方法,甚至是自己,其结果不过是在执行栈中再添加一个执行环境。这个过程可以是无限进行下去的,
除非发生了栈溢出,即超过了所能使用内存的最大值
。以上的过程说的都是同步代码的执行。那么当一个异步代码(如发送ajax请求数据)执行后会如何呢?
刚刚说过js的另一大特点是非阻塞,实现这一点的关键在于下面要说的这项机制——
事件队列(Task Queue)
。
6.事件队列(Task Queue)
JS引擎遇到一个异步事件后并不会一直等待其返回结果,而是会将这个事件挂起,继续执行执行栈中的其他任务,当一个异步事件返回结果后,js会将这个事件加入与当前执行栈不同的另一个队列,我们称之为
事件队列
。被放入事件队列不会立刻执行其回调,而是
等待当前执行栈中的所有任务都执行完毕, 主线程处于闲置状态时,主线程会去查找事件队列是否有任务
。如果有,那么主线程会从中取出排在第一位的事件,并把这个事件对应的回调放入执行栈中,然后执行其中的同步代码…,如此反复,这样就形成了一个无限的循环。这就是这个过程被称为“事件循环(Event Loop)”的原因。
stack表示我们所说的执行栈,web apis则是代表一些异步事件,而callback queue即事件队列。
以上的事件循环过程是一个宏观的表述,实际上因为异步任务之间并不相同,因此他们的执行优先级也有区别。
不同的异步任务被分为两类:微任务(micro task)和宏任务(macro task)
7.宏任务(macro task)与微任务(micro task)
(1)宏任务队列与微任务队列解释
顾名思义,宏任务放至宏任务队列(
简称宏队列
)中、微任务放至微任务队列(简称微队列
)中
- JS中用来存储待执行回调函数的队列包含2个不同特定的列队
宏队列
:用来保存待执行的宏任务(回调),比如:定时器
回调/ajax回调/dom事件回调微队列
:用来保存待执行的微任务(回调),比如:Promise
的回调/muntation回调
- JS执行时会区别这2个队列:
- JS执行引擎首先必须执行所有的
初始化同步任务
代码- 每次准备取出第一个
宏任务执行前
,都要将所有的微任务
一个一个取出来执行前面我们介绍过,在一个事件循环中,异步事件返回结果后会被放到一个任务队列中。然而,根据这个异步事件的类型,这个事件实际上会被对应的宏任务队列或者微任务队列中去。并且在当前执行栈为空的时候,主线程会 查看微任务队列是否有事件存在。如果不存在,那么再去宏任务队列中取出一个事件并把对应的回到加入当前执行栈;如果存在,则会依次执行队列中事件对应的回调,直到微任务队列为空,然后去宏任务队列中取出最前面的一个事件,把对应的回调加入当前执行栈…如此反复,进入循环。
我们只需记住:**
当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行
**
(2)由代码逆向理解宏任务与微任务
setTimeout(() => {
console.log('timeout callback1()')//立即放入宏队列
Promise.resolve(3).then(
value => {
console.log('Promise onResolved3()', value)//当这个宏任务执行后 立马放入微队列,所以这个微任务执行完后下个宏任务才能执行
}
)
}, 0)
setTimeout(() => {
console.log('timeout callback2()') //立即放入宏队列,
}, 0)
Promise.resolve(1).then(
value => {
console.log('Promise onResolved1()', value)//立即放入微队列
setTimeout(() => {
console.log('timeout callback3()', value) //立即放入宏任务
}, 0)
}
)
Promise.resolve(2).then(
value => {
console.log('Promise onResolved2()', value)//立即放入微队列
}
)
console.log('同步代码') //同步代码立即执行
//结果
'同步代码',
'Promise onResolved1()',
'Promise onResolved2()',
'timeout callback1()',
'Promise onResolved3()',
'timeout callback2()',
'timeout callback3()'
(3)node环境下的事件循环机制
-
与浏览器环境有何不同?
在node中,事件循环表现出的状态与浏览器中大致相同。不同的是node中有一套自己的模型。node中事件循环的实现是依靠的libuv引擎。我们知道node选择chrome v8引擎作为js解释器,v8引擎将js代码分析后去调用对应的node api,而这些api最后则由libuv引擎驱动,执行对应的任务,并把不同的事件放在不同的队列中等待主线程执行。
因此实际上node中的事件循环存在于libuv引擎中
。
-
事件循环模型
下面是一个libuv引擎中的事件循环的模型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 >//libuv引擎中的事件循环的模型 >┌───────────────────────┐ >┌─>│ timers │ >│ └──────────┬────────────┘ >│ ┌──────────┴────────────┐ >│ │ I/O callbacks │ >│ └──────────┬────────────┘ >│ ┌──────────┴────────────┐ >│ │ idle, prepare │ >│ └──────────┬────────────┘ ┌───────────────┐ >│ ┌──────────┴────────────┐ │ incoming: │ >│ │ poll │<──connections─── │ >│ └──────────┬────────────┘ │ data, etc. │ >│ ┌──────────┴────────────┐ └───────────────┘ >│ │ check │ >│ └──────────┬────────────┘ >│ ┌──────────┴────────────┐ >└──┤ close callbacks │ └───────────────────────┘注:模型中的每一个方块代表事件循环的一个阶段
这个模型是node官网上的一篇文章中给出的。
-
事件循环各阶段详解
从上面这个模型中,可以大致分析出node中的事件循环的顺序:
外部输入数据–>轮询阶段(poll)–>检查阶段(check)–>关闭事件回调阶段(close callback)–>定时器检测阶段(timer)–>I/O事件回调阶段(I/O callbacks)–>闲置阶段(idle, prepare)–>轮询阶段…
这些阶段大致的功能如下:
- timers(定时器检测阶段): 这个阶段执行定时器队列中的回调如
setTimeout()
和setInterval()
。 - I/O callbacks(I/O事件回调阶段): 这个阶段执行几乎所有的回调。但是不包括close事件,定时器和
setImmediate()
的回调。 - idle, prepare: 这个阶段仅在内部使用,可以不必理会。
- poll(轮询阶段): 等待新的I/O事件,node在一些特殊情况下会阻塞在这里。
- check(检查阶段):
setImmediate()
的回调会在这个阶段执行。 - close callbacks(关闭事件回调阶段): 例如
socket.on('close', ...)
这种close事件的回调。
下面按照代码第一次进入libuv引擎后的顺序来详细解说这些阶段:
-poll(轮询阶段)
当个v8引擎将js代码解析后传入libuv引擎后,循环首先进入poll阶段。poll阶段的执行逻辑如下: 先查看poll queue中是否有事件,有任务就按先进先出的顺序依次执行回调。 当queue为空时,会检查是否有setImmediate()的callback,如果有就进入check阶段执行这些callback。但同时也会检查是否有到期的timer,如果有,就把这些到期的timer的callback按照调用顺序放到timer queue中,之后循环会进入timer阶段执行queue中的 callback。 这两者的顺序是不固定的,收到代码运行的环境的影响。如果两者的queue都是空的,那么loop会在poll阶段停留,直到有一个i/o事件返回,循环会进入i/o callback阶段并立即执行这个事件的callback。
值得注意的是,poll阶段在执行poll queue中的回调时实际上不会无限的执行下去。
有两种情况poll阶段会终止执行poll queue中的下一个回调:1.所有回调执行完毕。2.执行数超过了node的限制。
-check(检查阶段)
check阶段专门用来执行
setImmediate()
方法的回调,当poll阶段进入空闲状态,并且setImmediate queue中有callback时,事件循环进入这个阶段。
-close callbacks(关闭事件回调阶段)
当一个socket连接或者一个handle被突然关闭时(例如调用了
socket.destroy()
方法),close事件会被发送到这个阶段执行回调。否则事件会用process.nextTick()
方法发送出去。
-timers(定时器检测阶段)
这个阶段以先进先出的方式执行所有到期的timer加入timer队列里的callback,一个timer callback指得是一个通过setTimeout或者setInterval函数设置的回调函数。
-I/O callbacks(I/O事件回调阶段)
如上文所言,这个阶段主要执行大部分I/O事件的回调,包括一些为操作系统执行的回调。例如一个TCP连接生错误时,系统需要执行回调来获得这个错误的报告。
(4)process.nextTick,setTimeout与setImmediate的区别与使用场景
在node中有三个常用的用来推迟任务执行的方法:process.nextTick,setTimeout(setInterval与之相同)与setImmediate
这三者间存在着一些非常不同的区别:
process.nextTick()
尽管没有提及,但是实际上node中存在着一个特殊的队列,即nextTick queue。这个队列中的回调执行虽然没有被表示为一个阶段,当时这些事件却会在每一个阶段执行完毕准备进入下一个阶段时优先执行。当事件循环准备进入下一个阶段之前,会先检查nextTick queue中是否有任务,如果有,那么会先清空这个队列。与执行poll queue中的任务不同的是,这个操作在队列清空前是不会停止的。这也就意味着,错误的使用
process.nextTick()
方法会导致node进入一个死循环。。直到内存泄漏。使用这个方法比较合适呢?下面有一个例子:
1 2 >const server = net.createServer(() => {}).listen(8080); >server.on('listening', () => {});这个例子中当,当listen方法被调用时,除非端口被占用,否则会立刻绑定在对应的端口上。这意味着此时这个端口可以立刻触发listening事件并执行其回调。然而,这时候
on('listening)
还没有将callback设置好,自然没有callback可以执行。为了避免出现这种情况,node会在listen事件中使用process.nextTick()
方法,确保事件在回调函数绑定后被触发。
setTimeout()和setImmediate()
在三个方法中,这两个方法最容易被弄混。实际上,某些情况下这两个方法的表现也非常相似。然而实际上,这两个方法的意义却大为不同。
setTimeout()
方法是定义一个回调,并且希望这个回调在我们所指定的时间间隔后第一时间去执行。注意这个“第一时间执行”,这意味着,受到操作系统和当前执行任务的诸多影响,该回调并不会在我们预期的时间间隔后精准的执行。执行的时间存在一定的延迟和误差,这是不可避免的。node会在可以执行timer回调的第一时间去执行你所设定的任务。
setImmediate()
方法从意义上将是立刻执行的意思,但是实际上它却是在一个固定的阶段才会执行回调,即poll阶段之后。有趣的是,这个名字的意义和之前提到过的process.nextTick()
方法才是最匹配的。node的开发者们也清楚这两个方法的命名上存在一定的混淆,他们表示不会把这两个方法的名字调换过来—因为有大量的node程序使用着这两个方法,调换命名所带来的好处与它的影响相比不值一提。
setTimeout()
和不设置时间间隔的setImmediate()
表现上及其相似。猜猜下面这段代码的结果是什么?
1 2 3 4 5 6 7 >setTimeout(() => { console.log('timeout'); >}, 0); >setImmediate(() => { console.log('immediate'); >});实际上,答案是不一定。没错,就连node的开发者都无法准确的判断这两者的顺序谁前谁后。这取决于这段代码的运行环境。运行环境中的各种复杂的情况会导致在同步队列里两个方法的顺序随机决定。但是,在一种情况下可以准确判断两个方法回调的执行顺序,那就是在一个I/O事件的回调中。下面这段代码的顺序永远是固定的:
1 2 3 4 5 6 7 8 9 10 >const fs = require('fs'); >fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); }); >});答案永远是:
1 2 >immediate >timeout因为在I/O事件的回调中,setImmediate方法的回调永远在timer的回调前执行。
8、Web Workers
(1)简介
- 官网
Web Workershttps://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API/Using_web_workers
- H5规范提供了js分线程的实现, 取名为: Web Workers
(2)相关API
- Worker: 构造函数, 加载分线程执行的js文件
- Worker.prototype.onmessage: 用于接收另一个线程的回调函数
- Worker.prototype.postMessage: 向另一个线程发送消息
(3)不足
- worker内代码不能操作DOM(更新UI)
- 不能跨域加载JS
- 不是每个浏览器都支持这个新特性
(4)例子
//斐波那契(Fibonacci)数列
<body>
<input type="text" placeholder="数值" id="number">
<button id="btn">计算</button>
<script type="text/javascript">
// 1 1 2 3 5 8 f(n) = f(n-1) + f(n-2)
function fibonacci(n) {
return n<=2 ? 1 : fibonacci(n-1) + fibonacci(n-2) //递归调用
}
// console.log(fibonacci(7))
var input = document.getElementById('number')
document.getElementById('btn').onclick = function () {
var number = input.value
var result = fibonacci(number)
alert(result)
}
//当传入计算数值为50左右(有的甚至更低),整个页面就会卡住好久的时间不能操作(计算结束后才会弹窗,但是未弹窗的这段时间用户并不能进行操作),这时候就会发现单线程的弊端了
</script>
(5)使用
-
主线程
- 创建一个Worker对象
- 绑定[主线程接收分线程返回的数据]方法
- 主线程向分线程发送数据,然后等待接受数据
- 接收到分线程回馈的数据,将数据进行处理(如弹窗)
<body>
<input type="text" placeholder="数值" id="number">
<button id="btn">计算</button>
<script type="text/javascript">
var input = document.getElementById('number')
document.getElementById('btn').onclick = function () {
var number = input.value
//创建一个Worker对象
var worker = new Worker('worker.js')
// 绑定接收消息的监听
worker.onmessage = function (event) { //此处变成回调代码,会在初始化工作完成后才会进行
console.log('主线程接收分线程返回的数据: '+event.data)
alert(event.data)
}
// 向分线程发送消息
worker.postMessage(number)
console.log('主线程向分线程发送数据: '+number)
}
// console.log(this) // window
</script>
</body>
-
分线程
- 将计算放置分线程中
注意
:alert(result) alert是window的方法, 在分线程不能调用,分线程中的全局对象不再是window
, 所以在分线程中不可能更新界面
//worker.js
function fibonacci(n) {
return n<=2 ? 1 : fibonacci(n-1) + fibonacci(n-2) //递归调用
}
console.log(this)
this.onmessage = function (event) {
var number = event.data
console.log('分线程接收到主线程发送的数据: '+number)
//计算
var result = fibonacci(number)
postMessage(result)
console.log('分线程向主线程返回数据: '+result)
// alert(result) alert是window的方法, 在分线程不能调用
// 分线程中的全局对象不再是window, 所以在分线程中不可能更新界面
}