原始/基本数据类型
JavaScript 的底层数据结构:
JavaScript基本类型数据都是直接按值存储在栈中的(Undefined、Null、不是new出来的布尔、数字和字符串),每种类型的数据占用的内存空间的大小是确定的,并由系统自动分配和自动释放。这样带来的好处就是,内存可以及时得到回收,相对于堆来说 ,更加容易管理内存空间。
JavaScript引用类型数据被存储于堆中 (如**对象、数组、函数**等,它们是通过拷贝和new出来的)。其实,说存储于堆中,也不太准确,因为,引用类型的数据的地址指针是存储于栈中的,当我们想要访问引用类型的值的时候,需要先从栈中获得对象的地址指针,然后,在通过地址指针找到堆中的所需要的数据。
Number
String
Boolean
Null
产生情况:主动,赋值为 null
**判断方法:**1.不能通过typeof判断,typeof null == object
(原因:JavaScript中的数据在底层是以二进制存储,比如null所有存储值都是 0,但是底层的判断机制,只要前三位为 0,就会判定为object,所以才会有typeof null === 'object'这个 bug。)
2.(!tmp && typeof(tmp)!=”undefined” && tmp!=0)
3.(tmp===null)
本质:数字 0,做数字运算时 null 都当成 0 处理
Undefined
产生情况:被动,变量没声明/声明了没赋值/函数没 return/对象里找不到的属性,用 undefined 替代
判断方法:typeof undefined == 'undefined'
Symbol
Symbol 本质上是一种唯一标识符,可用作对象的唯一属性名,这样其他人就不会改写或覆盖你设置的属性值。
声明方法及特点
let id = Symbol("id");
Symbol 数据类型的特点是唯一性,即使是用同一个变量生成的值也不相等。
Symbol 数据类型的特点是唯一性,即使是用同一个变量生成的值也不相等。
let id1 = Symbol("id");
let id2 = Symbol("id");
console.log(id1 == id2); //false
Symbol 数据类型的另一特点是隐藏性,for···in,object.keys() 不能访问
let id = Symbol("id");
let obj = {
[id]: "symbol",
};
for (let option in obj) {
console.log(obj[option]); //空
}
Object.getOwnPropertySymbols()
但是也有能够访问的方法:Object.getOwnPropertySymbols(), 此方法会返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。
let id = Symbol("id");
let obj = {
[id]: "symbol",
};
let array = Object.getOwnPropertySymbols(obj);
console.log(array); //[Symbol(id)]
console.log(obj[array[0]]); //'symbol'
Symbol.for()
虽然这样保证了 Symbol 的唯一性,但我们不排除希望能够多次使用同一个 symbol 值的情况。 为此,官方提供了全局注册并登记的方法:Symbol.for()
let name1 = Symbol.for("name"); //检测到未创建后新建
let name2 = Symbol.for("name"); //检测到已创建后返回
console.log(name1 === name2); // true
Symbol.keyFor()
通过这种方法就可以通过参数值获取到全局的 symbol 对象了,反之,也可以通过 symbol 对象获取到参数值呢,利用Symbol.keyFor()
let name1 = Symbol.for("name");
let name2 = Symbol.for("name");
console.log(Symbol.keyFor(name1)); // 'name'
console.log(Symbol.keyFor(name2)); // 'name'
最后,提醒大家一点,在创建 symbol 类型数据 时的参数只是作为标识使用,所以 Symbol() 也是可以的。
最后,提醒大家一点,在创建 symbol 类型数据 时的参数只是作为标识使用,所以 Symbol() 也是可以的。
Bigint
创建 BigInt
我们可以通过直接声明和为 BigInt 传入字符串,数值来创建 BigInt 类型的数值 直接声明时,只要在数字后面加上 n,就是一个 BigInt 类型了
var b1 = 123n;
var b2 = BigInt(123); // 123n
var b3 = BigInt("123"); // 123n
在传入字符串时,可以通过传入 0x,0o,0b 开头的字符串来对十六进制,八进制,二进制的数值创建 BigInt 类型的数值
var b16 = BigInt("0x10"); // 16n
var b8 = BigInt("0o10"); // 8n
var b2 = BigInt("0b10"); // 2n
如果传入其他字符串的话,就会报错
如果传入其他字符串的话,就会报错
BigInt("aaa");
// Uncaught SyntaxError: Cannot convert aaa to a BigInt
对 BigInt 类型的判断
要判断一个值是否为 BigInt,可以用以下几种方式
//typeof
typeof 1n; // "bigint"
typeof BigInt(1); // "bigint"
//constructor
BigInt("1").constructor === BigInt; // true
//Object.prototype.toString
Object.prototype.toString.call(1n); // "[object BigInt]"
关于其他的类型判断见 JavaScript 类型判断总结
关于其他的类型判断见 JavaScript 类型判断总结
Object.prototype.toString Object.prototype.toString.call(1n); // "[object BigInt]"
关于其他的类型判断见 JavaScript 类型判断总结
constructor BigInt('1').constructor === BigInt; // true
Object.prototype.toString Object.prototype.toString.call(1n); // "[object BigInt]"
关于其他的类型判断见 JavaScript 类型判断总结
超过安全数的计算
我们知道,JavaScript 原本的值的计算最大只到 2^53-1,超过这个数值的计算结果会出现错误,而使用 BigInt,我们可以超过这个数值正常计算,在 JavaScript 中,使用Number.MAX_SAFE_INTEGER来代表这个最大的安全数
我们看看不使用 BigInt 时的计算
var n1 = Number.MAX_SAFE_INTEGER; // 9007199254740991
n1 += 10; // 9007199254741000
这里 n1+10 后的答案显然是错误的,那么我们使用 BigInt 来计算
这里 n1+10 后的答案显然是错误的,那么我们使用 BigInt 来计算
var n1 = BigInt(Number.MAX_SAFE_INTEGER); // 9007199254740991n
n1 += 10n; // 9007199254741001n
可以看到,这里的数值正常了。
与 Number 类型的比较计算
要注意的是,BigInt 类型不能直接和 Number 类型混合计算,否则会报错
var n = 1n + 1;
// Uncaught TypeError: Cannot mix BigInt and other types, use explicit conversions
在比较的时候,BigInt 和 Number 类型的值是一样的
1n == 1; // true
1n < 2; // true
2n > 1; // true
转为布尔值的值
BigInt 变为布尔值是为 true 还是为 false 和 Number 一样:除了0n之外都为true。
Object
继承
es5 的继承
1.原型链继承
重点:让新实例的原型等于父类的实例
实例可继承的属性:实例的构造函数的属性,父类构造函数属性,父类原型的属性/方法。(新实例不会继承父类实例的属性!)
缺点:1. 创建实例时不能向父类构造函数传递参数,2. 引用类型属性被所用实例共享
function Parent() {
this.name = 'kevin';
}
Parent.prototype.getName = function () {
console.log(this.name);
}
function Child() { }
//原型链继承
Child.prototype = new Parent();
var child = new Child();
child.getName() //kevin
2.构造函数继承
重点:用.call()或.apply()将父类构造函数引入子类函数中进行普通执行
优点:可以传递参数,避免了引用类型共享
缺点:1. 只继承了父类构造函数的属性,没有继承父类原型的属性 2. 方法都在构造函数中定义,每次创建实例都会创建一遍方法。
function Parent() {
this.name = ['kevin'];
}
function Child() {
//构造函数继承
Parent.call(this);
}
var child1 = new Child();
var child2 = new Child();
child1.name.push("cc");
console.log(child1.name); //["kevin", "cc"]
console.log(child2.name); //["kevin"]
3.组合继承
优点:融合了原型继承和构造函数继承,可传参&复用,是 JavaScript 中常用的设计模式。
缺点:1.调用两次父构造函数 2.父类里会有同名属性name(这才是寄生组合继承要优化的关键点)
function Parent() {
this.name = ['kevin'];
}
Parent.prototype.getName = function () {
console.log(this.name, this.age);
}
function Child(age) {
//2.构造函数继承,第二次调用Parent(),覆盖原型中的同名属性
Parent.call(this);
this.age = age;
}
//1.原型链继承,第一次调用Parent()
Child.prototype = new Parent();
//一个自然的对象里这两个东西本来就应该相等:
Child.prototype.constructor=Child;
var child1 = new Child(19);
var child2 = new Child(20);
child1.name.push("cc");
child1.getName(); //["kevin", "cc"] 19
child2.getName(); //["kevin"] 20
4.原型式继承
Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的_ _ proto _ _(此方法es5新增)。下面的举例createObj是Object.create()的手动实现。
重点:用一个函数包装一个对象,然后返回这个函数的调用,这个函数就变成了个可以随意增添属性的实例或对象。object.create()就是这个原理。
缺点:创建实例时不能传递参数,所有属性都被实例共享?待补充
//封装一个函数容器,输入的实例对象obj = F.prototype = 返回值.__proto__ ,F是子类的构造函数,返回F即子类的一个实例
function createObj(obj) {
function F() { }
F.prototype = obj;
return new F();
}
var parent = new Parent();
//调用手写的方法
var child = creatObj(parent);
//利用es5 object.create实现
var child = Object.create(parent);
console.log(child.age);//继承父类属性
5.寄生式继承
重点:就是给原型式继承外面套了个壳子。把原型式继承再次封装,然后在对象上增加新的方法,再把新对象返回 优点:没有创建自定义类型,因为只是套了个壳子返回对象,这个函数顺理成章就成了创建的新对象。 缺点:无法做到函数复用。
function createObj(o) {
//核心还是这个Object.create()
var clone = Object.create(o);
clone.say = function() { console.log(this) };
return clone
}
console.log(createObj({})); //{say: ƒ}
6.寄生组合式继承
在组合继承中,无论在什么情况下,都会调用两次构造函数:一次是在创建子类型原型时,另一次是在子类型构造函数内部,很多文章说寄生组合式继承就是为了解决这一问题才出现的 -- 通过借用构造函数来继承属性 & 通过原型链来继承方法。但其实,我认为寄生组合继承想要解决的是一个更重要的问题:去除父类里的同名属性。
//声明父类
function Parent(name) {
this.name = name;
}
Parent.prototype.getName = function () {
console.log(this.name, this.age);
}
function Child(name, age) {
//2.构造函数继承
Parent.call(this, name);
this.age = age;
}
var F = function () { }
F.prototype = Parent.prototype;
//1.原型链继承
Child.prototype = new F();
var child1 = new Child("cc", 20)
console.log(child1) //Child {name: "cc", age: 20}
可以看出父类的Parent里已经没有了同名的name属性
es6的继承
class 本质还是函数,只是一个语法糖而已。
1.class 通过 extends 关键字实现继承。
class Parent {
constructor(x, y) {
this.x = x;
this.y = y
}
}
class Child extends Parent {
constructor(x, y, name) {
super(x, y);//调用父类的constructor(x,y)
this.name = name;
}
}
var child1 = new Child("x", "y", "ccg");
console.log(child1);//Child {x: "x", y: "y", name: "ccg"}
2.super 关键字
如果在子类构造函数中使用 this,就要采用 super 关键字,它表示调用父类的构造函数。这是必须的。
因为 ES6 的继承机制和 ES5 的不同,ES5 是先创造子类的实例对象 this,再将父类的方法添加到 this 上面(Parent.apply(this)),而 ES6 是先创建父类的实例对象 this,所以必须调用 super 关键字。
虽然 super 代表 A 的构造函数,但是返回的是子类 B 的实例,即 super 中的 this 指向的 B。
class A {
constructor() {
console.log(new.target.name);
}
}
class B extends A {
constructor() {
super();
}
}
new A();
new B();
作为函数,super 只能在子类的构造函数中。但是如果作为对象时,在普通方法中指向父类的原型对象;
在静态方法中指向父类。(!由于 super 指向父类的原型对象,
所以定义在父类实例的方法无法通过 super 调用)。
class Parent {
static myMethod(msg) {
console.log('static', msg);
}
myMethod(msg) {
console.log('instance', msg);
}
}
class Child extends Parent {
static myMethod(msg) {
super.myMethod(msg);
}
myMethod(msg) {
super.myMethod(msg);
}
}
Child.myMethod(1); //static 1
var child = new Child();
child.myMethod(2); //instance 2
使用 super,要显式指定使用方法。
class Parent {
constructor() {
super();
console.log(super);//报错
}
}
Array
判断一个变量是否为数组
1.通过instanceof判断
instanceof运算符用于检验构造函数的prototype属性是否出现在对象的原型链中的任何位置,返回一个布尔值。
let a = [];
a instanceof Array; //true
let b = {};
b instanceof Array; //false
在上方代码中,instanceof运算符检测Array.prototype属性是否存在于变量a的原型链上,显然a是一个数组,拥有Array.prototype属性,所以为true。
存在问题:
需要注意的是,prototype属性是可以修改的,所以并不是最初判断为true就一定永远为真。
其次,当我们的脚本拥有多个全局环境,例如html中拥有多个iframe对象,instanceof的验证结果可能不会符合预期,例如:
//为body创建并添加一个iframe对象
var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
//取得iframe对象的构造数组方法
xArray = window.frames[0].Array;
//通过构造函数获取一个实例
var arr = new xArray(1,2,3);
arr instanceof Array;//false
导致这种问题是因为iframe会产生新的全局环境,它也会拥有自己的Array.prototype属性,让不同环境下的属性相同很明显是不安全的做法,所以Array.prototype !== window.frames[0].Array.prototype,想要arr instanceof Array为true,你得保证arr是由原始Array构造函数创建时才可行。
2.通过constructor判断
我们知道,实例的构造函数属性constructor指向构造函数,那么通过constructor属性也可以判断是否为一个数组。
let a = [1,3,4];
a.constructor === Array;//true
同样,这种判断也会存在多个全局环境的问题,导致的问题与instanceof相同。
//为body创建并添加一个iframe标签
var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
//取得iframe对象的构造数组方法
xArray = window.frames[window.frames.length-1].Array;
//通过构造函数获取一个实例
var arr = new xArray(1,2,3);
arr.constructor === Array;//false
3.通过Object.prototype.toString.call()判断
Object.prototype.toString().call()可以获取到对象的不同类型,例如
let a = [1,2,3]
Object.prototype.toString.call(a) === '[object Array]';//true
它强大的地方在于不仅仅可以检验是否为数组,比如是否是一个函数,是否是数字等等
//检验是否是函数
let a = function () {};
Object.prototype.toString.call(a) === '[object Function]';//true
//检验是否是数字
let b = 1;
Object.prototype.toString.call(a) === '[object Number]';//true
甚至对于多全局环境时, Object.prototype.toString().call()也能符合预期处理判断。
//为body创建并添加一个iframe标签
var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
//取得iframe对象的构造数组方法
xArray = window.frames[window.frames.length-1].Array;
//通过构造函数获取一个实例
var arr = new xArray(1,2,3);
console.log(Object.prototype.toString.call(arr) === '[object Array]');//true
4.通过Array.isArray()判断
Array.isArray() 用于确定传递的值是否是一个数组,返回一个布尔值。
let a = [1,2,3]
Array.isArray(a);//true
简单好用,而且对于多全局环境,Array.isArray() 同样能准确判断,但有个问题,Array.isArray() 是在ES5中提出,也就是说在ES5之前可能会存在不支持此方法的情况。怎么解决呢?
数组的方法
不加粗的为实例调用的方法,加粗的为原型调用的方法
| 方法 | 常规用法 | 功能 | 是否改变原数组 | 返回值 |
|---|---|---|---|---|
| push | arr.push(8) | 末端插入 | 是 | 修改后的数组长度 |
| pop | arr.pop() | 末端移除 | 是 | 移除的那个值 |
| shift | arr.shift() | 首端移除 | 是 | 移除的那个值 |
| unshift | arr.unshift(8) | 首端插入 | 是 | 修改后的数组长度 |
| concat | arr.concat(9,[11,13]) | 将参数或数组拼接到原数组尾部 | 否 | 拼接后数组 |
| slice | arr.slice(a,b) | 截取数组,顾头不顾尾,b缺省表示取到最末尾,b负数表示从最后去掉b个数 | 否 | 截取后数组 |
| splice | arr.splice(a,b,c) | 删除、插入和替换,a-删除起始位置,b-删除项数,c-要增加的任意数量的项 | 是 | 被删除部分的数组 |
| indexOf | arr.indexOf(a,b) | 查找第一个a元素的下标,b表示查找起点索引,可缺省 | 否 | 下标值 |
| lastIndexOf | arr.lastIndexOf(a,b) | 查找最后一个a元素的下标,b表示查找起点索引,可缺省 | 否 | 下标值 |
| forEach | arr.forEach((value,index,wholearray)=>{...}) | 对数组进行遍历执行 | value为原始值时不会改变;value为引用值时会改变 | 无 |
| sort | arr.sort((a,b)=>a-b) 升序 | 对数组按指定规则进行排序,若函数缺省则按字母升序 | 是 | 排序后数组 |
| filter | arr.filter(item=>item>10) | 筛选数组中符合要求的或返回true的 | 否 | 过滤后数组 |
| map | arr.map(item=>item+1) | 逐个对应生成映射数组 | 否 | 映射数组 |
| ruduce | arr.reduce(function(total,currentValue),initialValue) | 将数组循环计算成一个最终值,initialValue可缺省为0 | 否 | 计算最终值 |
| every | arr.every(function(value)) | 元素全部返回true则返回true | 否 | true/false |
| some | arr.some(function(value)) | 元素有一个返回false则返回false | 否 | true/false |
| toString | arr.toString() | 转为字符串 | 否 | 字符串,例如 "1,2,3" |
| join | arr.join('-') | 转为字符串,参数缺省时为',' | 否 | 字符串,例如 "1-2-3" |
| reverse | arr.reverse() | 反转数组 | 是 | 反转后数组 |
| 数组原型方法 | ||||
| from | Array.from(set/伪数组/arguments...) | 从一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例 | 否 | 数组 |
| of | Array.of(1,2,3...) | 返回由参数构成的数组 | 否 | 数组[1,2,3,...] |
| isArray | Array.isArray(arr) | 判断参数是不是数组 | 否 | true/false |
*1*|*0***join()**
join,就是把数组转换成字符串,然后给他规定个连接字符,默认的是逗号( ,)
书写格式:join(" "),括号里面写字符串 ("要加引号"),
var arr = [1,2,3];
console.log(arr.join()); // 1,2,3
console.log(arr.join("-")); // 1-2-3
console.log(arr); // [1, 2, 3](原数组不变)
*2*|*0***push()和 pop()**
push(): 把里面的内容添加到数组末尾,并返回修改后的长度。
pop():移除数组最后一项,返回移除的那个值,减少数组的 length。
书写格式:arr.push(" "),括号里面写内容 ("字符串要加引号"),
书写格式:arr.pop( )
var arr = ["Lily","lucy","Tom"];
var count = arr.push("Jack","Sean");
console.log(count); // 5
console.log(arr); // ["Lily", "lucy", "Tom", "Jack", "Sean"]
var item = arr.pop();
console.log(item); // Sean
console.log(arr); // ["Lily", "lucy", "Tom", "Jack"]
*3*|*0***shift() 和 unshift() (和上面的 push,pop 相反,针对第一项内容)**
**shift():删除原数组第一项,并返回删除元素的值;如果数组为空则返回 undefined 。
**
unshift:将参数添加到原数组开头,并返回数组的长度 。
书写格式:arr.shift(" "),括号里面写内容 ("字符串要加引号"),
var arr = ["Lily","lucy","Tom"];
var count = arr.unshift("Jack","Sean");
console.log(count); // 5
console.log(arr); //["Jack", "Sean", "Lily", "lucy", "Tom"]
var item = arr.shift();
console.log(item); // Jack
console.log(arr); // ["Sean", "Lily", "lucy", "Tom"]
*4*|*0***sort()**
sort():将数组里的项从小到大排序
书写格式:arr.sort( )
var arr1 = ["a", "d", "c", "b"];
console.log(arr1.sort()); // ["a", "b", "c", "d"]
sort()方法比较的是字符串,没有按照数值的大小对数字进行排序,要实现这一点,就必须使用一个排序函数
function sortNumber(a,b)
{
return a - b
}
arr = [13, 24, 51, 3];
console.log(arr.sort()); // [13, 24, 3, 51]
console.log(arr.sort(sortNumber)); // [3, 13, 24, 51](数组被改变)
*5*|*0***reverse()**
reverse():反转数组项的顺序。
书写格式:arr.reverse( )
var arr = [13, 24, 51, 3];
console.log(arr.reverse()); //[3, 51, 24, 13]
console.log(arr); //[3, 51, 24, 13](原数组改变)
*6*|*0***concat()**
concat() :将参数添加到原数组中。这个方法会先创建当前数组一个副本,然后将接收到的参数添加到这个副本的末尾,最后返回新构建的数组。在没有给 concat()方法传递参数的情况下,它只是复制当前数组并返回副本。
书写格式:arr.concat(),括号里面写内容 ("字符串要加引号"),
var arr = [1,3,5,7];
var arrCopy = arr.concat(9,[11,13]);
console.log(arrCopy); //[1, 3, 5, 7, 9, 11, 13]
console.log(arr); // [1, 3, 5, 7](原数组未被修改)
*7*|*0***slice()**
slice():返回从原数组中指定开始下标到结束下标之间的项组成的新数组。slice()方法可以接受一或两个参数,即要返回项的起始和结束位置。在只有一个参数的情况下, slice()方法返回从该参数指定位置开始到当前数组末尾的所有项。如果有两个参数,该方法返回起始和结束位置之间的项——但不包括结束位置的项。
书写格式:arr.slice( 1 , 3 )
var arr = [1,3,5,7,9,11];
var arrCopy = arr.slice(1);
var arrCopy2 = arr.slice(1,4);
var arrCopy3 = arr.slice(1,-2);
var arrCopy4 = arr.slice(-4,-1);
console.log(arr); //[1, 3, 5, 7, 9, 11](原数组没变)
console.log(arrCopy); //[3, 5, 7, 9, 11]
console.log(arrCopy2); //[3, 5, 7]
console.log(arrCopy3); //[3, 5, 7]
console.log(arrCopy4); //[5, 7, 9]
arrCopy 只设置了一个参数,也就是起始下标为 1,所以返回的数组为下标 1(包括下标 1)开始到数组最后。
arrCopy2 设置了两个参数,返回起始下标(包括 1)开始到终止下标(不包括 4)的子数组。
arrCopy3 设置了两个参数,终止下标为负数,当出现负数时,将负数加上数组长度的值(6)来替换该位置的数,因此就是从 1 开始到 4(不包括)的子数组。
arrCopy4 中两个参数都是负数,所以都加上数组长度 6 转换成正数,因此相当于 slice(2,5)。
*8*|*0***splice()**
splice():删除、插入和替换。
删除:指定 2 个参数:要删除的第一项的位置和要删除的项数。
书写格式:arr.splice( 1 , 3 )
插入:可以向指定位置插入任意数量的项,只需提供 3 个参数:起始位置、 0(要删除的项数)和要插入的项。
书写格式:arr.splice( 2,0,4,6 ) 替换:可以向指定位置插入任意数量的项,且同时删除任意数量的项,只需指定 3 个参数:起始位置、要删除的项数和要插入的任意数量的项。插入的项数不必与删除的项数相等。
书写格式:arr.splice( 2,0,4,6 )
var arr = [1,3,5,7,9,11];
var arrRemoved = arr.splice(0,2);
console.log(arr); //[5, 7, 9, 11]
console.log(arrRemoved); //[1, 3]
var arrRemoved2 = arr.splice(2,0,4,6);
console.log(arr); // [5, 7, 4, 6, 9, 11]
console.log(arrRemoved2); // []
var arrRemoved3 = arr.splice(1,1,2,4);
console.log(arr); // [5, 2, 4, 4, 6, 9, 11]
console.log(arrRemoved3); //[7]
*9*|*0***indexOf()和 lastIndexOf()**
indexOf():接收两个参数:要查找的项和(可选的)表示查找起点位置的索引。缺省表示从数组的开头(位置 0)开始向后查找。
书写格式:arr.indexof( 5 )
**lastIndexOf:接收两个参数:要查找的项和(可选的)表示查找起点位置的索引。缺省表示 从数组的末尾开始向前查找。**
书写格式:arr.lastIndexOf( 5,4 )
var arr = [1,3,5,7,7,5,3,1];
console.log(arr.indexOf(5)); //2
console.log(arr.lastIndexOf(5)); //5
console.log(arr.indexOf(5,2)); //2
console.log(arr.lastIndexOf(5,4)); //2
console.log(arr.indexOf("5")); //-1
*10*|*0***forEach()**
forEach():对数组进行遍历循环,对数组中的每一项运行给定函数。这个方法没有返回值。参数都是 function 类型,默认有传参,参数分别为:遍历的数组内容;第对应的数组索引,数组本身。
书写格式:arr.forEach()
var arr = [1, 2, 3, 4, 5];
arr.forEach(function(x, index, a){
console.log(x + '|' + index + '|' + (a === arr));
});
// 输出为:
// 1|0|true
// 2|1|true
// 3|2|true
// 4|3|true
// 5|4|true
*11*|*0***map()**
map():指“映射”,对数组中的每一项运行给定函数,返回每次函数调用的结果组成的数组。
书写格式:arr.map()
var arr = [1, 2, 3, 4, 5];
var arr2 = arr.map(function(item){
return item*item;
});
console.log(arr2); //[1, 4, 9, 16, 25]
*12*|*0***filter()**
filter():“过滤”功能,数组中的每一项运行给定函数,返回满足过滤条件组成的数组。
书写格式:arr.filter()
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
var arr2 = arr.filter(function(x, index) {
return index % 3 === 0 || x >= 8;
});
console.log(arr2); //[1, 4, 7, 8, 9, 10]
*13*|*0***every()**
every():判断数组中每一项都是否满足条件,只有所有项都满足条件,才会返回 true。
书写格式:arr.every()
var arr = [1, 2, 3, 4, 5];
var arr2 = arr.every(function(x) {
return x < 10;
});
console.log(arr2); //true
var arr3 = arr.every(function(x) {
return x < 3;
});
console.log(arr3); // false
reduce
reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。
reduce() 可以作为一个高阶函数,用于函数的 compose。
注意: reduce() 对于空数组是不会执行回调函数的。
*14*|*0***some()**
some():判断数组中是否存在满足条件的项,只要有一项满足条件,就会返回 true。
书写格式:arr.some()
var arr = [1, 2, 3, 4, 5];
var arr2 = arr.some(function(x) {
return x < 3;
});
console.log(arr2); //true
var arr3 = arr.some(function(x) {
return x < 1;
});
console.log(arr3); // false
Function
判断一个变量是否为函数
Object.prototype.toString.call(obj) === "[object Function]";
JavaScript 类型判断总结
在对类型的识别进行总结前,首先明确一下,在 JavaScript 中,有八种内置类型,null,undefined,boolean,number,String,Object,Symbol(ES6 中新增),BigInt(ES10 新增),接下来对几种常见的类型识别进行总结。
typeof
typeof 运算符是我们常用的用于检测类型的一种方式,该运算符会返回一个字符串来表示其识别到的类型,我们使用 typeof 对七种内置类型的类型进行检测。
typeof 1; // "number"
typeof "str"; // "string"
typeof true; // "boolean"
typeof {}; // "object"
typeof Symbol(); // "symbol"
typeof 1n; // "bigint"
typeof BigInt("1"); // "bigint"
typeof undefined; // "undefined"
typeof null; // "object"
可以发现,大部分的类型检测都符合我们的,但是 null 却莫名奇妙地返回了一个"object",按我们的理解它应该返回一个"null"才对。这其实是这个检测方式的一种 bug,至于在后面会不会得到修改,现在还不得而知,至少我们知道在代码编写中要避免使用 typeof 对可能变为 null 的变量进行类型检测。如果非要使用 typeof 检测 null 类型,就得同时判断其转为 boolean 类型是否为 false,我将其封装在一个方法中如下代码
function typeCheck(n) {
if (!n && typeof n === "object") return "null";
else return typeof n;
}
typeCheck(null);
// "null"
关于数组:
typeof []; // "object"
关于函数:
function fn() {}
typeof fn; // "function"
此外,class 在使用 typeof 判断时也被判断为"function"
typeof class C {};
// "function"
对于使用 new 运算符构建的对象,typeof 统一返回"object",即我们无法更细致地区分这些对象
而我们常见的原生函数有String(),Number(),Boolean(),Array(),Object(),Function(),RegExp(),Date(),Error()。这些在使用 typeof 检测时都返回"object"。使用 typeof 时我们无法很好地区分这些内容。
typeof new String(); // "object"
typeof new Number(); // "object"
typeof new Array(); // "object"
typeof new Object(); // "object"
typeof new Function(); // "function"
typeof new RegExp(); // "object"
typeof new Date(); // "object"
typeof new Error(); // "object"
总的来说,typeof 只能返回以下八个值其中一个:number,string,boolean,object,undefined,symbol,function,还有最新的 bigint
判断原理
我们在上面看到了 typeof 判断 null 的时候为"object",那么为什么会为"object"呢,这要从 typeof 的判断原理说起
要说 typeof 的判断原理,我们要先从 js 存储变量时的二进制表示位说起,js 会把机器码的低位的 1-3 位拿来存储其类型信息,相应类型的表示如下
000 :对象 010 :浮点数 100 :字符串 110 :布尔值 001 :整数
那么,null 在机器码中又是什么表示呢,答案是所有位数都为 0,所以前三位自然也都是 0,因此就会被判断为"object"
constructor
如果我们要判断出对象是由哪些函数构建的,我们可以通过使用对象的 constructor 属性来判断
new String().constructor === String; // true
new Number().constructor === Number; // true (这个constructor打印出来是:[Function:number])
new Array().constructor === Array; // true
new Boolean().constructor === Boolean; // true
new Object().constructor === Object; // true
new Function().constructor === Function; // true
new RegExp().constructor === RegExp; // true
new Date().constructor === Date; // true
new Error().constructor === Error; // true
正如我们所期待的,constructor 出色地完成了我们的任务,成功辨别了对象由哪个函数构造的,那么 constructor 对于基本的内置类型又是否能完美判断呢
// 1.constructor===Number(错误写法)
// 数字字面量没有constructor
var num = 1;
num.constructor === Number;
// true
BigInt("1").constructor === BigInt;
// true
"str".constructor === String;
// true
true.constructor === Boolean;
// true
Symbol().constructor === Symbol;
// true
// {}.constructor===Object(错误写法)
// 对于一个直接构建的对象来说没有constructor属性
// 同样地null和undefined也没有constructor属性
可以看到,对于基本的内置属性,constructor 只能判断其中的五种。 除此之外,constructor 还有一个需要注意的点,就是在使用继承的时候,它可能不会出现我们期待的结果。
function People() {}
function Student() {}
Student.prototype = new People();
new Student().constructor === Student;
// false
new Student().constructor === People;
// true
可以看到 Student 方法构建的对象的 constructor 是 People,这是因为在调用 new Student().constructor 时,我们要找到 constructor 属性,明显的是这个对象没有 constructor 属性,所以我们沿着原型链寻找该属性,而Student.prototype 是 new People(),而 new People()中也没有 constructor 属性,沿着原型链我们在 new People().prototype 找到了该属性其值为 People,所以最后出现了我们期待以外的结果。
Object.prototype.toString
首先我们还是来看看 Object.prototype.toString 对八种内置类型的检测,Object.prototype.toString 返回的是"[Object (class)]",class 的部分就是我们要的内容。
Object.prototype.toString.call(1);
// "[object Number]"
Object.prototype.toString.call("str");
// "[object String]"
Object.prototype.toString.call({});
// "[object Object]"
Object.prototype.toString.call(null);
// "[object Null]"
Object.prototype.toString.call(undefined);
// "[object Undefined]"
Object.prototype.toString.call(Symbol());
// "[object Symbol]"
Object.prototype.toString.call(true);
// "[object Boolean]"
Object.prototype.toString.call(1n);
// "[object BigInt]"
可以看到,Object.prototype.toString 完美地回应了我们的期待,那来看看对于方法会返回什么
function fn() {}
Object.prototype.toString.call(fn);
// "[object Function]"
可以看到,Object.prototype.toString 和 typeof 一样,即使 function 是 Object 的子类,还是把它判断成了”Function“。
接着来看看对于原生函数的构造时会有什么检测结果
Object.prototype.toString.call(new String());
// "[object String]"
Object.prototype.toString.call(new Number());
// "[object Number]"
Object.prototype.toString.call(new Boolean());
// "[object Boolean]"
Object.prototype.toString.call(new Array());
// "[object Array]"
Object.prototype.toString.call(new Object());
// "[object Object]"
Object.prototype.toString.call(new Function());
// "[object Function]"
Object.prototype.toString.call(new RegExp());
// "[object RegExp]"
Object.prototype.toString.call(new Date());
// "[object Date]"
Object.prototype.toString.call(new Error());
// "[object Error]"
可以看到如果我们想分辨出原生函数构造对象是由什么原生函数构造的,Object.prototype.toString 是一个好的选择,而对于自定义的对象,这个方法就无法正确判断了。
function Person() {}
var person = new Person();
Object.prototype.toString.call(person);
// "[object Object]"
instanceof
instanceof 在对继承对象的判断上正确
class People {}
class Student extends People {}
new Student() instanceof Student; // true
new Student() instanceof People; // true
但是在对原始类型的判断上不尽人意
1 instanceof Number;
// false
true instanceof Boolean;
// false
"str" instanceof String;
// false
然而,如果我们使用 new 来创建相应的对象,那 instanceof 可以正确判别
new Number(1) instanceof Number;
// true
new Boolean(true) instanceof Boolean;
// true
new String("str") instanceof String;
// true
对于原生类型来说,instanceof 看似能正确判别
[1,2,3] instanceof Array
// true
/abc/ instanceof RegExp
// true
({}) instanceof Object
// true
function fn(){}
fn instanceof Function
// true
为什么说是看似呢,我们看下面的例子
[1,2,3] instanceof Object // true
/abc/ instanceof Object // true
function fn(){}
fn instanceof Object // true
可以看到,虽然我们可以通过 instanceof 来判断数组,正则表达式和函数,但是 instanceof 同样将他们当作对象,所以使用 instanceof 去判断这些类型时要做到在其判断为对象时也不会有问题,相关原理会在下文介绍。
class 和 function 一样,也会被判断为 Function 和 Object
class C {}
C instanceof Object; // true
C instanceof Function; // true
new Function() instanceof Object; //true
new Object() instanceof Function; //false
而 BigInt 不能用 instanceof 判断
1n instanceof BigInt; // false
BigInt(1) instanceof BigInt; // false
判断原理
instanceof 的判断原理实际上就是原型链,使用代码简单表示就是
function _instanceof(leftVal, rightVal) {
leftVal = leftVal.__proto__;
const rightPro = rightVal.prototype;
//左值一直向上追溯__proto__,直到它等于右值的prototyp
while (true) {
if (leftVal === null) {
return false;
}
if (leftVal === rightPro) {
return true;
}
leftVal = leftVal.__proto__;
}
}
按上面的在这个函数,我们也可以解释了上面的 Student 继承 People 后为什么使用 instanceof 可以判断为 People 的类型,因为 People 确实是在其原型链上的。
不同用法的总结
typeof
1.对其他内置类型的检测正确,在对null 进行类型检测时会返回"object"
2.对方法进行检测时,会返回”function"
3.typeof对于对象统一返回"object",即使使用了不同的原生函数,typeof 无法区分这些原生函数构造的对象
constructor
1.对于内置类型只能判断 Number(需要将值赋给变量才能判断),String,Boolean,Object,Symbol,BigInt,其他两种(null,undefined)无法判断
2.对于对象能返回构造对象的构造函数名
3.当使用继承时会返回原型链末端的构造函数名
Object.prototype.toString
1.对于所有内置对象都可以判断
2.对于对象能返回构造对象的构造函数名,但自定义对象只能返回 object
instanceof
1.对于对象能返回构造对象的构造函数名
2.能正确判断继承的对象
3.对于原始类型字面量无法正确判断(Number,Boolean,String),对 new 构造的对象可以正确判断,可以正确判断原生类型(RegExp,Array,Function,Object)
对 ES6 中的类型的判断
上面的内容基本都是对 ES6 之前的类型的判断,接下来看看对于 ES6 中的新类型上面的四种方法能否正确判断
Map 和 Set // typeof typeof new Set() // "object" typeof new Map() // "object"
// Object.prototype.toString Object.prototype.toString.call(new Map()) // "[object Map]" Object.prototype.toString.call(new Set()) // "[object Set]"
// constructor new Map().constructor===Map // true new Set().constructor===Set // true
// instanceof new Map() instanceof Map // true new Set() instanceof Set // true 可以看到,对于类数组 Map 和 Set,只有 typeof 没有返回我们所期待的类型
Promise var p=new Promise((resolve,reject)=>{}) typeof p // "object" Object.prototype.toString.call(p) // "[object Promise]" p.constructor===Promise // true p instanceof Promise // true 可以看到同样是 typeof 没有返回我们期待的类型
Proxy var p=new Proxy({},{}) typeof p // "object" Object.prototype.toString.call(p) // "[object Object]" p.constructor===Proxy // false p instanceof Proxy // Uncaught TypeError: Function has non-object prototype 'undefined' in instanceof check 可以看到没有一个返回我们期待的结果,instanceof 甚至报错了,如果我们要明确知道某个变量实际上是代理的话,仅靠上面的方法是行不通的
以上就是我对 JavaScript 中类型检测的总结,若有什么补充之处和错漏之处请各位指出
ES6 新特性
var 与 let/const
函数作用域与块级作用域
变量提升与暂时性死区
重复声明
set/map
箭头函数
promise
symbol 基本数据类型
原型链
前言
这里说明一点,proto属性的两边是各由两个下划线构成(这里为了方便大家看清,在两下划线之间加入了一个空格:_ proto _,读作“dunder proto”,“double underscore proto”的缩写),实际上,该属性在 ES 标准定义中的名字应该是[[Prototype]],具体实现是由浏览器代理自己实现,谷歌浏览器的实现就是将[[Prototype]]命名为proto,大家清楚这个标准定义与具体实现的区别即可,可以通过该方式检测引擎是否支持这个属性:Object.getPrototypeOf({proto: null}) === null。本文基于谷歌浏览器(版本 72.0.3626.121)的实验结果所得。 现在正式开始! 让我们从如下一个简单的例子展开讨论,并配以相关的图帮助理解:
function Foo() {...};
let f1 = new Foo();
以上代码表示创建一个构造函数 Foo(),并用 new 关键字实例化该构造函数得到一个实例化对象 f1。这里稍微补充一下 new 操作符将函数作为构造器进行调用时的过程:函数被调用,然后新创建一个对象,并且成了函数的上下文(也就是此时函数内部的 this 是指向该新创建的对象,这意味着我们可以在构造器函数内部通过 this 参数初始化值),最后返回该新对象的引用,详细请看:详解 JavaScript 中的 new 操作符。虽然是简简单单的两行代码,然而它们背后的关系却是错综复杂的,如下图所示:
图的说明:右下角为图例,红色箭头表示proto属性指向、绿色箭头表示 prototype 属性的指向、棕色实线箭头表示本身具有的 constructor 属性的指向,棕色虚线箭头表示继承而来的 constructor 属性的指向;蓝色方块表示对象,浅绿色方块表示函数(这里为了更好看清,Foo()仅代表是函数,并不是指执行函数 Foo 后得到的结果,图中的其他函数同理)。图的中间部分即为它们之间的联系,图的最左边即为例子代码。
_ _ proto _ _ 属性
首先,我们需要牢记两点:①proto和 constructor 属性是对象所独有的;② prototype 属性是函数所独有的。但是由于 JS 中函数也是一种对象,所以函数也拥有proto和 constructor 属性,这点是致使我们产生困惑的很大原因之一。上图有点复杂,我们把它按照属性分别拆开,然后进行分析:
第一,这里我们仅留下 proto 属性,它是对象所独有的,可以看到proto属性都是由一个对象指向一个对象,即指向它们的原型对象(也可以理解为父对象),那么这个属性的作用是什么呢?它的作用就是当访问一个对象的属性时,如果该对象内部不存在这个属性,那么就会去它的proto属性所指向的那个对象(可以理解为父对象)里找,如果父对象也不存在这个属性,则继续往父对象的proto属性所指向的那个对象(可以理解为爷爷对象)里找,如果还没找到,则继续往上找…直到原型链顶端 null(可以理解为原始人。。。),再往上找就相当于在 null 上取值,会报错(可以理解为,再往上就已经不是“人”的范畴了,找不到了,到此结束,null 为原型链的终点),由以上这种通过proto属性来连接对象直到 null 的一条链即为我们所谓的原型链。 其实我们平时调用的字符串方法、数组方法、对象方法、函数方法等都是靠proto继承而来的。
prototype 属性
第二,接下来我们看 prototype 属性:
prototype 属性,别忘了一点,就是我们前面提到要牢记的两点中的第二点,它是函数所独有的,它是从一个函数指向一个对象。它的含义是函数的原型对象,也就是这个函数(其实所有函数都可以作为构造函数)所创建的实例的原型对象,由此可知:f1.proto === Foo.prototype,它们两个完全一样。那 prototype 属性的作用又是什么呢?它的作用就是包含可以由特定类型的所有实例共享的属性和方法,也就是让该函数所实例化的对象们都可以找到公用的属性和方法。任何函数在创建的时候,其实会默认同时创建该函数的 prototype 对象。
constructor 属性
最后,我们来看一下 constructor 属性:
constructor 属性也是对象才拥有的,它是从一个对象指向一个函数,含义就是指向该对象的构造函数,每个对象都有构造函数(本身拥有或继承而来,继承而来的要结合proto属性查看会更清楚点,如下图所示),从上图中可以看出 Function 这个对象比较特殊,它的构造函数就是它自己(因为 Function 可以看成是一个函数,也可以是一个对象),所有函数和对象最终都是由 Function 构造函数得来,所以 constructor 属性的终点就是 Function 这个函数。
感谢网友的指出,这里解释一下上段中“每个对象都有构造函数”这句话。这里的意思是每个对象都可以找到其对应的 constructor,因为创建对象的前提是需要有 constructor,而这个 constructor 可能是对象自己本身显式定义的或者通过proto在原型链中找到的。而单从 constructor 这个属性来讲,只有 prototype 对象才有。每个函数在创建的时候,JS 会同时创建一个该函数对应的 prototype 对象,而函数创建的对象.proto === 该函数.prototype,该函数.prototype.constructor===该函数本身,故通过函数创建的对象即使自己没有 constructor 属性,它也能通过proto找到对应的 constructor,所以任何对象最终都可以找到其构造函数(null 如果当成对象的话,将 null 除外)。如下:
总结
总结一下:
我们需要牢记两点:①proto和 constructor 属性是对象所独有的;② prototype 属性是函数所独有的,因为函数也是一种对象,所以函数也拥有proto和 constructor 属性。 proto属性的作用就是当访问一个对象的属性时,如果该对象内部不存在这个属性,那么就会去它的proto属性所指向的那个对象(父对象)里找,一直找,直到proto属性的终点 null,再往上找就相当于在 null 上取值,会报错。通过proto属性将对象连接起来的这条链路即我们所谓的原型链。 prototype 属性的作用就是让该函数所实例化的对象们都可以找到公用的属性和方法,即 f1.proto === Foo.prototype。 constructor 属性的含义就是指向该对象的构造函数,所有函数(此时看成对象了)最终的构造函数都指向 Function。 本文就此结束了,希望对那些对 JS 中的 prototype、proto与 constructor 属性有困惑的同学有所帮助。
参考:
小彩蛋:实现继承
function inherit(Child, Parent) {
// 继承原型上的属性
Child.prototype = Object.create(Parent.prototype);
// 修复 constructor
Child.prototype.constructor = Child;
// 存储超类
Child.super = Parent;
// 静态属性继承
if (Object.setPrototypeOf) {
// setPrototypeOf es6
Object.setPrototypeOf(Child, Parent);
} else if (Child.__proto__) {
// __proto__ es6 引入,但是部分浏览器早已支持
Child.__proto__ = Parent;
} else {
// 兼容 IE10 等陈旧浏览器
// 将 Parent 上的静态属性和方法拷贝一份到 Child 上,不会覆盖 Child 上的方法
for (var k in Parent) {
if (Parent.hasOwnProperty(k) && !(k in Child)) {
Child[k] = Parent[k];
}
}
}
}
箭头函数
箭头函数:
let fun = () => {
console.log('lalalala');
}
普通函数:
function fun() {
console.log('lalla');
}
箭头函数相当于匿名函数,并且简化了函数定义。箭头函数有两种格式,一种只包含一个表达式,连{ ... }和 return 都省略掉了。还有一种可以包含多条语句,这时候就不能省略{ ... }和 return。
箭头函数是匿名函数,不能作为构造函数,不能使用 new
let FunConstructor = () => {
console.log('lll');
}
let fc = new FunConstructor();
箭头函数不绑定 arguments,取而代之用 rest 参数...解决
function A(a){
console.log(arguments);
}
A(1,2,3,4,5,8); // [1, 2, 3, 4, 5, 8, callee: ƒ, Symbol(Symbol.iterator): ƒ]
let B = (b)=>{
console.log(arguments);
}
B(2,92,32,32); // Uncaught ReferenceError: arguments is not defined
let C = (...c) => {
console.log(c);
}
C(3,82,32,11323); // [3, 82, 32, 11323]
箭头函数不绑定 this,会捕获其所在的上下文的 this 值,作为自己的 this 值
箭头函数没有自己的 this,它会捕获自己在 初始化时 (划重点!!!)所处的外层执行环境的 this,并继承这个 this 值.
所以, 箭头函数中 this 的指向在它被定义的时候就已经确定了, 之后永远不会改变.
var obj = {
a: 10,
b: () => {
console.log(this.a); // undefined
console.log(this); // Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, frames: Window, …}
},
c: function() {
console.log(this.a); // 10
console.log(this); // {a: 10, b: ƒ, c: ƒ}
}
}
obj.b();
obj.c();
var obj = {
a: 10,
b: function(){
console.log(this.a); //10
},
c: function() {
return ()=>{
console.log(this.a); //10
}
}
}
obj.b();
obj.c()();
也因此 vue 中的 data 返回函数不能使用箭头函数。
使用箭头函数给 data: () => {} this 指向 undefined 的时候,是会赋值给 vm._data,但是他会相当于一个全局的,只要你不刷新页面他就会缓存你的 data。
如果我们使用data() {}this指向 Vm 实例,所以他会随着实例更新。
箭头函数通过 call() 或 apply() 方法改变 this 指向时无效
let obj2 = {
a: 10,
b: function(n) {
let f = (n) => n + this.a;
return f(n);
},
c: function(n) {
let f = (n) => n + this.a;
let m = {
a: 20
};
return f.call(m,n);
}
};
console.log(obj2.b(1)); // 11
console.log(obj2.c(1)); // 11
箭头函数没有原型属性Protptype
但还是会有.__ proto __,其值等于Function.Prototype
var a = ()=>{
return 1;
}
function b(){
return 2;
}
console.log(a.prototype); // undefined
console.log(b.prototype); // {constructor: ƒ}
箭头函数不能当做 Generator 函数,不能使用 yield 关键字
try/catch
//try_catch好处:发现错误但不让程序终止,继续执行之后的语句
try {
//先从上到下执行try里面的语句,一旦发现错误则跳出try,不再执行try下面的语句
console.log("a");
console.log(b);
console.log("c");
} catch (e) {
//如果try中发现错误,则执行catch中的语句,如果没有错误,则跳过catch
//e是个系统封装好的对象,包含name和message两个属性
//分别是错误名称(ex:ReferenceError)和错误信心(ex:b is not defined)
console.log(e.name + ":" + e.message);
}
console.log("d");
this指向
对象里的this:指向对象
函数里的this:this的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定this到底指向谁,实际上this的最终指向的是那个调用它的对象
修正this指向的方法
call 挨个传值,立即执行
apply 传一个数组,立即执行
bind 传值或数组都可以,不立即执行,返回一个新的函数,需要立即执行的话要再加一对括号——()
内存泄漏与垃圾回收
一、什么是内存泄漏?
程序的运行需要内存。只要程序提出要求,操作系统或者运行时(runtime)就必须供给内存。
对于持续运行的服务进程(daemon),必须及时释放不再用到的内存。否则,内存占用越来越高,轻则影响系统性能,重则导致进程崩溃。
不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)。
有些语言(比如 C 语言)必须手动释放内存,程序员负责内存管理。malloc方法用来申请内存,使用完毕之后,必须自己用free方法释放内存。
这很麻烦,所以大多数语言提供自动内存管理,减轻程序员的负担,这被称为"垃圾回收机制"(garbage collector)。
二、垃圾回收机制
垃圾回收机制怎么知道,哪些内存不再需要呢?
最常使用的方法叫做**["引用计数"]**(reference counting):语言引擎有一张"引用表",保存了内存里面所有的资源(通常是各种值)的引用次数。如果一个值的引用次数是0,就表示这个值不再用到了,因此 js 会自动执行垃圾回收,将这块内存释放。
上图中,左下角的两个值,没有任何引用,所以可以释放。
如果一个值不再需要了,引用数却不为0,垃圾回收机制无法释放这块内存,从而导致内存泄漏。
const arr = [1, 2, 3, 4];
console.log("hello world");
上面代码中,数组[1, 2, 3, 4]是一个值,会占用内存。变量arr是仅有的对这个值的引用,因此引用次数为1。尽管后面的代码没有用到arr,它还是会持续占用内存。
如果增加一行代码,解除arr对[1, 2, 3, 4]引用,这块内存就可以被垃圾回收机制释放了。
let arr = [1, 2, 3, 4];
console.log("hello world");
arr = null;
上面代码中,arr重置为null,就解除了对[1, 2, 3, 4]的引用,引用次数变成了0,内存就可以释放出来了。
因此,并不是说有了垃圾回收机制,程序员就轻松了。你还是需要关注内存占用:那些很占空间的值,一旦不再用到,你必须检查是否还存在对它们的引用。如果是的话,就必须手动解除引用。
三、内存泄漏的识别方法
怎样可以观察到内存泄漏呢?
[经验法则]是,如果连续五次垃圾回收之后,内存占用一次比一次大,就有内存泄漏。这就要求实时查看内存占用。
3.1 浏览器
Chrome 浏览器查看内存占用,按照以下步骤操作。
- 打开开发者工具,选择 Timeline 面板
- 在顶部的
Capture字段里面勾选 Memory- 点击左上角的录制按钮。
- 在页面上进行各种操作,模拟用户的使用情况。
- 一段时间后,点击对话框的 stop 按钮,面板上就会显示这段时间的内存占用情况。
如果内存占用基本平稳,接近水平,就说明不存在内存泄漏。
反之,就是内存泄漏了。
3.2 命令行
命令行可以使用 Node 提供的process.memoryUsage方法。
console.log(process.memoryUsage());
// { rss: 27709440,
// heapTotal: 5685248,
// heapUsed: 3449392,
// external: 8772 }
process.memoryUsage返回一个对象,包含了 Node 进程的内存占用信息。该对象包含四个字段,单位是字节,含义如下。
- rss(resident set size):所有内存占用,包括指令区和堆栈。
- heapTotal:"堆"占用的内存,包括用到的和没用到的。
- heapUsed:用到的堆的部分。
- external: V8 引擎内部的 C++ 对象占用的内存。
判断内存泄漏,以heapUsed字段为准。
四、导致内存泄漏的情况
-
函数内的全局变量,函数内的变量不声明直接使用的话会被视为全局变量,不被销毁
-
定时器内部有引用的变量
在 JavaScript 中使用 setInterval 非常平常。一段常见的代码:
var someResource = getData(); setInterval(function() { var node = document.getElementById('Node'); if(node) { // 处理 node 和 someResource node.innerHTML = JSON.stringify(someResource)); } }, 1000);此例说明了什么:与节点或数据关联的计时器不再需要,node 对象可以删除,整个回调函数也不需要了。可是,计时器回调函数仍然没被回收(计时器停止才会被回收)。同时,someResource 如果存储了大量的数据,也是无法被回收的。
-
使用事件监听 addEventListener 监听的时候,在不再监听的情况下要使用 removeEventListener 取消监听
-
闭包
闭包是 JavaScript 开发的一个关键方面:匿名函数可以访问父级作用域的变量。
代码示例:
var theThing = null; var replaceThing = function () { var originalThing = theThing; var unused = function () { if (originalThing) console.log("hi"); }; theThing = { longStr: new Array(1000000).join("*"), someMethod: function () { console.log(someMessage); }, }; }; setInterval(replaceThing, 1000);代码片段做了一件事情:每次调用 replaceThing ,theThing 得到一个包含一个大数组和一个新闭包(someMethod)的新对象。同时,变量 unused 是一个引用 originalThing 的闭包(先前的 replaceThing 又调用了 theThing )。思绪混乱了吗?最重要的事情是,闭包的作用域一旦创建,它们有同样的父级作用域,作用域是共享的。someMethod 可以通过 theThing 使用,someMethod 与 unused 分享闭包作用域,尽管 unused 从未使用,它引用的 originalThing 迫使它保留在内存中(防止被回收)。当这段代码反复运行,就会看到内存占用不断上升,垃圾回收器(GC)并无法降低内存占用。本质上,闭包的链表已经创建,每一个闭包作用域携带一个指向大数组的间接的引用,造成严重的内存泄露。
五、解决内存泄漏 WeakMap
前面说过,及时清除引用非常重要。但是,你不可能记得那么多,有时候一疏忽就忘了,所以才有那么多内存泄漏。
最好能有一种方法,在新建引用的时候就声明,哪些引用必须手动清除,哪些引用可以忽略不计,当其他引用消失以后,垃圾回收机制就可以释放内存。这样就能大大减轻程序员的负担,你只要清除主要引用就可以了。
ES6 考虑到了这一点,推出了两种新的数据结构:WeakSet 和 WeakMap。它们对于值的引用都是不计入垃圾回收机制的,所以名字里面才会有一个"Weak",表示这是弱引用。
下面以 WeakMap 为例,看看它是怎么解决内存泄漏的。
const wm = new WeakMap();
const element = document.getElementById("example");
wm.set(element, "some information");
wm.get(element); // "some information"
上面代码中,先新建一个 Weakmap 实例。然后,将一个 DOM 节点作为键名存入该实例,并将一些附加信息作为键值,一起存放在 WeakMap 里面。这时,WeakMap 里面对element的引用就是弱引用,不会被计入垃圾回收机制。
也就是说,DOM 节点对象的引用计数是1,而不是2。这时,一旦消除对该节点的引用,它占用的内存就会被垃圾回收机制释放。Weakmap 保存的这个键值对,也会自动消失。
基本上,如果你要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap。
六、WeakMap 示例
WeakMap 的例子很难演示,因为无法观察它里面的引用会自动消失。此时,其他引用都解除了,已经没有引用指向 WeakMap 的键名了,导致无法证实那个键名是不是存在。
我一直想不出办法,直到有一天贺师俊老师提示,如果引用所指向的值占用特别多的内存,就可以通过process.memoryUsage方法看出来。
根据这个思路,网友 vtxf 补充了下面的例子。
首先,打开 Node 命令行。
$ node --expose-gc
上面代码中,--expose-gc参数表示允许手动执行垃圾回收机制。
然后,执行下面的代码。
// 手动执行一次垃圾回收,保证获取的内存使用状态准确
> global.gc();
undefined
// 查看内存占用的初始状态,heapUsed 为 4M 左右
> process.memoryUsage();
{ rss: 21106688,
heapTotal: 7376896,
heapUsed: 4153936,
external: 9059 }
> let wm = new WeakMap();
undefined
> let b = new Object();
undefined
> global.gc();
undefined
// 此时,heapUsed 仍然为 4M 左右
> process.memoryUsage();
{ rss: 20537344,
heapTotal: 9474048,
heapUsed: 3967272,
external: 8993 }
// 在 WeakMap 中添加一个键值对,
// 键名为对象 b,键值为一个 5*1024*1024 的数组
> wm.set(b, new Array(5*1024*1024));
WeakMap {}
// 手动执行一次垃圾回收
> global.gc();
undefined
// 此时,heapUsed 为 45M 左右
> process.memoryUsage();
{ rss: 62652416,
heapTotal: 51437568,
heapUsed: 45911664,
external: 8951 }
// 解除对象 b 的引用
> b = null;
null
// 再次执行垃圾回收
> global.gc();
undefined
// 解除 b 的引用以后,heapUsed 变回 4M 左右
// 说明 WeakMap 中的那个长度为 5*1024*1024 的数组被销毁了
> process.memoryUsage();
{ rss: 20639744,
heapTotal: 8425472,
heapUsed: 3979792,
external: 8956 }
上面代码中,只要外部的引用消失,WeakMap 内部的引用,就会自动被垃圾回收清除。由此可见,有了它的帮助,解决内存泄漏就会简单很多。
闭包
对闭包的理解:函数f1的返回值是一个函数f2,f2可以在f1外部读到f1的内部参数,示例:
function f1(){
var n=999;
function f2(){
alert(n)
}
return f2;
}
var result=f1();
result(); // 999
作用:一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中,避免垃圾回收。
使用闭包的注意点
1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
DOM
文档对象模型
一、创建节点
1、createElement:创建元素
let div = document.createElement("div");
2、createTextNode:创建文本节点
let text = document.createTextNode("hello");
3、cloneNode:克隆节点
let parent = document.getElementById("parent");
let parent2 = parent.cloneNode();
parent2.id = "parent2";
cloneNode() 接收一个 boolean 参数,可传入 true 或 false,如果不传入参数,则默认为 false。
let parent2 = parent.cloneNode(true); // 深拷贝
let parent2 = parent.cloneNode(false); // 浅拷贝
let parent2 = parent.cloneNode(); // 不传入参数,默认为false,浅拷贝
传 true 和传 false 的区别:
- true:表示复制调用方法的元素和该元素的子元素,即“深拷贝”。
- false:表示只复制调用方法的元素,不复制子元素,即“浅拷贝”。
4、createDocumentFragment:创建文档片段
fragment 有片段、分段的意思。
如果要向 html 文档中添加的元素太多,可能会造成浏览器的回流。回流是指元素大小和位置会被重新计算,这样会造成性能问题。
这时我们就可以使用 createDocumentFragment 。
createDocumentFragment 不是文档树的一部分,它是保存在内存中的, 所以不会造成回流问题。
可以简单将 fragment 理解为存在于内存中的一个容器,先将 js 创建出来的元素添加到这个容器中,然后再把这个容器中的元素一次性添加到 html 文档中。
let fragment = document.createDocumentFragment();
for (let i = 0; i < 100; i++) {
let li = document.createElement("li");
fragment.appendChild(li);
}
let ul = document.createElement("ul");
ul.appendChild(fragment);
二、节点修改
1、appendChild
在调用该方法的节点的子元素末尾添加指定节点。
parent.appendChild(child); // 在 parent 元素的子元素末尾添加 child 元素。
2、insertBefore
用来添加一个节点到某个节点之前。
parent.insertBefore(新节点, 参照节点);
注意:
- 参照节点为必传参数,如果不传就会报错
- 如果参照节点是 undefined 或 null ,则新节点会被添加到子元素的末尾
3、removeChild:删除子节点
let childNode = parent.removeChild(childNode);
注意:如果被删除的不是其子节点,会报错。
我们可以通过下面的方式,来确保删除的是子节点:
if (node.parentNode) {
node.parentNode.removeChild(node); // 获取节点的父节点,然后删除自身
}
4、replaceChild
用于使用新节点替换旧节点。
parent.replaceChild(新节点, 旧节点);
- 新节点是替换节点,可以是新的节点,也可以是页面中的某个节点,如果是页面中的节点,则该节点会被移动到新的位置。
- 旧节点是被替换的节点。
三、节点查询
1、document.getElementById
let parent = document.getElementById("parent"); // 获取 id 为 parent 的元素
2、document.getElementsByTagName
根据元素标签名获取元素,返回一个即时的 HTMLCollection 类型。
即时的 HTMLCollection,表示该集合是随时变化的,随着页面中指定元素的数量的变化而变化。
let divs = document.getElementsByTagName("div");
3、document.getElementsByName
通过指定的 name 属性,来获取元素。
与 document.getElementsByTagName 类似,它返回的是一个即时的 NodeList 对象,它也是随时变化的。
4、document.getElementsByClassName
根据元素的 class 属性值返回一个即时的 HTMLCollection 对象。
let elements = document.getElementsByClassName("name");
注意点:
- 即时的 HTMLCollection,会随时变化
- IE9 以下浏览器不支持
如果要获取多个类名不同的元素,可传入多个 classname,每个用空格相隔。
let elements = document.getElementsByClassName("test1 test2");
5、document.querySelector 与 document.querySelectorAll
这两个 API,都是通过 css 选择器来查找元素。
不同点是,前者获取到的是一个元素,后者获取到的是多个元素。
- document.querySelector 返回第一个匹配的元素,如果没有匹配的元素,返回 null。
- document.querySelectorAll 返回的是一个非即时的 NodeList ,也就是说结果不会随着文档树的变化而变化。
IE8 以下的浏览器都不支持这两个 API。
四、节点关系
1、父关系型
parentNode:每个节点都有一个 parentNode 属性,它表示元素的父节点。Element 的父节点可能是 Element,Document 或 DocumentFragment。
let parentNode = node.parentNode;
parentElement:返回元素的父元素节点,与 parentNode 的区别在于,其父元素必须是一个 Element,如果不是,返回 null。
let parentElement = node.parentElement;
2、兄弟关系型
previousSibling:当前节点的前一个节点。如果当前节点为第一个节点,则返回 null。
let previousNode = node.previousSibling;
previousElementSibling:当前节点的前一个元素节点。
let previousElement = node.previousElementSibling;
previousSibling 和 previousElementSibling 的区别:
- 前者返回的可能是元素节点,也有可能是文本节点或注释节点
- 后者返回的只是元素节点
nextSibling:当前节点的后一个节点。如果当前节点是最后一个节点,则返回 null。
let nextNode = node.nextElementSibling;
nextElementSibling:当前节点的后一个元素节点。
let nextElement = node.nextElementSibling
nextSibling 和 nextElementSibling 的区别:
- 前者返回的可能是元素节点,也有可能是文本节点或注释节点
- 后者返回的只是元素节点
3、子关系型
childNodes:返回一个即时的 NodeList。
let child = parent.childNodes;
children:返回一个即时的 HTMLCollection。
let children = node.children;
childNodes 和 children 的区别:
- 前者返回的子节点列表中,可能包含元素节点、文本节点、注释节点等
- 后者返回的子节点中,只包含元素节点
firstChild:获取第一个子节点,有可能是元素节点、文本节点、注释节点等。
var childNode = node.firstChild;
firstElementChild:获取第一个元素节点。
var element = node.firstElementChild;
lastChild:获取最后一个子节点,有可能是元素节点、文本节点、注释节点等。
var last_child = element.lastChild;
lastElementChild:获取最后一个元素节点。
var element = node.lastElementChild;
五、元素属性型 API
1、setAttribute
根据名称和值修改元素的特性。
element.setAttribute(name, value);
name 是属性名,value 是属性值。
你也可以使用下面这个方法来替代上面的方法:
element.name = value;
2、getAttribute
用来获取属性名对应的属性值。
let value = element.getAttribute(name);
如果属性值不存在,则返回 null 或空字符串。
你也可以使用下面的方法代替:
let value = element.name;
3、innerHTML、innerText
innerText不管你写什么都直接显示。但是innerHTML的话你写的 html 代码(包括 js 部分)可以执行。所以最好不要用 innerHTML 因为它会把用户的代码当做开发者的代码,不够安全。
element.innerHTML="hello";
六、样式相关
直接修改元素的样式
element.style.color = 'red';
element.style.setProperty('font-size', '16px');
element.style.removeProperty('color');
动态添加样式规则
var style = document.createElement('style');
style.innerHTML = 'body{color:red} #top:hover{background-color: red;color: white;}';
document.head.appendChild(style);
BOM
浏览器对象模型
BOM(Browser Object Model) 是指浏览器对象模型,浏览器对象模型提供了独立于内容的、可以与浏览器窗口进行互动的对象结构。BOM 由多个对象组成,其中代表浏览器窗口的 Window 对象是 BOM 的顶层对象,其他对象都是该对象的子对象。
我们在浏览器中的一些操作都可以使用 BOM 的方式进行编程处理,
比如:刷新浏览器、后退、前进、在浏览器中输入 URL 等
window 对象包括六个子对象:document,frames,history,location,navigator,screen
window 对象窗口处理
-
document 对象窗口中加载的文档处理(该对象的属性和方法都很少用或不用!)
-
frames 对象窗口的多个框架布局(该对象也不用!)
-
history 对象处理浏览器的浏览历史
-
location 对象处理当前文档的 URL
-
navigator 对象提供浏览器的相关信息
-
screen 对象提供显示器的信息(窗口大小,分辨率)
window
window 是浏览器的顶级对象,当调用 window 下的属性和方法时,可以省略 window
open(targetUrl,pageName,"height=400,width=400,top=10,left=10"):打开一张新的网页
close():关闭。
innerHeight:浏览器窗口的内部高度(兼容所有浏览器)—包含滚动条
innerWidth:浏览器窗口的内部宽度(兼容所有浏览器)
outerWidth :整个浏览器宽度
outerHeight :整个浏览器高度
history
相当于操作浏览器前进后后退按钮
back:向后退一页
forward:向前进一页
go():根据参数做不同操作,0/空刷新页面,-1 退一页,1 前进一页,2 前进两页
length:返回历史列表中的网址数。
location
对浏览器地址栏,和 URL 的操作
reload():重新加载当前页面(刷新嘛)
assign(url):浏览器地址复上新的地址并载入
replace():同上
hash:获取当前 url#后面的字符串,如果没有就返回空字符串
herf:指定地址栏的新地址并载入
hostname :返回 web 主机域名
pathname:返回当前页面路径和文件名
port:返回 web 主机的端口
protocol:返回使用的 web 协议(http:// 或 https://)
navigator
浏览器信息查询
userAgent:返回由客户机发送服务器的 userAgent。
appName:返回浏览器名称。
appVersion:返回浏览器平台和版本信息。
platform:返回运行浏览器的操作系统平台。
screen
获取屏幕像宽高
availHeight:屏幕的高度像素减去系统部件高度之后的值
availWidth:屏幕的宽度像素减去系统部件宽度后的值
height:屏幕像素的高度
width:屏幕像素的宽度
Promise
Promise
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理且更强大。它最早由社区提出并实现,ES6 将其写进了语言标准,统一了用法,并原生提供了 Promise 对象。
特点
- 对象的状态不受外界影响 (3 种状态)
- Pending 状态(进行中)
- Fulfilled 状态(已成功)
- Rejected 状态(已失败)
- 一旦状态改变就不会再变 (两种状态改变:成功或失败)
- Pending -> Fulfilled
- Pending -> Rejected
用法
创建 Promise 实例
var promise = new Promise(function(resolve, reject){
// ... some code
if (/* 异步操作成功 */) {
resolve(value);
} else {
reject(error);
}
})
Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。
resolve 作用是将 Promise 对象状态由“未完成”变为“成功”,也就是Pending -> Resolved,在异步操作成功时调用,并将异步操作的结果作为参数传递出去;而 reject 函数则是将 Promise 对象状态由“未完成”变为“失败”,也就是Pending -> Rejected,在异步操作失败时调用,并将异步操作的结果作为参数传递出去。
then
Promise 实例生成后,可用then方法分别指定两种状态回调参数。then 方法可以接受两个回调函数作为参数:
- Promise 对象状态改为 Resolved 时调用 (必选)
- Promise 对象状态改为 Rejected 时调用 (可选)
基本用法示例
function sleep(ms) {
return new Promise(function (resolve, reject) {
setTimeout(resolve, ms);
});
}
sleep(500).then(() => console.log("finished"));
这段代码定义了一个函数 sleep,调用后,等待了指定参数(500)毫秒后执行 then 中的函数。值得注意的是,Promise 新建后就会立即执行。
执行顺序
接下来我们探究一下它的执行顺序,看以下代码:
let promise = new Promise(function (resolve, reject) {
console.log("AAA");
resolve();
});
promise.then(() => console.log("BBB"));
console.log("CCC");
// AAA
// CCC
// BBB
执行后,我们发现输出顺序总是 AAA -> CCC -> BBB。表明,在 Promise 新建后会立即执行,所以首先输出 AAA。(包括resolve()如果有后面语句也会执行)然后,then 方法指定的回调函数将在当前脚本所有同步任务执行完后才会执行,所以BBB 最后输出。
与定时器混用
首先看一个实例:
let promise = new Promise(function (resolve, reject) {
console.log("1");
resolve();
});
setTimeout(() => console.log("2"), 0);
promise.then(() => console.log("3"));
console.log("4");
// 1
// 4
// 3
// 2
可以看到,结果输出顺序总是:1 -> 4 -> 3 -> 2。1 与 4 的顺序不必再说,而 2 与 3 先输出 Promise 的 then,而后输出定时器任务。原因则是 Promise 属于 JavaScript 引擎内部任务,而 setTimeout 则是浏览器 API,而引擎内部任务优先级高于浏览器 API 任务,所以有此结果。
拓展 async/await
async
顾名思义,异步。async 函数对 Generator 函数的改进,async 函数必定返回 Promise,我们把所有返回 Promise 的函数都可以认为是异步函数。特点体现在以下四点:
- 内置执行器
- 更好的语义
- 更广的适用性
- 返回值是 Promise
await
顾名思义,等待。正常情况下,await 命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。另一种情况是,await 命令后面是一个 thenable 对象(即定义 then 方法的对象),那么 await 会将其等同于 Promise 对象。
混合使用
先看示例:
function sleep(ms) {
return new Promise(function (resolve, reject) {
setTimeout(resolve, ms);
});
}
async function handle() {
console.log("AAA");
await sleep(5000);
console.log("BBB");
}
handle();
// AAA
// BBB (5000ms后)
我们定义函数 sleep,返回一个 Promise。然后在 handle 函数前加上 async 关键词,这样就定义了一个 async 函数。在该函数中,利用 await 来等待一个 Promise。
Promise 优缺点
| 优点 | 缺点 |
|---|---|
| 解决回调 | 无法监测进行状态 |
| 链式调用 | 新建立即执行且无法取消 |
| 减少嵌套 | 内部错误无法抛出 |
链式调用
then 在链式调用时,会等前一个 then 或者函数执行完毕,返回状态,才会执行回调函数。后面的 then 里可以不用调用 resolve
(1)代码顺序执行,第一步调用了函数 cook ,cook 执行返回了一个 promise,promise 返回的是成功状态,即 resolve('鸡蛋炒饭'),那么参数“'鸡蛋炒饭'”会传递给下一个 then。
(2)第一个 then 接收“'鸡蛋炒饭'”,执行 then 的回调。回调中调用了 eat,把'鸡蛋炒饭'作为参数传递给了 eat。eat 执行(里面输出的步骤就不讲了,代码顺序执行,输出的“开始吃饭”等等),并返回 promise,promise 返回的是成功状态,并给下一个 then 传递了参数'一块碗和一双筷子'。
(3)第二个 then 接收'一块碗和一双筷子','执行 then 的回调。回调中调用了 wash,把'一块碗和一双筷子'作为参数传递给了 wash。wash 执行,并返回 promise,promise 返回成功状态,并给下一个 then 传递了参数'干净的碗筷'。
(4)最后一个 then,接收'干净的碗筷”,执行回调,输出'干净的碗筷”。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.js"></script>
<script type="text/javascript">
function cook() {
console.log('开始做饭。');
var p = new Promise(function(resolve, reject){
setTimeout(function() {
console.log('做饭完毕!');
resolve('鸡蛋炒饭');
}, 1000);
});
return p;
}
function eat(data) {
console.log('开始吃饭:' + data);
var p = new Promise(function(resolve, reject) {
setTimeout(function() {
console.log('吃饭完毕!');
resolve('一块碗和一双筷子');// resolve()的值会传递给then中function的data参数,供下一个方法使用。
}, 2000);
});
//这里的return的作用是把第一个回调函数的返回结果作为参数,传递给第二个回调函数
return p;
}
function wash(data) {
console.log('开始洗碗:' + data);
var p = new Promise(function(resolve, reject) {
setTimeout(function() {
console.log('洗碗完毕!');
resolve('干净的碗筷');
}, 2000);
});
return p;
}
//补充代码
cook().then(resolve => {
return eat(resolve) //第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。所以这里要用return返回函数去传参。
}).then(resolve => {
return wash(resolve);
}).then(resolve => {
//在Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。
//then方法执行的是resolve这个回调,并且这个函数都接受Promise对象传出的值作为参数。而这里“鸡蛋炒饭”就是作为参数传递的
console.log(resolve); //resolve中的值是传递到then方法中的参数,只有在then中通过console.log输出传入的参数,才可以在控制台查看到消息
})
//也可以像下面这样写,因为这三个函数本身设置的有return才可以这样直接写
//下一个then的回调函数,会等上一个then中的回调函数执行完毕,返回promise状态,就执行.
//首先eat,wash本身就是一个函数,所以可以直接作为then中的回到函数.
//然后eat,wash函数内部也返回了promise,所以这样写没有问题.
cook()
.then(eat)
.then(wash);
</script>
</body>
</html>
.then 中 1.resolve 返回一个值,将返回的值作为下一个 then 中回调函数的参数值 2.如果返回的是一个 promise 对象,将这个 Promise 接受状态的回调函数中参数值作为下一个 then 回调函数的参数值。 .then(fn(argument){})中的匿名函数实际上就是执行实例化 Promise 对象中的 resolve(),当 resolve(argument)中有参数时,可以将参数传给.then()中的匿名函数 这里原理都是一样的,就是 Promise 实例生成以后,可以用 then 方法分别指定 resolved 状态和 rejected 状态的回调函数。且都会返回一个 Promise 实例,所以可以链式调用 then 方法,上一个的返回值,可以作为下一个的参数。
asnyc/await
1. async 和 await 在干什么
任意一个名称都是有意义的,先从字面意思来理解。async 是“异步”的简写,而 await 可以认为是 async wait 的简写。所以应该很好理解 async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。
另外还有一个很有意思的语法规定,await 只能出现在 async 函数中。然后细心的朋友会产生一个疑问,如果 await 只能出现在 async 函数中,那这个 async 函数应该怎么调用?
如果需要通过 await 来调用一个 async 函数,那这个调用的外面必须得再包一个 async 函数,然后……进入死循环,永无出头之日……
如果 async 函数不需要 await 来调用,那 async 到底起个啥作用?
1.1. async 起什么作用
这个问题的关键在于,async 函数是怎么处理它的返回值的!
我们当然希望它能直接通过 return 语句返回我们想要的值,但是如果真是这样,似乎就没 await 什么事了。所以,写段代码来试试,看它到底会返回什么:
async function testAsync() {
return "hello async";
}
const result = testAsync();
console.log(result);
看到输出就恍然大悟了——输出的是一个 Promise 对象。
c:\var\test> node --harmony_async_await .
Promise { 'hello async' }
所以,async 函数返回的是一个 Promise 对象。从文档中也可以得到这个信息。async 函数(包含函数语句、函数表达式、Lambda表达式)会返回一个 Promise 对象,如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。
补充知识点 [2020-06-04]
Promise.resolve(x)可以看作是new Promise(resolve => resolve(x))的简写,可以用于快速封装字面量对象或其他对象,将其封装成 Promise 实例。
async 函数返回的是一个 Promise 对象,所以在最外层不能用 await 获取其返回值的情况下,我们当然应该用原来的方式:then() 链来处理这个 Promise 对象,就像这样
testAsync().then(v => {
console.log(v); // 输出 hello async
});
现在回过头来想下,如果 async 函数没有返回值,又该如何?很容易想到,它会返回 Promise.resolve(undefined)。
联想一下 Promise 的特点——无等待,所以在没有 await 的情况下执行 async 函数,它会立即执行,返回一个 Promise 对象,并且,绝不会阻塞后面的语句。这和普通返回 Promise 对象的函数并无二致。
那么下一个关键点就在于 await 关键字了。
1.2. await 到底在等啥
一般来说,都认为 await 是在等待一个 async 函数完成。不过按语法说明,await 等待的是一个表达式,这个表达式的计算结果是 Promise 对象或者其它值(换句话说,就是没有特殊限定)。
因为 async 函数返回一个 Promise 对象,所以 await 可以用于等待一个 async 函数的返回值——这也可以说是 await 在等 async 函数,但要清楚,它等的实际是一个返回值。注意到 await 不仅仅用于等 Promise 对象,它可以等任意表达式的结果,所以,await 后面实际是可以接普通函数调用或者直接量的。所以下面这个示例完全可以正确运行
function getSomething() {
return "something";
}
async function testAsync() {
return Promise.resolve("hello async");
}
async function test() {
const v1 = await getSomething();
const v2 = await testAsync();
console.log(v1, v2);
}
test();
1.3. await 等到了要等的,然后呢
await 等到了它要等的东西,一个 Promise 对象,或者其它值,然后呢?我不得不先说,await 是个运算符,用于组成表达式,await 表达式的运算结果取决于它等的东西。
如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。
如果它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。
看到上面的阻塞一词,心慌了吧……放心,这就是 await 必须用在 async 函数中的原因。async 函数调用不会造成阻塞,它内部所有的阻塞都被封装在一个 Promise 对象中异步执行。
2. async/await 帮我们干了啥
2.1. 作个简单的比较
上面已经说明了 async 会将其后的函数(函数表达式或 Lambda)的返回值封装成一个 Promise 对象,而 await 会等待这个 Promise 完成,并将其 resolve 的结果返回出来。
现在举例,用 setTimeout 模拟耗时的异步操作,先来看看不用 async/await 会怎么写
function takeLongTime() {
return new Promise(resolve => {
setTimeout(() => resolve("long_time_value"), 1000);
});
}
takeLongTime().then(v => {
console.log("got", v);
});
如果改用 async/await 呢,会是这样
function takeLongTime() {
return new Promise(resolve => {
setTimeout(() => resolve("long_time_value"), 1000);
});
}
async function test() {
const v = await takeLongTime();
console.log(v);
}
test();
眼尖的同学已经发现 takeLongTime() 没有申明为 async。实际上,takeLongTime() 本身就是返回的 Promise 对象,加不加 async 结果都一样,如果没明白,请回过头再去看看上面的“async 起什么作用”。
又一个疑问产生了,这两段代码,两种方式对异步调用的处理(实际就是对 Promise 对象的处理)差别并不明显,甚至使用 async/await 还需要多写一些代码,那它的优势到底在哪?
2.2. async/await 的优势在于处理 then 链
单一的 Promise 链并不能发现 async/await 的优势,但是,如果需要处理由多个 Promise 组成的 then 链的时候,优势就能体现出来了(很有意思,Promise 通过 then 链来解决多层回调的问题,现在又用 async/await 来进一步优化它)。
假设一个业务,分多个步骤完成,每个步骤都是异步的,而且依赖于上一个步骤的结果。我们仍然用 setTimeout 来模拟异步操作:
/**
* 传入参数 n,表示这个函数执行的时间(毫秒)
* 执行的结果是 n + 200,这个值将用于下一步骤
*/
function takeLongTime(n) {
return new Promise(resolve => {
setTimeout(() => resolve(n + 200), n);
});
}
function step1(n) {
console.log(`step1 with ${n}`);
return takeLongTime(n);
}
function step2(n) {
console.log(`step2 with ${n}`);
return takeLongTime(n);
}
function step3(n) {
console.log(`step3 with ${n}`);
return takeLongTime(n);
}
现在用 Promise 方式来实现这三个步骤的处理
function doIt() {
console.time("doIt");
const time1 = 300;
step1(time1)
.then(time2 => step2(time2))
.then(time3 => step3(time3))
.then(result => {
console.log(`result is ${result}`);
console.timeEnd("doIt");
});
}
doIt();
// c:\var\test>node --harmony_async_await .
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
// doIt: 1507.251ms
输出结果 result 是 step3() 的参数 700 + 200 = 900。doIt() 顺序执行了三个步骤,一共用了 300 + 500 + 700 = 1500 毫秒,和 console.time()/console.timeEnd() 计算的结果一致。
如果用 async/await 来实现呢,会是这样
async function doIt() {
console.time("doIt");
const time1 = 300;
const time2 = await step1(time1);
const time3 = await step2(time2);
const result = await step3(time3);
console.log(`result is ${result}`);
console.timeEnd("doIt");
}
doIt();
结果和之前的 Promise 实现是一样的,但是这个代码看起来是不是清晰得多,几乎跟同步代码一样
2.3. 还有更酷的
现在把业务要求改一下,仍然是三个步骤,但每一个步骤都需要之前每个步骤的结果。
function step1(n) {
console.log(`step1 with ${n}`);
return takeLongTime(n);
}
function step2(m, n) {
console.log(`step2 with ${m} and ${n}`);
return takeLongTime(m + n);
}
function step3(k, m, n) {
console.log(`step3 with ${k}, ${m} and ${n}`);
return takeLongTime(k + m + n);
}
这回先用 async/await 来写:
async function doIt() {
console.time("doIt");
const time1 = 300;
const time2 = await step1(time1);
const time3 = await step2(time1, time2);
const result = await step3(time1, time2, time3);
console.log(`result is ${result}`);
console.timeEnd("doIt");
}
doIt();
// c:\var\test>node --harmony_async_await .
// step1 with 300
// step2 with 800 = 300 + 500
// step3 with 1800 = 300 + 500 + 1000
// result is 2000
// doIt: 2907.387ms
除了觉得执行时间变长了之外,似乎和之前的示例没啥区别啊!别急,认真想想如果把它写成 Promise 方式实现会是什么样子?
function doIt() {
console.time("doIt");
const time1 = 300;
step1(time1)
.then(time2 => {
return step2(time1, time2)
.then(time3 => [time1, time2, time3]);
})
.then(times => {
const [time1, time2, time3] = times;
return step3(time1, time2, time3);
})
.then(result => {
console.log(`result is ${result}`);
console.timeEnd("doIt");
});
}
doIt();
有没有感觉有点复杂的样子?那一堆参数处理,就是 Promise 方案的死穴—— 参数传递太麻烦了,看着就晕!
事件绑定、冒泡/捕获、事件委托
绑定事件的两种方式/DOM 事件的级别
这里讲一下绑定(注册)事件的两种方式,我们以 onclick 事件为例。
DOM0 的写法:onclick
element.onclick = function () {};
举例:
<body>
<button>点我</button>
<script>
var btn = document.getElementsByTagName("button")[0];
//这种事件绑定的方式,如果绑定多个,则后面的会覆盖掉前面的
btn.onclick = function () {
console.log("事件1");
};
btn.onclick = function () {
console.log("事件2");
};
</script>
</body>
点击按钮后,上方代码的打印结果:
事件2
我们可以看到,DOM对象.事件 = 函数的这种绑定事件的方式:一个元素的一个事件只能绑定一个响应函数。如果绑定了多个响应函数,则后者会覆盖前者。
DOM2 的写法:addEventListener(高版本浏览器)
element.addEventListener("click", function () {}, false);
参数解释:
-
参数 1:事件名的字符串(注意,没有 on)
-
参数 2:回调函数:当事件触发时,该函数会被执行
-
参数 3:true 表示捕获阶段触发,false 表示冒泡阶段触发(默认)
举例:
<body>
<button>按钮</button>
<script>
var btn = document.getElementsByTagName("button")[0];
// addEventListener: 事件监听器。 原事件被执行的时候,后面绑定的事件照样被执行
// 这种写法不存在响应函数被覆盖的情况。(更适合团队开发)
btn.addEventListener("click", fn1);
btn.addEventListener("click", fn2);
function fn1() {
console.log("事件1");
}
function fn2() {
console.log("事件2");
}
</script>
</body>
点击按钮后,上方代码的打印结果:
事件1 事件2
我们可以看到,addEventListener()这种绑定事件的方式:
-
一个元素的一个事件,可以绑定多个响应函数。不存在响应函数被覆盖的情况。执行顺序是:事件被触发时,响应函数会按照函数的绑定顺序执行。
-
addEventListener()中的 this,是绑定事件的对象。
-
addEventListener()不支持 IE8 及以下的浏览器。在 IE8 中可以使用attachEvent来绑定事件(详见下一小段)。
事件对象
当事件的响应函数被触发时,会产生一个事件对象event。浏览器每次都会将这个事件event作为实参传进之前的响应函数。
这个对象中包含了与当前事件相关的一切信息。比如鼠标的坐标、键盘的哪个按键被按下、鼠标滚轮滚动的方向等。
获取 event 对象(兼容性问题)
所有浏览器都支持 event 对象,但支持的方式不同。如下。
(1)普通浏览器的写法是 event。比如:
(2)ie 678 的写法是 window.event。此时,事件对象 event 是作为 window 对象的属性保存的。
于是,我们可以采取一种兼容性的写法。如下:
event = event || window.event; // 兼容性写法
代码举例:
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8" />
<title></title>
</head>
<body>
<script>
//点击页面的任何部分
document.onclick = function (event) {
event = event || window.event; ////兼容性写法
console.log(event);
console.log(event.timeStamp);
console.log(event.bubbles);
console.log(event.button);
console.log(event.pageX);
console.log(event.pageY);
console.log(event.screenX);
console.log(event.screenY);
console.log(event.target);
console.log(event.type);
console.log(event.clientX);
console.log(event.clientY);
};
</script>
</body>
</html>
event 属性
event 有很多属性,比如:
由于 pageX 和 pageY 的兼容性不好,我们可以这样做:
- 鼠标在页面的位置 = 滚动条滚动的距离 + 可视区域的坐标。
Event 举例
举例 1:使 div 跟随鼠标移动
代码实现:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title></title>
<style type="text/css">
#box1 {
width: 100px;
height: 100px;
background-color: red;
/*
* 开启box1的绝对定位
*/
position: absolute;
}
</style>
<script type="text/javascript">
window.onload = function () {
/*
* 使div可以跟随鼠标移动
*/
//获取box1
var box1 = document.getElementById("box1");
//给整个页面绑定:鼠标移动事件
document.onmousemove = function (event) {
//兼容的方式获取event对象
event = event || window.event;
// 鼠标在页面的位置 = 滚动条滚动的距离 + 可视区域的坐标。
var pagex = event.pageX || scroll().left + event.clientX;
var pagey = event.pageY || scroll().top + event.clientY;
// 设置div的偏移量(相对于整个页面)
// 注意,如果想通过 style.left 来设置属性,一定要给 box1开启绝对定位。
box1.style.left = pagex + "px";
box1.style.top = pagey + "px";
};
};
// scroll 函数封装
function scroll() {
return {
//此函数的返回值是对象
left:
window.pageYOffset ||
document.body.scrollTop ||
document.documentElement.scrollTop,
right:
window.pageXOffset ||
document.body.scrollLeft ||
document.documentElement.scrollLeft,
};
}
</script>
</head>
<body style="height: 1000px;width: 2000px;">
<div id="box1"></div>
</body>
</html>
举例 2:获取鼠标距离所在盒子的距离
关键点:
鼠标距离所在盒子的距离 = 鼠标在整个页面的位置 - 所在盒子在整个页面的位置
代码演示:
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8" />
<title></title>
<style>
.box {
width: 300px;
height: 200px;
padding-top: 100px;
background-color: pink;
margin: 100px;
text-align: center;
font: 18px/30px "simsun";
cursor: pointer;
}
</style>
</head>
<body>
<div class="box"></div>
<script src="animate.js"></script>
<script>
//需求:鼠标进入盒子之后只要移动,哪怕1像素,随时显示鼠标在盒子中的坐标。
//技术点:新事件,onmousemove:在事件源上,哪怕鼠标移动1像素也会触动这个事件。
//一定程度上,模拟了定时器
//步骤:
//1.老三步和新五步
//2.获取鼠标在整个页面的位置
//3.获取盒子在整个页面的位置
//4.用鼠标的位置减去盒子的位置赋值给盒子的内容。
//1.老三步和新五步
var div = document.getElementsByTagName("div")[0];
div.onmousemove = function (event) {
event = event || window.event;
//2.获取鼠标在整个页面的位置
var pagex = event.pageX || scroll().left + event.clientX;
var pagey = event.pageY || scroll().top + event.clientY;
//3.获取盒子在整个页面的位置
// var xx =
// var yy =
//4.用鼠标的位置减去盒子的位置赋值给盒子的内容。
var targetx = pagex - div.offsetLeft;
var targety = pagey - div.offsetTop;
this.innerHTML =
"鼠标在盒子中的X坐标为:" +
targetx +
"px;<br>鼠标在盒子中的Y坐标为:" +
targety +
"px;";
};
</script>
</body>
</html>
实现效果:
举例 3:商品放大镜
代码实现:
(1)index.html:
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8" />
<title></title>
<style>
* {
margin: 0;
padding: 0;
}
.box {
width: 350px;
height: 350px;
margin: 100px;
border: 1px solid #ccc;
position: relative;
}
.big {
width: 400px;
height: 400px;
position: absolute;
top: 0;
left: 360px;
border: 1px solid #ccc;
overflow: hidden;
display: none;
}
/*mask的中文是:遮罩*/
.mask {
width: 175px;
height: 175px;
background: rgba(255, 255, 0, 0.4);
position: absolute;
top: 0;
left: 0;
cursor: move;
display: none;
}
.small {
position: relative;
}
img {
vertical-align: top;
}
</style>
<script src="tools.js"></script>
<script>
window.onload = function () {
//需求:鼠标放到小盒子上,让大盒子里面的图片和我们同步等比例移动。
//技术点:onmouseenter==onmouseover 第一个不冒泡
//技术点:onmouseleave==onmouseout 第一个不冒泡
//步骤:
//1.鼠标放上去显示盒子,移开隐藏盒子。
//2.老三步和新五步(黄盒子跟随移动)
//3.右侧的大图片,等比例移动。
//0.获取相关元素
var box = document.getElementsByClassName("box")[0];
var small = box.firstElementChild || box.firstChild;
var big = box.children[1];
var mask = small.children[1];
var bigImg = big.children[0];
//1.鼠标放上去显示盒子,移开隐藏盒子。(为小盒子绑定事件)
small.onmouseenter = function () {
//封装好方法调用:显示元素
show(mask);
show(big);
};
small.onmouseleave = function () {
//封装好方法调用:隐藏元素
hide(mask);
hide(big);
};
//2.老三步和新五步(黄盒子跟随移动)
//绑定的事件是onmousemove,而事件源是small(只要在小盒子上移动1像素,黄盒子也要跟随)
small.onmousemove = function (event) {
//新五步
event = event || window.event;
//想要移动黄盒子,必须要知道鼠标在small小图中的位置。
var pagex = event.pageX || scroll().left + event.clientX;
var pagey = event.pageY || scroll().top + event.clientY;
//x:mask的left值,y:mask的top值。
var x = pagex - box.offsetLeft - mask.offsetWidth / 2; //除以2,可以保证鼠标mask的中间
var y = pagey - box.offsetTop - mask.offsetHeight / 2;
//限制换盒子的范围
//left取值为大于0,小盒子的宽-mask的宽。
if (x < 0) {
x = 0;
}
if (x > small.offsetWidth - mask.offsetWidth) {
x = small.offsetWidth - mask.offsetWidth;
}
//top同理。
if (y < 0) {
y = 0;
}
if (y > small.offsetHeight - mask.offsetHeight) {
y = small.offsetHeight - mask.offsetHeight;
}
//移动黄盒子
console.log(small.offsetHeight);
mask.style.left = x + "px";
mask.style.top = y + "px";
//3.右侧的大图片,等比例移动。
//如何移动大图片?等比例移动。
// 大图片/大盒子 = 小图片/mask盒子
// 大图片走的距离/mask走的距离 = (大图片-大盒子)/(小图片-黄盒子)
// var bili = (bigImg.offsetWidth-big.offsetWidth)/(small.offsetWidth-mask.offsetWidth);
//大图片走的距离/mask盒子都的距离 = 大图片/小图片
var bili = bigImg.offsetWidth / small.offsetWidth;
var xx = bili * x; //知道比例,就可以移动大图片了
var yy = bili * y;
bigImg.style.marginTop = -yy + "px";
bigImg.style.marginLeft = -xx + "px";
};
};
</script>
</head>
<body>
<div class="box">
<div class="small">
<img src="images/001.jpg" alt="" />
<div class="mask"></div>
</div>
<div class="big">
<img src="images/0001.jpg" alt="" />
</div>
</div>
</body>
</html>
(2)tools.js:
/**
* Created by smyhvae on 2018/02/03.
*/
//显示和隐藏
function show(ele) {
ele.style.display = "block";
}
function hide(ele) {
ele.style.display = "none";
}
function scroll() {
// 开始封装自己的scrollTop
if (window.pageYOffset != null) {
// ie9+ 高版本浏览器
// 因为 window.pageYOffset 默认的是 0 所以这里需要判断
return {
left: window.pageXOffset,
top: window.pageYOffset,
};
} else if (document.compatMode === "CSS1Compat") {
// 标准浏览器 来判断有没有声明DTD
return {
left: document.documentElement.scrollLeft,
top: document.documentElement.scrollTop,
};
}
return {
// 未声明 DTD
left: document.body.scrollLeft,
top: document.body.scrollTop,
};
}
效果演示:
事件冒泡与事件捕获
HTML DOM 中有两种事件传播方式,即捕获和冒泡。
事件传播是一种在事件发生时定义元素顺序的方法。事件传播的三个阶段是:事件捕获、事件冒泡和目标。
-
事件捕获阶段:事件从祖先元素往子元素查找(DOM 树结构),直到捕获到事件目标 target。在这个过程中,默认情况下,事件相应的监听函数是不会被触发的。
-
事件目标:当到达目标元素之后,执行目标元素该事件相应的处理函数。如果没有绑定监听函数,那就不执行。
-
事件冒泡阶段:事件从事件目标 target 开始,从子元素往冒泡祖先元素冒泡,直到页面的最上一级标签。
如下图所示:
事件捕获
addEventListener 可以捕获事件:
box1.addEventListener(
"click",
function () {
alert("捕获 box3");
},
true
);
上面的方法中,参数为 true,代表事件在捕获阶段执行。
代码演示:
//参数为true,代表事件在「捕获」阶段触发;参数为false或者不写参数,代表事件在「冒泡」阶段触发
box3.addEventListener(
"click",
function () {
alert("捕获 child");
},
true
);
box2.addEventListener(
"click",
function () {
alert("捕获 father");
},
true
);
box1.addEventListener(
"click",
function () {
alert("捕获 grandfather");
},
true
);
document.addEventListener(
"click",
function () {
alert("捕获 body");
},
true
);
效果:body -> grandfather -> father -> child
重点:捕获阶段,事件依次传递的顺序是:window --> document --> html--> body --> 父元素、子元素、目标元素。
这几个元素在事件捕获阶段的完整写法是:
window.addEventListener(
"click",
function () {
alert("捕获 window");
},
true
);
document.addEventListener(
"click",
function () {
alert("捕获 document");
},
true
);
document.documentElement.addEventListener(
"click",
function () {
alert("捕获 html");
},
true
);
document.body.addEventListener(
"click",
function () {
alert("捕获 body");
},
true
);
fatherBox.addEventListener(
"click",
function () {
alert("捕获 father");
},
true
);
childBox.addEventListener(
"click",
function () {
alert("捕获 child");
},
true
);
说明:
(1)第一个接收到事件的对象是 window(有人会说 body,有人会说 html,这都是错误的)。
(2)JS 中涉及到 DOM 对象时,有两个对象最常用:window、doucument。它们俩是最先获取到事件的。
补充一个知识点:
在 js 中:
-
如果想获取
html节点,方法是document.documentElement。 -
如果想获取
body节点,方法是:document.body。
二者不要混淆了。
事件冒泡
事件冒泡: 当一个元素上的事件被触发的时候(比如说鼠标点击了一个按钮),同样的事件将会在那个元素的所有祖先元素中被触发。这一过程被称为事件冒泡;这个事件从原始元素开始一直冒泡到 DOM 树的最上层。
通俗来讲,冒泡指的是:子元素的事件被触发时,父元素的同样的事件也会被触发。取消冒泡就是取消这种机制。
代码演示:
//事件冒泡
box3.onclick = function () {
alert("child");
};
box2.onclick = function () {
alert("father");
};
box1.onclick = function () {
alert("grandfather");
};
document.onclick = function () {
alert("body");
};
效果:child -> father -> grandfather -> body
即使我改变代码的顺序,也不会影响效果的顺序。
当然,上面的代码中,我们用 addEventListener 这种 DOM2 的写法也是可以的,但是第三个参数要写 false,或者不写。
冒泡顺序:
一般的浏览器: (除 IE6.0 之外的浏览器)
- div -> body -> html -> document -> window
IE6.0:
- div -> body -> html -> document
不是所有的事件都能冒泡
以下事件不冒泡:blur、focus、load、unload、onmouseenter、onmouseleave。意思是,事件不会往父元素那里传递。
我们检查一个元素是否会冒泡,可以通过事件的以下参数:
event.bubbles;
如果返回值为 true,说明该事件会冒泡;反之则相反。
举例:
box1.onclick = function (event) {
alert("冒泡 child");
event = event || window.event;
console.log(event.bubbles); //打印结果:true。说明 onclick 事件是可以冒泡的
};
阻止冒泡
大部分情况下,冒泡都是有益的。当然,如果你想阻止冒泡,也是可以的。可以按下面的方法阻止冒泡。
阻止冒泡的方法
w3c 的方法:(火狐、谷歌、IE11)
event.stopPropagation();
IE10 以下则是:
event.cancelBubble = true;
兼容代码如下:
box3.onclick = function (event) {
alert("child");
//阻止冒泡
event = event || window.event;
if (event && event.stopPropagation) {
event.stopPropagation();
} else {
event.cancelBubble = true;
}
};
上方代码中,我们对 box3 进行了阻止冒泡,产生的效果是:事件不会继续传递到 father、grandfather、body 了。
阻止冒泡的举例
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title></title>
<style type="text/css">
#box1 {
width: 100px;
height: 100px;
background-color: red;
/*
* 开启box1的绝对定位
*/
position: absolute;
}
</style>
<script type="text/javascript">
window.onload = function () {
/*
* 使div可以跟随鼠标移动
*/
//获取box1
var box1 = document.getElementById("box1");
//给整个页面绑定:鼠标移动事件
document.onmousemove = function (event) {
//兼容的方式获取event对象
event = event || window.event;
// 鼠标在页面的位置 = 滚动条滚动的距离 + 可视区域的坐标。
var pagex = event.pageX || scroll().left + event.clientX;
var pagey = event.pageY || scroll().top + event.clientY;
// 设置div的偏移量(相对于整个页面)
// 注意,如果想通过 style.left 来设置属性,一定要给 box1 开启绝对定位。
box1.style.left = pagex + "px";
box1.style.top = pagey + "px";
};
// 【重要注释】
// 当 document.onmousemove 和 box2.onmousemove 同时触发时,通过 box2 阻止事件向 document 冒泡。
// 也就是说,只要是在 box2 的区域,就只触发 document.onmousemove 事件
var box2 = document.getElementById("box2");
box2.onmousemove = function (event) {
//阻止冒泡
event = event || window.event;
if (event && event.stopPropagation) {
event.stopPropagation();
} else {
event.cancelBubble = true;
}
};
};
// scroll 函数封装
function scroll() {
return {
//此函数的返回值是对象
left:
window.pageYOffset ||
document.body.scrollTop ||
document.documentElement.scrollTop,
right:
window.pageXOffset ||
document.body.scrollLeft ||
document.documentElement.scrollLeft,
};
}
</script>
</head>
<body style="height: 1000px;width: 2000px;">
<div
id="box2"
style="width: 300px; height: 300px; background-color: #bfa;"
></div>
<div id="box1"></div>
</body>
</html>
关键地方可以看代码中的注释。
效果演示:
事件委托
事件委托,通俗地来讲,就是把一个元素响应事件(click、keydown......)的函数委托到另一个元素。
比如说有一个列表 ul,列表之中有大量的列表项 <a>标签:
<ul id="parent-list">
<li><a href="javascript:;" class="my_link">超链接一</a></li>
<li><a href="javascript:;" class="my_link">超链接二</a></li>
<li><a href="javascript:;" class="my_link">超链接三</a></li>
</ul>
当我们的鼠标移到<a>标签上的时候,需要获取此<a>的相关信息并飘出悬浮窗以显示详细信息,或者当某个<a>被点击的时候需要触发相应的处理事件。我们通常的写法,是为每个<a>都绑定类似 onMouseOver 或者 onClick 之类的事件监听:
window.onload = function () {
var parentNode = document.getElementById("parent-list");
var aNodes = parentNode.getElementByTagName("a");
for (var i = 0, l = aNodes.length; i < l; i++) {
aNodes[i].onclick = function () {
console.log("我是超链接 a 的单击相应函数");
};
}
};
但是,上面的做法过于消耗内存和性能。我们希望,只绑定一次事件,即可应用到多个元素上,即使元素是后来添加的。
因此,比较好的方法就是把这个点击事件绑定到他的父层,也就是 ul 上,然后在执行事件函数的时候再去匹配判断目标元素。如下:
<script type="text/javascript">
window.onload = function () {
// 获取父节点,并为它绑定click单击事件。 false 表示事件在冒泡阶段触发(默认)
document.getElementById("parent-list").addEventListener(
"click",
function (event) {
event = event || window.event;
// e.target 表示:触发事件的对象
//如果触发事件的对象是我们期望的元素,则执行否则不执行
if (event.target && event.target.className == "link") {
// 或者写成 if (event.target && event.target.nodeName.toUpperCase() == 'A') {
console.log("我是ul的单击响应函数");
}
},
false
);
};
</script>
<ul id="parent-list" style="background-color: #bfa;">
<li>
<p>我是p元素</p>
</li>
<li><a href="javascript:;" class="link">超链接一</a></li>
<li><a href="javascript:;" class="link">超链接二</a></li>
<li><a href="javascript:;" class="link">超链接三</a></li>
</ul>
上方代码,为父节点注册 click 事件,当子节点被点击的时候,click 事件会从子节点开始向父节点冒泡。父节点捕获到事件之后,开始执行方法体里的内容:通过判断 event.target 拿到了被点击的子节点<a>。从而可以获取到相应的信息,并作处理。
换而言之,参数为 false,说明事件是在冒泡阶段触发(子元素向父元素传递事件)。而父节点注册了事件函数,子节点没有注册事件函数,此时,会在父节点中执行函数体里的代码。
总结:事件委托是利用了冒泡机制,减少了事件绑定的次数,减少内存消耗,提高性能。
事件循环机制
宏任务:script 整体代码、setTimeout、setInterval、setImmediate、I/O、UI rendering、UI 交互事件
微任务:promise(promise 里面立即执行,then 里面的放入异步)、node 里的 process.nextTick、MutationObserver
优先级:主线程 -> 清空微任务队列(包括这期间新产生的微任务) -> 一个宏任务(比如一个 setTimeOut) -> 清空微任务队列(包括这期间新产生的微任务) -> 一个宏任务
微任务中的微任务会一口气执行完
CommonJS
node 应用由模块组成,采用的 commonjs 模块规范。每一个文件就是一个模块,拥有自己独立的作用域,变量,以及方法等,对其他的模块都不可见。CommonJS 规范规定,每个模块内部,module 变量代表当前模块。这个变量是一个对象,它的 exports 属性(即 module.exports)是对外的接口。加载某个模块,其实是加载该模块的 module.exports 属性。require 方法用于加载模块。
CommonJS 模块的特点
所有代码都运行在模块作用域,不会污染全局作用域。
模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
模块加载的顺序,按照其在代码中出现的顺序。
CommonJS 用法
Node.js 应用由模块组成,每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。
CommonJS 规范规定,每个模块内部有两个变量可以使用,require 和 module。
require 用来加载某个模块。
module 代表当前模块。exports 是 module 上的一个属性,保存了当前模块要导出的接口或者变量,使用 require 加载的某个模块获取到的值就是那个模块使用 exports 导出的值
// a.js
var name = 'morrain'
var age = 18
module.exports.name = name
module.exports.getAge = function(){
return age
}
//b.js中引用a.js
var a = require('a.js')
console.log(a.name) // 'morrain'
console.log(a.getAge())// 18