认识浏览器
-
用户能操作的部分叫 shell外核
-
主流浏览器 内核 IE trident Chrome webkit/blink firefox Gecko Opera presto
-
浏览器不能显示的部分叫内核
- 渲染引擎 :html css语法识别 绘制
- js引擎
- 其他模块
-
2008开发的Google浏览器 v8引擎
- 直接把js代码翻译成机械码 最快
-
浏览器是多线程的:
- js引擎
- UI引擎
- 事件线程
- 发起请求的线程
- 定时器的线程
js开始
一.特色:
-
解释性语言,不生成特点文件直接翻译由机器执行 带<>都是解释语言
- 优点:跨平台
- 缺点:速度稍微慢
-
单线程
二.执行队列:
- 轮转时间片
- 切分任务为时间片
- 进行随机排列
- 送往引擎进行执行
三.js三大部分
- ECMAScript
- DOM
- BOM
如何引入js
外部和内部都存在只会执行外部
页面内嵌 <script></script>
外部引入 <script src="location.js"></script>
变量和数据类型
1.变量
用于存放数据,便于后续使用
-
命名规则:
- 开头必须是 下划线( _ ) 字母 $ _
- 中间可以是 下划线( _ ) 字母 $ 数字
- 不可使用关键字、保留字
//声明变量(开辟内存空间)
//a:空间名
var a;
//赋值 :将100放入a这个空间
a = 100;
2.数据类型
-
原始值
-
不可改变 只是变量重新开辟空间
-
// 以下这几种 Number(天生就是浮点型) Boolean String undefined null -
在内存存储形式 是 (stack) 栈
- 先进的最后出来 有底没顶的口袋 栈和栈的赋值是拷贝 互不影响
-
-
引用值
-
Array Object function -
// Array var arr = [1,2,3] -
在内存存储形式 是heap 堆
- 名字存在栈(指针),内容存在堆(地址),实现形式 名字指向 地址
-
-
语法
-
每句后面用 ; 结尾 告诉程序这一句结尾
-
{ } 后面不需要
-
开发规范
- 任何符号的两侧都要一个空格
-
var a = 1;
-
语法错误
-
一个代码块的错误不会影响到其他代码块
-
语法解析错误(低级错误)
- 整体扫描的时候会发现,一行代码不会执行
-
逻辑错误(标准错误)
- 开始执行js代码到错误行就会中断报
-
-
运算符
1.数学运算,字符串连接
var a = 'aa' + 'aa '; => aaaa //string2.任何数据类型加字符串都等于字符串
var a = 'aa '+ 1; => aa1 //string
运算符
比较运算符
- 结果只有false true
- 只有NaN不等于NaN ,引用数据类型也不等于自己 ,其他的都与自己相等
undefined == undefined //true
{ } == { } //false
[] == [] //false
null == null //true
&& (逻辑与)
-
全都为true 才会是true
-
先看第一表达式转换为布尔值是否为true,
-
如果为true 那么它会看第二表达式转换为布尔值的结果,
-
如果为false ,那么就会继续判断 是多个表达式还是仅有两个表达式
-
如果只有两个表达式的话,只看到第二个表达式,就返回该表达式的值了。可以模仿if语句
-
2 > 1 && document.write("aa"); //通过左边的表达式来判断
-
-
如果有多个表达式就会那当前比较的结果继续比
-
1 > 2 && 2 > 1 && 3 < 2 // false
-
-
-
undefined null NaN "" 0 flase // 只有这六个值是false
& 二进制与运算
-
||(或)
- 碰到真(转换为布尔值后的结果)就返回,
- 碰到假还继续
- 一个真就是真,全假就是假
-
!(非)
- 转换为布尔值再取反
-
,
- 先计算左边的结果然后计算右边的结果,返回右边的结果
逻辑语句 (if else)
if语句
if(条件){ 执行语句 }
for循环
(1) (2) (3)
for (var i = 0;i < 10; i++ ){
document.write('a');
}
//进入for先执行(1)
//然后会执行(2)进行判断
//成功的话执行 函数体 不成功就结束循环
//如果执行了函数体,在函数体执行完后会 执行(3),
实际执行顺序:
1.var i=0;
2.if(i<10){ document.write('a');}
3.i++ -> i=1;
4.if(){}
5.i++
for循环可以灵活使用 条件一,三都可以不写在括号里面,但是条件二必须写在里面
var i = 100;
for( ;i--; ){
documen.write(“i”)
}
do while 先执行后判断
do {
text += "<br>数字为 " + i;
i++;
} while (i < 5);
switch 用法
var n='a';
switch(n){
case 'a':console.log('a');
//进入一个case中后没有break的话会一直往下进人每个case
break;
case '1':console.log('b');
case '2':console.log('c');
}
break 用法 :中断本层操作(只能跳出一层循环)
var i = 100;
for(let k = 0;k<999;k++{for( ;i--; ){
documen.write(“i”);
if(i<=0){
break;
//当为0的时候 直接结束但是外层的循环 还会执行
}
}}
continue : 中止本次操作 开始后续操作
var i = 100;
for( ;i--; ){
documen.write(“i”);
if(i<=0){
continue;
//终止当次循环 继续循环
}
}
对象
js的对象不同于其他语言的对象,可以后天的修改对象,而其他语言基本上是写死的,不能修改
对象:把一些有共同特性的数据放在一起成为对象
var mrdeng = {
name : "deng",
age : 50,
sex : "male",
smoke : function(){ console.log("I am smoking ");}
}
对象的创建
1.plainObject 对象字面量/对象直接量 直接赋值创建
var obj = { }
2.构造函数
-
1.系统自带的构造函数 Object()
-
// 产生的结果等于第一种方法 // 本质上通过子字面量创建在系统最终也是这么实现 var obj = new Object();
-
-
2.自定义构造函数
- 要符合大驼峰式命名规则 TheFirstName
-
function Person(){} // 实例化自己定义的构造函数 var person = new Person ();
对象属性的增删改查(对象的属性不存在调用的话并不会报错,会报undefined)
//增加对象属性
mrdeng.health = "100"
// 增加和修改一样, 在修改时如果不存在该属性就会自动创建
mrdeng.health = "gao"
//获取对象属性,通过点 . 操作符直接调用对象属性即可
console.log(mrdeng.name);
//删除对象属性
delete mrdeng.name;
构造函数内部原理
只有碰到new方法才会操作创建this对象
function Student(name, age, sex) {
//step 1 在函数体最前面隐式的加上this = {} 创建理论上的空对象
var this = { }
// step 2 将构造函数的属性 按照写的顺序把属性写入this的空对象
this.name = name;
this.age = age;
this.sex = sex;
this.grade = 2017;
//step 3 隐式实现return this; 将这个修改后的this返回出去
}
遍历对象
对象属性访问规则
-
a. xx.属性名
- 常规使用方法
-
b. xx['xx']
- 系统访问规则 系统在访问xx.属性名的时候会转换为xx['属性名']
var test = { name : 'gao'}
test.name === test['name']
对象遍历(枚举)方法 (for in)
//缺点:会取出对象原型的属性,系统设置的原型不会取出,自己给Object加原型属性会被取出
for(var xx in obj){ }
var obj = {
name : 'qugao',
sex : 'male',
}
var prop;
for(prop in obj) {
// 输出undefined 因为系统转换 obj.prop---->obj.['prop'] 把prop当做字符串 访问prop属性
console.log(obj.prop);
//prop 接受对象的属性名 此时已经是字符串不需要加引号了
console.log(obj[prop]);
}
hasOwnProperty
判断是否在原型上,让for in循环不直接取原型的属性值
var obj = {
name : 'qugao',
height : 100,
__proto__ :{
lastName : 'gao'
}
}
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
console.log(obj[key]);
}
}
in 操作
判断该对象能否访问该属性 //原型属性也算
格式: XX in obj
var test = { name : 'qugao' }
console.log('name' in test) //true
instanceof
A对象的原型上有没有B的原型
格式: A instanceof B
[] instanceof Array; //true
var test = {} ;
test instanceof Object; //true
对象克隆
浅层克隆对象
var obj = {
name : 'abc',
sex : 'male',
age : 23
}
var obj1 = {}
function clone (origin, target){
var target = target || {}
for(var prop in obj){
target[prop] = origin[prop];
}
return target;
}
clone(obj,obj1);
深层克隆
var obj ={
name :'abc',
age : 123,
card : ['visa', 'master'],
wife : {
name : "bcd",
son : {name : "aaa"}
}
}
var obj1 = {}
deepClone(obj,obj1);
// ---------------
function deepClone (origin, target){
var target = target || {},
toStr = Object.prototype.toString,
arrStr = "[Object Array]";
for(var prop in origin){
if(origin.hasOwnProperty(prop)){
if(origin[prop] !== "null" && typeof(origin[prop]) == 'object'{
// 此处可以用三目运算符简化 :
if(toStr.call(origin[prop]) == arrStr){
target[prop] = [];
}else {
target[prop] = {};
}
target[prop] = (toStr.call(origin[prop]) == arrStr) ? [] : {}
deepClone(origin[prop],target[prop]);
}else{
target[prop] = origin[prop];
}
}
}
return target;
}
数组
数组的下标从0开始,内容可以是任意变量,并且数组就是一种特殊的对象
//标准写法a[0] 数组的第0
var arr = [1,2,3,4,"acv",undefined,function test() {}];
//数组的长度(length是数组的属性,可以获得长度)
arr.length
数组声明
-
字面量声明,指明数组内容
-
var arr = [1,2,3,4]
-
-
构造函数
-
// 只传一个参数只会规定长度,值为空 var arr1 = new Array(10) //传多个参数设置数组的值 var arr2 = new Array(10,3)
-
数组的读和写
-
js的数组不会报数组越界溢出,因为js的数组基于对象,对象没有该属性会报undefined
-
var arr = [1] console.log(arr[1]) // undefined
-
-
可以溢出写,相当于新增属性
-
var arr = [1] arr[1] = '2' console.log(arr[1]) // 2
-
数组的方法
不改变原数组
concat
- 连接两个或多个数组
- 返回被连接数组的一个副本(新数组) 可以实现数组拼接
var a1 = [1,2,3];
var a2 = [3,4,5];
var a3 = a1.concat(a2);
//a3 -> [1,2,3,4,5,6]
join
- 把数组中所有元素放入一个字符串 join方法括号里面的字符串是分隔符
var a1 = [1,2,3];
var a3 = a1.join(分隔符);
// a3 -> "1,2,3";
slice
-
写法一: array.slice(开始下标);
- 取从下标开始之后的所有元素(包括下标)
-
写法二: array.slice(开始下标,结束下标);
- 取从开始下标(包括开始下标)到结束下标之间(不包括结束下标)
var a1 = [0,1,2,3];
var a2 = a1.slice(1); //a2 -> [1,2,3]var a1 = [0,1,2,3];
var a2 = a1.slice(1,2); //a2 -> [1]
toString
- 将数组转为字符串 不需要参数
var a1 = [1,2,3];
var a2 = a1.toString();
//a2 -> "1,2,3"
改变原数组
pop
- 删除数组最后一个元素,
- 如果数组为空,则不改变数组,返回undefined,
- 正常删除就会返回被删除的元素
var a1 = [1,2,3];
var a2 = a1.pop();
// a2 -> 3 a1 => [1,2]
push
- 向数组末尾添加一个或多个元素,返回新数组的长度
var a1 = [1,2,3];
var a2 = a1.push(4);
//a2 -> 4 a1 => [1,2,3,4]
reverse
- 颠倒数组中元素的顺序,返回该数组
var a1 = [1,2,3];
var a2 = a1.reverse();
//a2 -> [3,2,1]
shift
- 删除数组第一个元素,
- 若数组为空,不进行任何删除,返回undefined,
- 正常删除会返回被删除的元素
var a1 = [1,2,3];
var a2 = a1.shift();
//a2 -> 2 a1 => [2,3]
unshift
- 在数组的最前面添加元素 返回添加后的长度
var a1 = [1,2,3];
var a2 = a1.unshift(4);
//a2 -> 4 a1 => [4,1,2,3]
sort
-
对数组进行排序,返回该数组 默认是升序(小到大)
-
sort方法留给了编程者一个自定规则的接口
- 可以在sort方法中写一个function来返回返回值来规定是升序还是降序
var a1 = [1,4,5,2,3];
var a2 = a1.sort();
//a2 -> [1,2,3,4,5]
//(降序)
var a1 = [1,4,5,2,3];
var a2 = a1.sort(function(a,b) {return a < b ? 1 : -1});
//a2 -> [5,4,3,2,1]
splice
相当于从目标数组指定位置开辟切口,删除被选择的元素,如果指定了插入元素,将插入元素放置到缺口未知
-
array.splice(指定下标);
-
var a1 = [1,4,5,2,3]; var a2 = a1.splice(1); //a2 => [4,5,2,3] 截取从第一位开始的所有 //
-
-
格式 array.splice(start,length,在切口处添加新的元素(可选));
- 返回截取完后的片段
-
var a1 = [1,4,5,2,3]; var a2 = a1.splice(1,3); //a2 = > [4,5,2] 截取从第一位开始保留三位 a1 => [1,3] var a4 = [1,4,5,2,3]; var a3 = a4.splice(1,3,'6','5','42') //a3 = > [4,5,2] a4 => [1,'6','5','42',3]
包装类
-
部分原始值没有属性和方法
-
1.length // 报错 '1'.length // 1 true.length // undefined undefined.length // 报错
-
-
原始值构造函数
- new Number()
- new String()
- new Boolean()
-
var a = new Number(1) // 1 var b = new String(1) // '1' var c = new Boolean(1) // true
-
为什么原始值可以用 length 属性
var str = "123";
console.log(str.length);
// 系统检测到之后不会报错,而是通过判断对应的类型,调用构造函数
// 而是通过new String()创建一个会字符串对象,然后调用length方法,之后再删除该字符串对象
修改字符串的length
var str = 'abcd';
str.length = 2;
//改变的只是字符串对象的属性 改变不了str的长度
console.log(str); //abdc
console.log(str.length); //4
显式数据转换
// 将数字字符串转为数字类型,不是数字字符串则是NaN
Number();
//将字符串类型转换为数字类型
Number("123")
//转换成了NaN 因为字母无法被转换成数字
Number(“12a”);
// 结果都是0
NaNNumber(undefined);
NaNNumber(null);
parseInt
-
从头开始判断,是数字就保留
-
不是数字的或小数点就中断保留前面的数字
-
是小数点就找小数点后的数字,遇到非数字中断
-
如果开头就是非数字,就直接返回 NaN
-
转为整型
-
// 得到123 parseInt("123aa"); // 得到1 parseInt("1a23aa"); // NaN parseInt("a1a23aa");
-
-
转换为其他进制
-
// 参数2 指定目标算换进制,可以是 2-36 var a = parseInt("0101",2) // 默认是将其他进制转为十进制 // 得到结果是10 parseInt(a) // 与paserInt类似,转化为浮点型 parseFloat() // 将类型转为字符串类型 String() // 将类型转为布尔类型 Boolean() // 将调用的变量属性转换为字符串类型 // undefined null 不能使用 toString() // 对数字进行进制转换 // 可以设置目标进制,默认以十进制为基础转为目标进制 var demo = 10; // 12 var demo1 = demo.toSting(8);
-
-
隐式类型转换
1 + 1 = 2
1 + '1' = '11'
1 / '1' = 1
1 * "1" = 1
1 + [] = 1
1 / [] = Infinity
1 * [] = 0
1 - [] = 1
1 - {} = NaN
1 + {} = "1[object Object]"
1 * {} = NaN!{} //false
![] //false
-
isNaN()
- 先会把变量使用Number()
- 再和NaN比对
-
++/-- +/-(正负)
- 先调用Number()转换
- 再++
-
+和- 运算
- 只要有一边是字符串就调用String() 转换为字符串类型
-
*和/
- 先调用Number() 方法转换再乘
-
&& || !
- 隐式调用boolean方法判断
-
==和!=
- 也有类型转换
- undefined == null undefined != 0 NaN!=NaN
-
=== 和!==
- 绝对等于、不等于
- 不发生类型转换,直接判断字面量
类型判断
typeof
判断数据类型,返回值都是字符串
- null和数组都是object
- Array和Object等构造器都是function
- 基本数据类型 number boolean string object function undefined
typeof null // "object"
typeof [] // "object"
typeof Array (Array构造函数)//"function"
typeof 1 //"number"
typeof false //"boolean"
变量未定义输出会报错 但是放在typeof里面的话不会报错
typeof a //undefined
Object.prototype.toString.call
通过构造类的tostring方法打印构造类型判断,所有的构造类都基于Object构造函数
Object.prototype.toString.call(999) // "[object Number]"
Object.prototype.toString.call('') // "[object String]"
Object.prototype.toString.call(Symbol()) // "[object Symbol]"
Object.prototype.toString.call(42n) // "[object BigInt]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call(true) // "[object Boolean]
Object.prototype.toString.call({a:1}) // "[object Object]"
Object.prototype.toString.call([1,2]) // "[object Array]"
Object.prototype.toString.call(new Date) // "[object Date]"
Object.prototype.toString.call(function(){}) // "[object Function]"
obj instanceof Object
可以左边放你要判断的内容,右边放类型来进行JS类型判断
- 只能用来判断复杂数据类型,
- 因为instanceof 是用于检测构造函数(右边)的
prototype属性是否出现在某个实例对象(左边)的原型链上
[1,2] instanceof Array // true
(function(){}) instanceof Function // true
({a:1}) instanceof Object //
true(new Date) instanceof Date // true
constructor
- 通过原型链进行查找的
- 可以用来查找对象和普通数据
- 相比于 instanceof 和 typeof 会更加精准
var x = []
var y = {}
var z = Date()
var a = 'aaa'
console.log(x.constructor == Object) //false
//在这里 数组并不被识别成对象
console.log(x.constructor == Array) //true
console.log(y.constructor == Object) //true
//字符串可以正常被识别
console.log(a.constructor == String) //true
console.log(z.constructor == Object) //false
//时间对象也不被识别成对象
console.log(z.constructor == Date) // true
三目运算符
// 相当于if else 加 return
// 判断成功会返回值
// 格式
// 条件判断 ? 是 : 否
// 返回4
var num = 1 > 0 ? 2 + 2 : 1 + 1;
函数
函数声明
// 标准函数声明
function test() {}
// 函数表达式,表达式会忽略函数名(abc),相当于匿名函数
var test = function abc() {}
// 正常函数表达式
var demo = function() {}
//()执行符号,函数执行
demo()
// 声明函数时使用执行符合,就变成立即执行函数,会直接触发函数执行
var demo = function(){}( console.log(1));
组成形式
-
函数名 必须
-
参数 可选
- 位数不匹配会按顺序匹配,无论有没有形参接收,系统都会生成实参列表arguments(数组)
- 接收形参和arguments具有映射关系,一个改变值另一个会跟着改,
- 但是实参列表没有就不会改不会往里面追加(也就是空缺参数不受影响)
function test(a){
//a 形参
//参数相当于隐式定义一个变量 var a;
}
test(1)//实参 1;
//----------------
function test(a,b) {
//arguments -> [2]
a = 2;
console.log(arguments[0]); //2
b = 2;
console.log(b); //2
console.log(arguments[1]); // undefined 因为b没有传递进来,没有映射关系
}
test(2);
函数作用域
单个作用域里面的可以访问外面的,外面的不能访问里面的。
-
全局变量
- 在整个script标签里面定义的变量
-
函数变量
- 在函数里面定义的变量
作用域属性是什么
- 每个Javascript函数都是一个对象,对象中有些属性我们可以访问,但是有些不可以,
- 不能读取的这些对象仅供 Javascript引擎存取,[[scope]]就是其中一个。
- [[scope]] 属性代表的就是我们所说的作用域,其中存储了运行期上下文的集合
- 在预编译阶段创建执行期上下文(AO对象)
函数作用域构建顺序
构建作用域的过程就是生成scope和增加scope
- 全局函数、变量在定义时生成的scope指向GO
- 全局函数执行时生成的scope指向当前函数生成的AO对象放在作用域链顶端
- 然后将GO对象放在作用域第二位
- 局部函数、变量定义时生成的scope首先会复制父级的scope
- 局部变量、函数执行时会将自身生成的AO对象放在scope顶端,将复制的作用域链向下挪下标
function a(){
function b(){
function c(){}
c();
}
b();
}
a();
//--------------
0. 创建GO对象
// 因为定义在全局,可以直接获取GO对象
// 此时a 的scope 为 [Go]
1. 函数a 定义
// 创建 AO对象 为AOa ,并且插入GO对象
// 函数执行时a的[[scope]] 为 [GO,AOa]
2. 函数a 执行
// 获取 a 的scope数组
// b的 scope 为 [GO,AOa]
3. 函数b 定义
// 创建AO对象为 AOb
// b的 scope 为 [GO,AO,AOb]
4. 函数b 执行
// 获取 b 的scope数组
// c的 scope 为 [GO,AOa,AOb]
3. 函数c 定义
// 创建AO对象为 AOc
// c的 scope 为 [GO,AO,AOb,AOc]
4. 函数b 执行
// 销毁AOc
5.函数c执行完成
// 销毁AOb
5.函数b执行完成
// 销毁AOa
5.函数a执行完成
//最后还是只剩GO对象
scope的销毁
-
当一个函数执行完毕时会销毁scope_chain的最顶端指向,回归到被定义时的环节
- 也就是销毁AO对象
-
当再次被执行时会重新指向一个独一无二的执行期上下文
函数作用域实例
var bb = 1;
function aa(bb) {
bb = 2;
alert(bb);
};
aa(bb);
alert(bb);
// 2 1
// 函数形参等于是函数内容的局部变量,如果值被修改,不会覆盖全局同名值
递归
(时间空间复杂度较高 不实用)
-
规律
- 每一步需要拿到上一次的结果做什么
-
出口
- 到哪一步需要结束,将最后的返回,最后实现每一次都是拿到上一次的结果做计算
//例子:实现阶乘
var num = parseInt(window.prompt("输入所求阶乘")
function test(num) {
if(num==0 || num==1) {
return 1
}
//层层调用本身 遇到出口再返回
return num*test(num-1);
}
test(num)
Js运行过程(编译)
-
1.语法分析 通篇执行,扫描低级语法错误
-
2.预编译
- 生成执行期上下文对象(AO,GO)
-
3.解释执行
window对象
mply global(暗示全局)
-
1.任何变量如果未经声明就赋值,此变量则归全局对象所有(window对象)
-
a =10; //等于一下写法,会自动放入window对象 window.a = 10;
-
-
连等赋值的时候,没被var包括到的变量,默认为全局变量,归window所有
-
function test() { var a = b = "123" }
-
-
全局变量
// 一切声明的全局变量都是window对象的属性,window就是全局
var a = 10;//window { a:10;}
//等于
window.a = 10;
函数预编译
预编译过程发生在函数执行的前一刻
function fn(a){
// AO{
// a:function a(){},
// b:undefined,
// d:funciton d(){}
// }
console.log(a); // function a(){}
var a = 123; // AO{a:123} 覆盖函数定义 函数定义优先于变量,
console.log(a); // 123
function a() {} // 函数声明 此处声明已经提示到函数最顶部,不会再次声明
console.log(a); // 123
var b = function () {} //函数表达式 只声明变量b 到赋值阶段才能读到
console.log(b); //function (){}
function d() {}
}
fn(1);
每次执行一个函数的时候都会创建一个独一无二的(AO)对象,多次调用会多次创建,函数执行完毕会立即销毁
-
创建AO对象(activation Object)
-
// 函数执行期上下文 AO =>{}
-
-
找形参和变量声明,将形参名和变量名作为AO对象的属性名,值为undefined
-
AO => { a:undefined,b:undefined}
-
-
将实参和形参统一
-
AO => { a:1,b:undefined}
-
-
在函数体里面找函数声明,值赋予函数体
-
AO => { a:function a() {},b:undefined,d:function d() {}}
-
全局预编译
//GO{a:123}
var a = 123;
function a(){}
//123
console.log(a);
-
生成一个GO对象(Global Object ) ,此时 GO===window
-
找变量声明,值为undefined
GO => { a:undefined} -
找函数声明,值赋给函数体
GO => {a: function a() {}} -
执行 a = 123的赋值操作
Go => {a:123}
逻辑总结
-
先根据var x 创建变量挂载在OA对象上,形成
-
{x:undefined }
-
-
查找是否有函数,如果有的话 先进行函数赋值
-
{x:fnA}
-
-
然后如果还有 x= 'xxx' 的赋值操作,则执行赋值操作 覆盖之前的变量
-
{ x = 'xxx'}
-
闭包
内部的函数被保存到外部(由于自身scope的指向没有被销毁,还是能够访问已保存的scope指向)
function a() {
var aaa = 123;
function b() {
console.log(aaa);
}
return b;
}
var glob = 100;
var demo = a();
demo()
-
创建GO对象
- 创建 function a
- 挂载glob 属性
-
执行 demo = a()
-
a执行创建AOa
- 创建函数b 挂载到AOa
- 创建 变量 aaa 并赋值 123,然挂载 aaa 到AOa
- 返回 函数 b
-
此时 demo 保存b 函数到了GO环境, 拿到了 b 变量的 scope
- 此时该 scope 还保存着 AOa,由此可以访问 函数a 的属性 aaa
-
a函数执行完理因被销毁,但是由于b 函数被保存到GO环境
-
又由于GO不会被销毁的情况下,a 函数的AOa也不会销毁
-
而b的scope中还在指向AOa,就还可以使用AOa,所以之后使用b就能随意用AOa
缺点:会导致原有作用域链不释放,造成内存泄漏(占用内存)。
具体实现
1.利用闭包实现共有变量
//累加器
function Foo() {
var i = 0;
return function() {
console.log(i++);
}
}
var f1 = Foo(),
f2 = Foo();
f1(); // 1
f1(); // 2
f2(); // 1
//每次闭合指向的函数中使用到的父函数的属性,都是唯一的.独立的 且不会重复和覆盖
2.利用闭包实现缓存
function eater() {
var food = "";
var obj = {
eat:function () {
console.log("i am eat"+" "+food);
food = "";
},
push:function (myFood) {
food = myFood;
}
}
return obj;
}
var eater1 = eater();
eater1.push('banana');
eater1.eat(); // banana
3.利用闭包属性私有化
function Deng(){
//正常情况下自身是不能访问该属性,但是该属性会被保持到外部形成闭包
var name = 'deng';
this.say = function () {
console.log(name);
}
//函数通过自身的方法可以访问私有变量
}
}
var deng = new Deng();
deng.name // undefined
deng.say() //deng
4.模块发开发,防止污染全局变量
var name = 'bcd';
var init = (function (){
var name = 'abc';
function callName(){
console.log(name);
}
return function (){
callName();
}
}())
init();
总结
把里面的函数保存到外部必然形成闭包。
立即执行函数
用于针对初始化功能的函数,执行完立即会被销毁(函数具备的操作立即执行函数都要,包括AO)
(function (){})()
立即执行函数为什么可以这么写
- 通过括号将函数声明变为表达式,只有表达式才能使用执行符号
var a=(function(a, b, c){ return a+b+c;}(1,2,3)) //标准语法
通过立即执行函数解决污染全局变量的问题
//普通函数在前面加上一个数字符号就能将其变为立即执行函数
+function(){}()
原型
在函数声明时创建 为一个空对象
-
定义
- 原型是function对象的一个属性,它定义了构造函数制造出的对象的公共祖先。
- 通过该构造函数产生的对象,可以继承该原型的属性和方法。原型也是对象。
function Person (age){
this.age = age;
}
Person.prototype.name = 'qugao';
var person = new Person(19);
console.log(person.name); // 到原型访问name属性
原型的第一个应用
//提取共有属性
function Car(color, owner){
this.color = color;
this.owner = owner;
}
Car.prototype.height = 14000;
Car.prototype.lang = 4900;
Car.prototype.carName = 'BMW';
var car = new Car('red', 'qugao');
console.log('颜色:'+car.color,'持有者:'+car.owner,'车高:'+car.height,'车长:'+car.lang,'车名:'+car.carName);
3.查看对象的构造函数
- constructor 属性
function Car() {}
var car = new Car();
//car.constructor =>funtion Car(){}
- 通过prototype对象修改constructor的指向
function Person(){}
Car.prototype = {
constructor:Person
}
function Car(){}
var car = new Car();
console.log(car.constructor); //fn Person(){}
4.proto 属性存放的是对象指向的原型
- 构造函数this对象的属性
- 系统规定的隐式属性
- 可以修改当前对象的原型指向
Person.prototype = {
name : 'qugao',
age : 20,
sex : 'male'
}
function Person (){
// var this = {__proto__ : Person.prototype }
}
var person = new Person();
console.log(person.__proto__); //{name : 'qugao',age : 20}
通过__proto__属性修改原型指向
Person.prototype.name = 'abc';
function Person(){}
var obj = { name : 'sunny'}
var person = new Person();
console.log(person.__proto__); //{name : 'abc',constructor: fn}
person.__proto__ = obj;
console.log(person.__proto__); //{name : 'sunny' }
原型链
(Object.prototype是所有原型的最高级)
和作用域链的区别
-
相同点
- 通过_proto_属性存放原型,由自身依次访问上级
-
不同点
- 需要通过一级一级继承实现
Td.prototype.lastName = 'deng';
function Td() { }
var td = new Td();
Father.prototype = td;
function Father(){}
var father = new Father();
Son.prototype = father;
function Son() {}
var son = new Son();
原型链的增删查改
//只有当前构造函数能增删查改
//原型链的删除
delete Td.prototype.age
son.age //->undefined
this指向
this (函数作用域上下文)
- this代表的就是触发函数执行环境中的对象内容
- window 或者 AO 对象
this 的指向的核心规则
- 谁调用该方法this就指向谁
Person.prototype = {
name : 'gao',
sayName : function () {
console.log(this.name);
}
}
function Person(){
this.name = 'b';
}
var person = new Person();
console.log(person.sayName); // b
console.log(Person.prototype.sayName); // gao
this指向
非箭头函数下
-
指向调用其所在函数的对象,而且是离谁近就是指向谁。
- 也就是谁调用该函数,该函数的this指向谁
- obj.xxx() (xx是个函数) 那么this指向obj
-
构造函数下,this与被创建的新对象绑定,
-
DOM事件,this指向触发事件的元素
-
原型的this与函数用法一样
- 谁调用原型的方法this指向谁
具体分析 1.全局环境下:在全局环境下,this始终指向全局对象(window),无论是否严格模式
console.log(this.document == document) //true
console.log(this == window ); //true
this.a = 37;console.log(window.a); //37
函数调用this指向
函数直接调用,this分两种情况
-
严格模式和非严格模式
-
非严格模式下,this默认指向全局对象(window)
-
function f1(){ return this; } //GO对象调用f1方法,指向window f1() === window ; //true
-
-
严格模式下,this为undefined
-
"use strict"; function f2(){ return this;} f2() === undefined; //true
-
对象中的this
-
函数的定义位置不影响其this指向,this指向只和调用函数的对象有关
- 也是谁调用指向谁
-
var o = { prop : 37, f : function (){ console.log(this); return this.prop; } }; console.log(o.f()); //多层函数嵌套 this指向就近的对象 this指向f对象 f中没有prop 就在作用域链往上找 =》37
-
多层嵌套的对象,内部方法的this指向离被调用函数最近的对象
- window也是对象,其内部对象调用方法的this指向内部对象,而非window
-
var o = { prop : 36 }; function independent(){ console.log(this); // 第一次是对象o ,第二次是对象window ,第三次是对象b return this.prop; } o.f = independent; //多层函数嵌套指向就近对象(对象o调用),指向o,o中存在prop属性 打印36 console.log(o.f()); //生成闭包 this指向window var a = o.f; //window中没有prop属性 返回 undefined console.log(a()); o.b = { g : independent, prop : 42 }; //多层嵌套指向就近(b对象调用),指向b对象 =》42 console.log(o.b.g());
原型链中this
原型链中的方法的this仍然指向调用它的对象,
var o = {
f:function() {
console.log(this);
return this.a + this.b;
}
};
//Object.create(o) 的意思是讲 p的_proto_属性指向o的prototype 实现原型链
var p = Object.create(o);
p.a = 1;
p.b = 4;
//此时p调用父级原型的f方法但是this指向的是调用该方法的对象
console.log(p.f());
构造函数中this
- 构造函数中this都会与新创建的对象绑定
- 在遇到new关键字之后,构造函数会隐式创建一个this的空对象
- 然后将下面的this.xx写入该空对象,最后返回,如果有自己写返回则this对象会被替换
function Person(){
this.a = 37;
}
var f = new Person();
console.log(f.a); //37
function Person(){
this.a = 37;
return {a: 38}; //自定义返回对象覆盖应该返回的this对象
}
var f = new Person();
console.log(f.a);
settimeout
const c = { d:'name',say:function() {
setTimeout(function(){
function todo() { console.log(this)}
todo()
},1000)
}
}
// 此时this指向window
// 因为定时器是在window上的属性
// 此时定时器定时器里面的代码被放到了全局环境下运行
c.say()
call&apply
call和apply中this指向第一个参数所写的对象
function add(c, d){
console.log(this.a + this.b + c + d) //16 //34
}
var o = {a:1, b:3};
add.call(o,5,7);
add.apply(o,[10,20]);
this指向总结
-
函数预编译过程
- this->window
-
全局作用域
- this->window
-
call和apply可以改变函数运行时this的指向
-
obj.function();
- func()(里面的this指向obj)
-
定时器这种会在全局环境下执行,所以this会指向window
call
借用别人的方式实现自己需要的功能
- call第一个参数是对象(改变this指向)
- 第二个是正常的传参(一一对应)
function Person(name, age) {
this.name = name ;
//obj.name =zhao;
this.age = age;
//obj.age = 300;
console.log(this);
}
var person = new Person('deng',100); //第一次this指向
Personvar obj = {}
Person.call(obj,'zhao',300); //第二次this指向obj
apply
xx.apply(this,[xx]) //第一个参数为对象,第二个必须为数组[arguments]
call和apply的作用是用来改变this的指向,区别是传参列表不同
-
继承的实现
- 通过原型链实现继承 层层继承
- call,apply方法 借用构造函数
-
共享原型 多个构造函数公用一个原型
- 改一个原型属性会导致所有的原型属性都被修改
-
Father.prototype.lastName = 'qugao'; function Father(){} function Son(){} Son.prototype = Father.prototype; var son = new Son();
圣杯模式
// 通过一个新的构造函数实现中间继承
function inherit(Target, Origin){
function F(){}
// 保存原来的原型
F.prototype = Origin.prototype;
// 这一步主要实现 修改新的构造函数 __proto__ 隐式原型的指向
Target.prototype = new F();
// 修改新的原型指向的构造函数
Target.prototype.constuctor = Target;
// 保存原来的构造函数原型
Target.prototype.uber = Origin.prototype;
}
function Father (){}
function Son(){}
inherit(Son, Father);
var son = new Son();
var father = new Father();
箭头函数中this
-
由于js的this是在运行时被创建,箭头函数也是一样(稍微不一样)
-
但是箭头函数本身不会创建this,他会捕获父级作用域的this做为自己的this
- 在创建的时候会就会绑定this
- 根据作用域链一直往上找,直到存在this为止
// 这种情况下
const a =function (){
const b = () => {
return () => {
console.log(this)
}
}
return b
}
a()()() //此时this 还是指向window
//--------------
//使用一个对象去接收这个a方法
const d = { say : a}
d.say()()()
// 这个时候 this 指向的是d 因为在function的时候this指向d,后续箭头函数都会使用这个this
//--------------
// 在settimeout中 使用箭头函数保证this指向准确
const c = {
d:'name',
say:function() {
setTimeout(()=>{
console.log(this)
},1000)
}
}
try catch()
try {} 在try里面发生错误,不会执行错误后的try里面的代码
catch(e){} 如果try里面发错误,catch会捕捉错误并将错误信息封装到一个对象里面(e),如果try没发生错误就不会执行catch
//第一个语句执行 第二个发生错误
//不执行后续代码
try{
console.log("a")
}catch(e){
console.log(e);
}finally{
console.log('finally')
}
//只能紧跟在catch后括号后
//执行完try catch之后立即执行finally ,
//是否报错都会执行finally
error.name(错误名称) 的六种值对应的信息
- EvalError : eval()的使用与定义不一致 ->eval() 不允许自己使用
- RangeError : 数值越界
- ReferenceError : 非法或不能识别引用数值
- SyntaxError :发生语法解析错误
- TypeError : 操作数类型错误
- URLError : URL处理函数使用不当
严格模式( es5)
“use strict”
-
不再兼容es3的一些不规则语法(和es3产生冲突的部分)。
-
使用全新的es5规范
- 全局严格模式(最顶端)
- 局部函数内严格模式(推荐)
-
就是一行字符串,不会对不兼容严格模式的浏览器产生影响。
-
不支持with,arguments.callee,func.caller,变量赋值前必须声明,
-
局部this必须被赋值(Person.call(null/undefined)赋值什么就是什么),拒绝重复属性和参数
with(){}
-
作用
- 改变当前执行的作用域链,括号里面传的参数将会放在作用域最顶端(作用域链开头)
-
缺点
- 增加js内核消耗,影响执行速度(修改作用域链消耗内存)
var person = { name : "gao"}
function test (){
var name = "qu";
with(person){
console.log(name)
}
}
test();
类数组
- 可以利用属性名模拟数组的特性
- 可以动态的增长length属性
- 如果强行让类数组调用push方法,则会根据length属性值的位置进行属性的拓充 类数组的实现
- 属性要为索引(数字)属性,必须要有length属性
- 优点:存储数据更加方便,将对象和数组结合在一起
- 缺点:不是所有的数组的方法类数组都能用
var obj = {
"0" : "a",
"1" : "b",
name : "gao",
"length" : 2,
"push" : Array.prototype.push,
"splice": Array.prototype.splice
}
obj.push('c');
obj.push('d');
类数组push方法实现
Array.prototype.push = function(target) {
obj[obj.length] = target;
obj.length ++;
}
DOM对象
DOM ( Document Object Model )
- DOM定义了表示和修改文档所需的方法。DOM对象即为宿主对象,由浏览器厂商定义,用来操作html和xml功能的一类对象的集合。又有人称DOM是对html以及xml的标准编程接口
- DOM对象实际上指的是通过document捕获的标签对象(DOM元素)
- DOM对象操作实际上是修改该对象的属性(包括行内css)
- document代表整个文档
对DOM对象进行修改css
var dom = document.getElementsByTagName("")[0];
dom.style.color = "red"; //修改字体颜色
创建节点
createElement
// 过程:先创建再将该节点放入需要的容器中
//第一步 创建节点
var div = document.createElement("div");
var div1 = document.getElementById("div");
//第二步 将节点放入需要的容器中
// 放入普通节点的写法
div1.appendChild(div);
//放入body中的写法
document.appendChild(div);
DOM基本操作
查看元素节点(捕获节点对象)
//通过元素id捕获
//返回的是单个对象 只会捕获到第一个
//在ie8以下兼容捕获name和id不区分大小写
document.getElementById("");
//通过类名捕获,类数组,ie8以及以下都没有
document.getElementsByClassName("");
//通过标签名捕获,选取后返回的是类数组
document.getElementsByTagName("");
//通过标签name属性捕获,返回的是类数组,节点列表
//只有部分标签name可以生效(能提交数据的标签form等)
document.getElementsByName();
//query用于保存副本原件消失副本不会消失//选择符合的标签
//只会捕获第一个元素 以下两个选择器 ie7和以下的版本没有
document.querySelector("div > span a");
//选择所有符合的标签 返回的是类数组
document.querySelectorAll('div');
遍历节点树
每个html文档的结构为一个树形,html为最顶端也就是document
- parentNode ->父节点(最顶端的parentNode为#document)
- childNodes ->子节点们 (所有子节点)
- irstChild ->第一个子节点
- lastChild ->最后一个子节点
- nextSibling ->后一个兄弟节点
- previousSibling ->前一个兄弟节点
基于元素节点树的遍历
(元素节点就是html标签) (元素节点的根节点没有document)
- a.parentElement ->返回当前元素的父元素节点 (IE不兼容)
- b.children ->只返回当前元素的元素子节点
- c.childElementCount ===children.length 当前元素的子元素节点个数(IE不兼容)
- d.firstElementChild ->返回的是第一个元素节点(IE不兼容)
- e.lastElementChild -> 返回的是最后一个元素的节点(IE不兼容)
- f.nextElementSibling/previousElementSibling ->返回后一个/前一个兄弟元素节点(IE不兼容)
html节点的类型
(nodeType返回的值)
元素节点
- --1 属性节点
- --2 文本节点
- --3 注释节点Comment --8 document--8 DocumentFragment(文档碎片) --11
例子:div.childNodes
NodeList(9) [text, comment, text, strong, text, span, text, em, text]
0: text //空格、注释和文字都是文本节点
1: comment //元素节点
2: text
3: strong
4: text
5: span
6: text
7: em
8: text
length: 9
节点的四个属性
- a.nodeName : 元素的标签名,以大写形式表示,只读
- b.nodeValue : text节点或Comment(注释)节点的文本内容,可读写
- c.nodeType :该节点的类型,只读
- d.attribute :Element节点的属性集合
节点的一个方法
Node.hasChildNodes();
注意
- getElementById 方法定义在Document.prototype上 所以Element不能使用,只能使用documen调用
- getElementsByName 方法定义在HTMLDocument上,意思是只有html中的document能够使用(xml的document Element都不能使用)
- getElementsByTagName 方法都能使用,括号里面为*则是选择所有元
- HTMLDocument.prototype 定义了一些常用的属性,body,head 分别指代html文档中的标签 document.head ->标签
- Document.prototype上定义了documentElement属性,指代文档的根元素,在HTMl文档中,他总是指代元素。
- document.documentElement -> 元素
- getElementClassName、querySelectAll、querySelector、Document.prototype,Element.prototype类中均有定义
创建节点
a.创建元素节点
//括号里面写什么标签名创建的就是什么标签
document.createElement();
b.创建文本节点
// 里面写什么就是什么文本
document.createTextNode();
c.创建注释节点
//写什么就是什么
document.createComment();
d.创建一个新的空白的文档片段
document.createDocumentFragment();
节点API
插入节点
(.号前面是父亲节点) 实质是将节点进行剪切
PARENTNODE.appendChild();
(将a节点插入到b节点之前)
PARENTNODE.insertBefore(a, b);
删除节点
//通过父亲节点删除节点,实际上并不是删除而是剪切
parent.removeChild();
//通过自身节点删除自己,同样是剪切
child.remove();
替换节点
//父级调用,拿新的元素替换老的元素(也是剪切)
parent.replaceChild(new ,origin);
DOM对象属性
-
innerHTML 属性,直接使用该属性返回的是该标签的整个节点结构,直接赋值会覆盖整个结构,可以使用+=添加内容, 可以修改整个节点的结构
//直接生成span节点覆盖之前的节点,而且还带有行内样式 div.innerHTML = "<span style =' color : red' >123</span>" -
innerText(火狐不兼容),返回的是节点中的文本内容,赋值修改的时候要注意节点的结构,直接赋值的话会影响节点的结构。
//会将里面的span节点覆盖,变成纯文本节点 div.innerText = 123; -
textContent (等于innerText) ——》老版本IE不好使 一般使用innerText
Element节点的API
//设置元素节点属性,可以加id,class等属性
ele.setAttribute(属性名,属性值); //获取元素节点属性
ele.getAttribute(属性名);
事件对象
绑定事件处理函数
ele.onxxx(事件类型) = function(){}
在一个元素的事件上绑定一个处理函数 a.兼容性很好,但是一个元素的同一个事件上只能绑定一个处理函数 b.基本等同于写在html行间上 -》句柄绑定方法
div.onclick = function(){} == <body onclick = "console.log()"></body>
obj.addEventListener(type,fn,false)
//type -> 事件类型 而且是字符串 "click"
第一个参数//fn -> 处理函数
第二个参数//false ->冒泡还是直接捕获
第三个参数//IE9以下不兼容,可以为一个事件绑定多个处理函数
IE独有方法
obj.attachEvent("on"+type,fn);
注意:一个事件可以绑定多个处理程序
事件处理程序的运行环境(this指向)
//程序this指向是dom元素本身
//程序this指向是dom元素本身
ele.onxxx = function(event){}
obj.addEventListerner(type,fn,false)//程序this指向
windowobj.attachEvent('on'+type,fn)
封装添加事件兼容的方法
function addEvent(elem, type, handle){
if(elem.addEventListener){
elem.addEventListener(type, handle, false)
}else if(elem.attachEvent){
elem.attachEvent('on'+type,function(){
handle.call(elem)
})
}else{
elem['on'+ type] = handle;
}
}
解除事件处理程序
注意:若绑定匿名函数则无法解除)
ele.onclick = false|''|null
//这个里面的fn最好不要用匿名函数,否则不好解除
ele.removeEventListerner(type, fn, false);
//ele.datechEvent('on'+ type,fn)
//例子:
div.onclick = function(){
console.log('a');
this.onclick = null;
//解除绑定事件
}
div.addEventListener("click",test,false);
div.removeEventListener('click',test,false);
function test(){
console.log("a");
}
事件处理模型
执行顺序
事件冒泡、捕获的执行顺序为 先捕获后冒泡+先绑定先执行
事件冒泡
- (非视觉上)嵌套关系的元素,会存在事件冒泡的功能,即同一事件,自子元素冒泡向父元素。
- (自底向上)(代码结构上的冒泡)
- 为什么会冒泡: 子元素继承父级元素的绑定事件
事件捕获
- (非视觉上)嵌套关系的元素,会存在事件捕获的功能,即同一事件,
- 自父元素捕获至子元素(事件源元素)(自顶而下)。(与冒泡事件传递方向相反,先外面再里面)
- (IE没有捕获事件)
- focus,blur,change,submit,reset,select等事件不冒泡
取消冒泡和组织默认事件
-
取消冒泡 a.W3C标准event.stopPropagation(); 但不支持IE9以下版本 b.IE独有event.cancelBubble = true; c.封装取消冒泡的函数 stopBubble(event);
-
2.阻止默认事件
- 默认事件-表单提交,a标签跳转,右键菜单等
-
//只有用句柄的方式绑定事件时才有用(也就是行间里面写)return false; 以对象属性的方式注册的事件才生效 //W3C标注,IE9以下兼容 event.preventDefault(); //兼容IE event.returnValue = false;
特例:
<a href = "javascript:void(false)"></a>
// 同样可以取消默认事件,:后面写代码在点击a标签会执行js代码
事件对象
每个事件执行的时候系统会把所有的事件属性封装在一个对象中,可以在处理函数形参定义来接受
事件对象属性解析
-
targe :事件源对象 用法 e.target 返回的是被触发事件的元素节点
-
clientX/clientY 返回鼠标在窗口客户区域中的x/y坐标。
-
screenX/screenY 返回鼠标相对于用户屏幕的x/y坐标
-
x/y 获取鼠标指针位置相对于父文档的 x/y像素坐标。
- 在IE中。IE8以下的版本并非是相对文档。它们的值和clientX,clientY的值一样,相对窗口客户区
-
layerX/layerY 返回相对于触发事件的对象,鼠标位置的x/y坐标
-
pageX/pageY 获取鼠标指针位置相对于父文档的 x/y像素坐标
-
preventDefault() 阻止执行默认动作
兼容属性判断
var event = e || window.event(IE中独有);
事件源对象
var target = event.target || event.srcElement;
通过冒泡机制和事件源对象写的例子
var ul = document.getElementsByTagName("ul")[0];
ul.onclick = function(e){
var event = e || window.event;
var target = event.target(火狐只有这个) || event.srcElement (IE只有这个);
console.log(event.target.innerText);
}
//首先通过给父级元素添加事件,然后通过冒泡机制所有的子元素都会获得父级元素的事件,最后是通过事件源获取li的属性
鼠标键盘事件分类
鼠标事件
click,mousedown(鼠标按下),
mousemove(鼠标移动),
mouseup(鼠标松开),
contextmenu(鼠标右击显示菜单),
mouseover(鼠标经过),
mouseout(鼠标完全离开),
mouseenter(进入或者穿过),
mouseleave(鼠标指针离开)
- mouseover和mouseout在父元素和其子元素都可以触发,当鼠标穿过一个元素时,触发次数得依子元素数量而言。
- mouseenter和mouseleave只在父元素触发,当鼠标穿过一个元素时,只会触发一次。
- mouseover和mouseout比mouseenter和mouseleave先触发
区分鼠标是左键点击还是右键点击
- 只有onmousedown和onmouseup的事件对象中button属性才能判断 0是左键 2是右键
- DOM3规定click只能监听左键
键盘事件
//只有三个事件
onkeydown,
onkeypress,
onkeyup
//触发顺序
onkeydown > onkeypress > onkeyup
//当一个按键按下去的事件先触发keydown 然后是keypress 最后松开触发keyup
区别
- 可以监听所有的键盘按键onkeydown
- 只能监听字符类按键(ASCII码) 可以通过charCode属性返回按键ASCII码
- 最后通过string.fromcharcode 来转换为字符onkeypress
键盘事件对象属性
key 返回按键名 还会区分大小写
文本操作事件
oninput
//里面的内容改变就会触发事件
onchange
//修改内容后失去焦点触发事件
onfocus/onblur
//获得/失去焦点触发事件
系统触发事件
1.sroll
日期对象
//Date()
系统提供的日期构造函数
var date = new Date();
date对象的API
//Date();
返回当前时间的详细时间信息
var date = new Date();
//返回当前时间并将信息封装在新对象中
//返回date对象中的日期是一周中的第几天 0 -6 星期日是0
var day = date.getDay();
var date1 = date.getDate();
//返回date对象中的日期是一个月中第几天 1 -31
var month = date.getMonth()+1;
//从date对象中返回月份 0-11
var year = date.getFullYear();
//返回年份 -》 不要使用
getYearvar hours = date.getHours();
//返回当前时间的小时数
var minutes = date.getMinutes();
//返回当前时间的分钟数
var seconds = date.getSeconds();
//返回当前时间的秒数
var milliSeconds = date.getMilliseconds();
//返回当前时间的毫秒数
var times = date.getTime();
//返回自1970年1月1日(纪元时间)至今的毫秒数
定时器
setInterval()
每多少秒执行一次 无限次执行(返回本次定时器的唯一标识) serTimeout
每多少秒执行一次 执行一次(返回本次定时器的唯一标识)
//无限执行函数
var 无限定时器标识 = setInterval(function(){},time)
//多少秒执行一次 但是只执行一次
var 有限定时器标识 = serTimeout(function() {},1000);
//把前面的字符串当js代码执行
setInterval("字符串",100)
clearInterval,clearTimeout 清除对应定时器
clearInterval(无限定时器标识)
clearTimeout(有限定时器标识);
定时器方法全是定义在window上的 所以其中的this指向window
arguments
实参列表 用于接收调用方法传来的实参所形成的对象
- 等于形参第一项arguments[0]
- 如果argumens的值被修改,形参接收的值也会被修改
function test(name, age){
arguments[0] = 'hh';
this.name = name;
console.log(this.name)
}
test('qugao', 12);
argumenrts.callee
指向自身引用 常用于立即执行函数
function test(){
console.log(arguments.callee);
//arguments.callee == test
}
test();
var foo = (function (n){
if(n == 1) return 1;
return n * arguments.callee(n - 1);
//此时arguments等于自身这个立即执行函数})(100)
caller
是方法自带的属性 返回自身被调用的环境(执行自己的那个方法)
function aa(){
dd();
}
function dd(){
console.log(dd.caller);
}
aa();
JSON对象
- json是一种传输数据格式
- (以对象为样板,本质上就是对象,但用途有区别,对象就是本地用,json是用来传输的)
- 为了区别json和普通对象,规定所有的属性名都要加双引号
JSON = {"key":"value"};
两个方法: JSON是一个静态类,类似于Math,里面封装了json的方法
JSON.parse();
// string - >json 字符串转为json格式JSON.stringify();
// json - >string json格式转为字符串
渲染引擎机制
1.先回进行html代码扫描,生成htmlDOM树(文件下载是在解析最后才完成)
先是深度优先再是广度优先
//模拟DOM树
html 1
head 2
body5title 3
meta4
div6 .....
2.DOMtree解析完之后就进行cssTree的解析
3.cssTree解析完之后 渲染引擎会将cssTree和domTree拼在一起,形成randerTree,此时渲染引擎才会真正的开始进行页面绘制(一像素一像素的绘制 从上到下)
4.页面重排(重构)(影响页面效率) reflow(应该避免)(影响布局的操作会导致页面重排)
- DOM节点的添加、删除,dom节点的宽高变化、位置变化
- dispaly none -> block
- offsetWidth offsetLeft(会重构randerTree)
5.repaint 重绘 (影响相对较小) 只修改一部分 (改背景图片,字体颜色等不影响页面布局的)
异步加载JS文件
- 不阻塞js文档的加载 另外开辟时间线
JS加载的缺点:加载工具方法没必要阻塞文档,过多的js加载会影响页面效率,一旦网络不好,那么整个网站将等待js加载而不进行后续渲染等工作。
异步加载的三种方案
-
defer (只有IE能用)异步加载 写在工具方法的标签中 但是也要等到DOM文档全部解析完才会被调用,可以在标签内写代码
例子: <script src = 'xxx.js' defer></script> -
async
- 异步加载,加载完就执行,async只能加载外部脚本,不能把js写在script标签里面
-
通过创建DOM再插入形成异步
var script = document.createElement('script'); script.type = "text/javascript"; script.src = 'xxx.js'; script.onload = function(){ test();} document.body.appendChild(script);
JS加载时间线
- 创建Document对象,开始解析web页面。解析HTML元素和他们的文本内容后添加ELement对象,和Text节点,这个阶段document.readyState = 'loading'
- 遇到link外部的css,创建线程加载,并继续解析文档。
- 遇到script外部js,并且没有设置async、defer,浏览器加载,并阻塞,等待js加载完成并执行该脚本,然后继续解析文档。
- 遇到script外部js,并且设置有defer、async,浏览器创建线程加载,并继续解析文档。对于async属性的脚本,脚本加载完立即执行。(异步禁止使用document.write())
- 遇到img等,现正常解析dom结构,然后浏览器异步加载src,并继续解析文档
- 当文档解析完成,document.readyState = 'interactive'
- 文档接续完成后,所有设置defer的脚本会按顺序执行。(注意async的不同,但同样禁止使用document.write())
- document对象触发DOMContentLoaded事件,这也标志着程序从同步脚本执行阶段,转化为事件驱动
- 当所有async的脚本加载完成并执行后、img等加载完成后,document.readyState = ‘complete’,window对象触发load事件
- 从此,以异步响应方式处理用户输入、网络事件等
精简 -> 文档解析是指DOM树构造
- 创建Document对象并开始解析页面 此时readyState 为 ‘loading’
- 继续即解析,
- 遇到link创建异步线程加载src文件,
- 遇到外部js没有设置异步就阻塞等待文件下载,有设置异步就创建异步线程加载外部文件,
- 遇到img也是先解析dom结构然后浏览器会异步加载src。
- 文档解析完成,此时readyState = ‘interactive’,设置defer的脚本会顺序执行,完成文档加载进入事件驱动(click)之类的交互监听事件。
- 当所有异步线程(src async)执行完后,eadyState = ‘complete',window对象触发load事件,从此,以异步响应方式处理用户输入、网络事件等
主要是分三步
- 创建Document对象开始解析文档 readyState ='loading'
- 文档解析完成 readyState =‘interactive’
- 文档解析并加载、执行完 readyState =‘complete'
验证文档加载过程
//1.在文档中直接打印readyState console.log(document.readyState);--->loading
//2.使用window.onload事件window.onload = fn(){console.log(document.readyState);}->complete
//3.使用文档加载监听事件xx.onreadystatechange = fn(){document.readyState);} -》interactive
浏览器解析 dom 过程
什么是 dom
当我们浏览一个页面,浏览器通过网络线程获取页面内容,这个时候获取到的只是 html 文件字节流,html 文件字节流是无法被渲染引擎理解的,所以要将先将其转化为渲染引擎能够理解的结构,这个结构就是 dom,也就是常说的 dom 树,表现形式就是我们肉眼可见的一个个 html 标签。
dom 树如何生成在渲染引擎内部,有一个叫 html 解析器(HTMLParser)的模块,它的职责就是负责将 html 字节流转换为 dom 结构。参考上图,解析过程分2步:
- 通过分词器将字节流转换为 token;
- 将 token 解析为 dom 节点,并将 dom 节点添加到 dom 树中。
在这个过程中,大家要注意到的一点就是 html 解析器并不是等整个文档加载完成之后再解析的,而是网络进程加载了多少数据,html 解析器便解析多少数据。
dom 遇上 js
在解析到 `<script>` 标签之前,解析流程还是和上面介绍的一样。但是解析到 `<script>` 标签时,渲染引擎判断这是一段脚本,此时 html 解析器就会暂停 dom 的解析,因为接下来的 js 可能要修改当前已经生成的 dom 结构.
如果 `<script>` 标签内引入的是外部 js 资源,也需要等 js 文件下载完成以后,再继续后面的 html 解析工作。如果网络较慢,那么用户会明显的感觉到页面卡顿,不流畅。所以,**js 会阻塞 html 的解析**。
js 直接操作 dom 的缺陷
当我们调用 `document.body.appendChild(element)` 往 body 节点上添加一个元素,首先渲染引擎会将 element 节点添加到 body 节点之上,然后触发样式计算、布局、绘制、栅格化、合成等任务,我们把这一过程称为重排。除了重排之外,还有可能引起重绘或者合成操作,可谓牵一发而动全身。如果每次修改界面都是直接修改 dom,虽然对于简单的应用,无不会感到有什么问题,但是对于复杂的应用,页面结构比较复杂,不只是代码不好维护,性能上也受到很大的影响。
正则表达式
正则表达式 RegExp
1.创建
-
字面量创建
-
var reg = /abc/; (推荐使用)
-
-
b.构造函数创建
-
//(不加new引用地址相同,类似于对象构造) var reg = new RegExp('字符串')
-
2.属性
i
//忽视大小写 g
//执行全局匹配(匹配到最后一个字符)m
//多行匹配 一个字符串中有\n就需要用m匹配另一行var reg = /abc/igm;
3.表达式
[] 一个代表式一位 里面写只能取的内容(可以使用区间表示)
//a-z A-Z都可以var reg = /[a-z A-Z]/igm;
//[A-z]
按ASCII表顺序var reg = /[A-z]/igm;
写在区间里面叫非 除这个之外都可以
//叫或,符合这两个任意一个(abc|bcd)
//这两个任意组合加一个数字
(abc|bcd)[0-9]
var reg = /(abc|bcd)[0-9]
/igm;
4.元字符
\w
// 代表的区间是 [0-9A-z _]\W
// 和\w取反\d
// 0-9\D
//\d取反\s
//空白字符
\t制表符(table)
\n换行符
\r回车符
\v垂直制表符
\f换页符\S
//和\s取反\b
//匹配单词边界
"abc abc"
a c就是单词边界\B
//和\b取反
"abc abc"
bunicode编码 // \u0000
5.量词
贪婪匹配选择 能多久多 量词后面加?取消贪婪匹配
a. + a+
//代表a可以出现1次或者若干次 b. ? a?
//代表a可以出现1次或者0次c. * a*
// 代表a可以出现0次或者若干次 没有也会返回d. {} a{2,3}
// 里面是匹配数量,按几个几个组合,按多的取e. $
//以什么结尾
6.正则表达式的方法
1.exec方法,检索字符串中指定的值。返回找到的值,并确定其位置。
//返回类数组,带有匹配成功的字符,下标,原数组reg.exec(str)
- a.按表达式匹配,遇到符合规则的就返回,返回的是类数组,没有符合要求的就返回null
- b.执行一次exec对str的下标进行往后移动 第二次执行实在第一次执行完并返回后的记载下标开始
- c.还没开始匹配的时候lastIndex为0 一旦匹配成功lastIndex的值会变为字符串匹配成功的下标
var str = 'aa baa babab';
var reg = /[\w]{3}/g;
//匹配三个连着的字符console.log(str);
console.log(reg.lastIndex);
//0console.log(reg.exec(str));
//baaconsole.log(reg.lastIndex);
//6console.log(reg.exec(str));
//bab
2.test方法,检索字符串中指定的值。返回 true 或 false。
reg.test(str); //->返回true false
//用正则表达式的规则来验证该字符串是否符合
var str = '123abc123';
var reg = /^[\d]+/;
7.支持正则表达式的 String 对象的方法
search() 方法用于检索字符串中指定的子字符串,或检索与正则表达式相匹配的子字符串。
var str = '123abcabc';
var reg = /\d+/g;
//返回第一个匹配成功的下标
str.search(正则|字符串)
match() 方法可在字符串内检索指定的值,或找到一个或多个正则表达式的匹配。
var str = 'abc11';
var reg = /\d/g;
//返回类数组,里面存放的是符合规则字符串
str.match(正则|字符串)
replace()
方法用于在字符串中用一些字符替换另一些字符或替换一个与正则表达式匹配的子串。
-
参数1 :必需。
- 规定子字符串或要替换的模式的 RegExp 对象。如果是字符串则将它作为要检索的直接量文本模式,而不是首先被转换为 RegExp 对象
-
参数2 :必需。一个字符串值。规定了替换文本或生成替换文本的函数。
-
返回值:一个新的字符串,通过规则生成
var str = 'aavv'; //(\w)\1 表示两个一个的数 \1表示第一个符合的var reg = /(\w)\1(\w)\2/g; //此时第一个参数如果是字符串那就只会匹配一次console.log(str.replace('a','c')) //$1 表示符合正则第一个匹配的段落console.log(str.replace( reg,'$2$2$1$1')) console.log(str.replace(reg,function ($,$1,$2) { return $2+$2+$1+$1;}))
split() 方法用于把一个字符串分割成字符串数组
var str = 'aabb';
var demo = str.split('');
// 返回值:一个数组
//str.split(separator(字符串或是正则),howmany(限制可选返回字符串的最大长度))
严格模式
"use strict" 指令只允许出现在脚本或函数的开头。
为什么使用严格模式:
- 消除Javascript语法的一些不合理、不严谨之处,减少一些怪异行为;
- 消除代码运行的一些不安全之处,保证代码运行的安全;
- 提高编译器效率,增加运行速度;
- 为未来新版本的Javascript做好铺垫。
- "严格模式"体现了Javascript更合理、更安全、更严谨的发展方向,包括IE 10在内的主流浏览器,都已经支持它,许多大项目已经开始全面拥抱它
严格模式的限制
- 1.不允许使用未声明的变量(包括对象等)
- 2.不允许删除变量或对象。
- 3.不允许变量重名。
- 4.不允许使用八进制。
- 5.不允许使用转义字符。
- 6.不允许对只读属性赋值。
- 7.不允许对一个使用getter方法读取的属性进行赋值。
- 8.不允许删除一个不允许删除的属性。
- 9.变量名不能使用 "eval" 字符串。
- 10.变量名不能使用 "arguments" 字符串。
- 11.不允许使用以下这种语句。
- 12.由于一些安全原因,在作用域 eval() 创建的变量不能被调用。
- 13.禁止this关键字指向全局对象。
保留关键字
- implements
- interface
- let
- package
- private
- protected
- public
- static
- yield
vue双向绑定实现
我们可以通过Object.defineProperty这个方法,直接在一个对象上定义一个新的属性,或者是修改已存在的属性。最终这个方法会返回该对象。
Object.defineProperty(object, propertyname, descriptor)
Object.defineProperty(object,'name',{.... })
参数
- object 必需。 要在其上添加或修改属性的对象。 这可能是一个本机 JavaScript对象(即用户定义的对象或内置对象)或 DOM 对象。
- propertyname 必需。 一个包含属性名称的字符串。
- descriptor 必需。 属性描述符。 它可以针对数据属性或访问器属性。
descriptor内容:
- 【value】 属性的值,默认为 undefined。
- 【writable】 该属性是否可写,如果设置成 false,则任何对该属性改写的操作都无效(但不会报错),对于像前面例子中直接在对象上定义的属性,这个属性该特性默认值为为 true。
- 【configurable]】如果为false,则任何尝试删除目标属性或修改属性以下特性(writable, configurable, enumerable)的行为将被无效化,对于像前面例子中直接在对象上定义的属性,这个属性该特性默认值为为 true。
- 【enumerable】 是否能在for-in循环中遍历出来或在Object.keys中列举出来。对于像前面例子中直接在对象上定义的属性,这个属性该特性默认值为为 true。
- 【get】一旦目标对象访问该属性,就会调用这个方法,并返回结果。默认为 undefined
- 【set】 一旦目标对象设置该属性,就会调用这个方法。默认为 undefined。
let temp = null;
Object.defineProperty({},'name',{
get(){
retrun temp;
}
set(val){
temp = val
}
})
总结
可以得知,我们可以通过使用Object.defineProperty,来定义和控制一些特殊的属性,如属性是否可读,属性是否可枚举,甚至修改属性的修改器(setter)和获取器(getter)
实现双向绑定的原理
利用该属性给对象添加一个属性名,调用该属性名实现添加删除就会触发里面的set.get方法 实现数据的绑定
js进阶
函数防抖
概念:函数防抖(debounce),就是指触发事件后,在 n 秒内函数只能执行一次,如果触发事件后在 n 秒内又触发了事件,则会重新计算函数延执行时间。
“函数防抖”的关键在于,在 一个事件 发生 一定时间 之后,才执行 特定动作。在这个计时时间内触发事件,会导致计时时间重置,又要重新等待时间
解决方案
函数防抖的要点,是需要一个 setTimeout 来辅助实现,延迟运行需要执行的代码。如果方法多次触发,则把上次记录的延迟执行代码用 clearTimeout 清掉,重新开始计时。若计时期间事件没有被重新触发,等延迟时间计时完毕,则执行目标代码。
具体实现
function debounce(fn,wait){
var timer = null;
return function(){
if(timer){
clearTimeout(timer);
}
timer = setTimeout(fn,wait);
}
}
function handle(){
console.log(Math.random());
}
window.addEventListener("resize",debounce(handle,1000));
一般使用场景
- 搜索框搜索输入。只需用户最后一次输入完,再发送请求;
- 用户名、手机号、邮箱输入验证;
- 浏览器窗口大小改变后,只需窗口调整完后,再执行 resize 事件中的代码,防止重复渲染
立即执行版
所谓立即执行版就是 触发事件后函数不会立即执行,而是在 n 秒后执行,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。
/** *
@desc 函数防抖---“立即执行版本” 和 “非立即执行版本” 的组合版本 *
@param func 需要执行的函数 * @param wait 延迟执行时间(毫秒)
@param immediate---true 表立即执行,false 表非立即执行
***/
function abounce(fnc, wait, immediate = false) {
let timer = null;
return function () {
let content = this;
let args = arguments;
//如果是规定时间内二次 继续清除 走下面延时操作 进入冷却
if (timer) {
clearTimeout(timer);
}
//如果是立即执行版本
if (immediate) {
//如果是要触发了 timer为undefined
let callNow = !timer;
//触发后 规定时间才变空 这段时间不会触发
timer = setTimeout(() => {
timer = null;
}, wait);
callNow && fnc.apply(content, args);
//不是立即执行版本 的话 还是按原来的走
} else {
timer = setTimeout(() => {
fnc.apply(content, args);
}, wait);
}
};
}
函数节流
概念:**所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数。**节流会稀释函数的执行频率
时间戳版
function throttle(fnc, wait) {
let pre = 0;
return function () {
let now = Date.now();
let content = this;
let args = arguments;
//当前触发事件 减去之前触发时间 有1秒 则会触发
if (now - pre >= wait) {
fnc.apply(content, args);
pre = now;
}
};
}
定时器版
//定时器版
function throttle(fnc, wait) {
let timmer = null;
return function () {
let content = this;
let args = arguments;
//用于锁住当前执行的定时器 如果已经有定时器在执行 重复触发则不会执行 必须要等定时器执行完毕
if (!timmer) {
timmer = setTimeout(() => {
timmer = null;
fnc.apply(fnc, args);
}, wait);
}
};
}
高阶函数
- 接收一个函数或者多个函数作为参数的函数,如js中自带的
map some every filter reduc - 函数执行返回一个函数,如
bind
// map 接收一个函数 数组中的每一项都会执行这个方法// 返回一个新数组 数组的元素由原数组的每一项执行接收函数处理后的值
const arr = [1,5,6,44,99,36,77];
const res = arr.map(item => item + 1);
// 原数组每一项调用函数处理后返回的 新数组console.log(res)
// [2, 6, 7, 45, 100, 37, 78]
Array.prototype.myMap = function(fn){
const len = this.length;
const newArr = [];
for(let i = 0;i < len;i++){
// 原数组的每一项执行fn函数处理后的值push到新数组中
newArr.push(fn(this[i]))
}
return newArr
}
const res = arr.myMap(item => item + 1);
console.log(res);
//[2, 6, 7, 45, 100, 37, 78]
柯里化函数
const res = add(1,2)(3);
console.log(res) // 6
//解析: add 函数传入参数执行后返回一个函数再次传入参数再次执行 然后求和
const curring = () => {
// 在闭包中创建一个params集合用来预存储每次add执行所接收的实参
let parmas = [];
const add = (...args) => {
parmas = parmas.concat(args)
// 这里返回add 函数 因为我们不确定add函数执行完会调用多少次、每执行一次 接着返回一个add函数供其下次执行
return add
}
----------------------------------
function fn(x, y,z) {
return x + y + z;
}
function curry(fn) {
const preArgs = Array.prototype.slice.call(arguments, 1);
const that = this;
return function () {
const args = Array.from(arguments);
const totalArgs = preArgs.concat(args);
if(totalArgs.length >= fn.length) {
return fn.apply(null, totalArgs);
}else {
totalArgs.unshift(fn);
return that.curry.apply(that,totalArgs)
}
};
}
const a = curry(fn, 3);
let res = a(2,3);
console.log(res);
函数柯里化:利用闭包保存的作用,把一些信息预先存储起来,[预处理],供其下级上下文中后期使用。
componse 函数
函数组合 把处理的函数像管道一样连接起来,然后数据通过这个管道得到最终的结果(链式操作)
const compose = function compose(...fns){
// x 为compose 第一次执行完 小函数的参数 初始值
return function operate(x){
//循环compose 实参 函数列表
// result 为上一次函数执行返回的结果
// item 为当前循环到的函数
// reduce 从左到右迭代 reduceRight从右到左
return fns.reduceRight((result,item)=>{
// 将当前函数执行的结果返回给result
return item(result)
},x)
}
}
const res = compose(fn3,fn1,fn2)(1);
//reduce实现
const fn = (x) => {
return x
}
const fn1 = (x) => {
return 1 + x
};
const fn2 = (x) => {
return 2 + x
};
const compose = (...options) => {
return options.reduce((per,item) => {
if (item && typeof item == "function") {
return item(per)
}
},0)
}
let res = compose(fn,fn1,fn2);
console.log(res);
js为什么单线程
- 作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题
- 假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
- 所以,为了避免复杂性,从一诞生,JavaScript就是单线程
任务队列
概念
- 单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着
- 所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)
- 同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务
- 异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行
异步任务
又分微任务和宏任务 来区分优先级,微任务优先级高于宏任务,先执行微任务 再执行宏任务,
promise的then属于微任务
setTimeout
- 属于 宏任务
- 它在"任务队列"的尾部添加一个事件,因此要等到同步任务和"任务队列"现有的事件都处理完,才会得到执行
总结Event loop
先考虑小孩(微任务) 然后大人(宏任务)
- 执行栈首先执行同步代码,遇到异步代码,丢入异步队列,直到同步队列执行完(js代码块,new Promise 构造函数前置内容)
console.log(111)
new Promise((res,rej)=>{ console.log(222) })
//这两项都是同步代码
- 此时开始执行异步队列,异步队列又有微任务和宏任务的区别
- 微任务先执行,然后执行宏任务,此时宏任务执行完成后就有计算结果反馈给同步任务
- 宏任务执行完成后,开始下一轮计算
- 要注意所有的执行过程都是在js执行栈中执行,只是每次被执行的对象变化了
setTimeout(() => {
console.log(0);
}, 0);
new Promise((resolve, reject) => {
console.log(1);
resolve();
})
.then(() => {
console.log(2);
new Promise((resolve, reject) => {
console.log(3);
resolve();
})
.then(() => {
console.log(4);
})
.then(() => {
console.log(5);
})
.then(() => {
console.log(6);
});
})
.then(() => {
console.log(7);
});
new Promise((resolve, reject) => {
console.log(8);
resolve();
}).then(() => {
console.log(9);
});
//1 8 2 3 9 4 7 5 6 0//所有的微任务执行完 才执行宏任务 就算是连续的
定时器任务编排
- 定时器有独立的模块进行计算,比如定时器设置的2毫秒,定时器模块会在2毫秒后丢入宏任务
- 所以并不是时间达到后就执行,而是时间达到后进入执行队列,
- 还是要先等待微任务执行完成,才会执行
- 如果存在多个定时器任务,那么定时器模块会进行排列,时间端会优先放入宏任务队列
setTimeout(() => { console.log('2000');}, 2000);
setTimeout(() => { console.log('1000');}, 1000);
Promise.resolve().then(res => {console.log("promise");})
console.log(2);
for (let index = 0; index < 3000; index++) {console.log('');}
//2 3000次空字符 promise 1000 2000
主进程和任务队列是共享内存的
let i = 0;
setTimeout(() => {console.log(++i);}, 1000);
setTimeout(() => {console.log(++i);}, 1000);
//两次settimeout 并不会同时执行 而是也有先后顺序 写在在前的先执行
//而且操作的i 是同一个i 且前面如果改变了 后面的拿到的是最新的值