JavaScript
JavaScript基础分为三个部分:
- ECMAScript:JavaScript的语法标准。包括变量、表达式、运算符、函数、if语句、for语句等。
- DOM:文档对象模型(Document object Model),操作网页上的元素的API。比如让盒子移动、变色、轮播图等。
- BOM:浏览器对象模型(Browser Object Model),操作浏览器部分功能的API。比如让浏览器自动滚动。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z5J77N9m-1629475321522)(assets\image-20210123170543868.png)]
ECMAScript
这一块可以结合js语法和es6语法一起复习
值类型和引用类型
引用类型作为参数传入函数,函数里面修改了,函数外面的值也会修改;而值类型就不会(会深拷贝一个值)
-
值类型:Number(数值),String(字符串),Boolean(布尔值),Undefined,Null
string类型比较特殊,可以将字符串看成行为与值类型相似的不可变引用类型
-
引用类型:Array(数组),Object(对象),Function(函数)
值类型也可以用new的方式创建出来,但这种方法创建出来的是对象。例如:
new String()
区别:
-
能否添加属性和方法
- 值类型无法添加属性和方法
- 引用类型可以添加属性和方法
-
存储位置不一样
- 值类型的变量会保存在栈内存中,如果在一个函数中声明一个值类型的变量,那么这个变量当函数执行结束之后会自动销毁
- 引用类型的变量名会保存在栈内存中,但是变量值会存储在堆内存中,引用类型的变量不会自动销毁,当没有引用变量引用它时,系统的垃圾回收机制会回收它
-
复制方式不一样
-
值类型的变量直接赋值就是深复制,如 var a = 10; var b = a;那么a的值就复制给b了,b修改值不会影响a
-
引用类型的变量直接赋值实际上是传递引用,只是浅复制。要想实现深复制,必须在堆内存中再开辟一块空间。
-
-
比较方式不一样
- 值类型的比较是值的比较,只有当它们的值相等的时候它们才相等。比较的时候注意””和”=”,双等号()在做比较的时候做了类型转换,而全等号(=)是值和类型相等是才能相等
- 引用类型的比较是引用地址的比较
数值
JavaScript的Number
类型为双精度IEEE 754 64位浮点类型。
// 字面量声明,得到基本数值
let num = 1;
// 对象声明,得到Number对象
var a = new Number('123'); // a === 123 is false,因为a是一个对象,不是一个数值
// 没有 new 操作符,Number能被用来执行类型转换(字符串转换为数字),转换后是数值,不是对象
var b = Number('123'); // b === 123 is true,
内置对象Number的属性
内置Number对象的属性和方法都不会用于基本数值上,而是直接作为Number对象的属性和方法使用。这点有别于基本字符串值可以使用String对象的属性和方法。
-
两个可表示(representable)数之间的最小间隔(1与Number可表示的大于1的最小浮点数的差值)。
-
JavaScript 中最大的安全整数 (
2^53 - 1
)。 -
能表示的最大正数。最小的负数是
-MAX_VALUE
。区别于全局对象Infinity
。 -
JavaScript 中最小的安全整数 (
-(2^53 - 1)
). -
能表示的最小正数即最接近 0 的正数 (实际上不会变成 0)。最大的负数是
-MIN_VALUE
。 -
特殊的“非数字”值。
-
特殊的负无穷大值,在溢出时返回该值。
-
特殊的正无穷大值,在溢出时返回该值。
Number.POSITIVE_INFINITY
的值同全局对象Infinity
属性的值相同。 -
Number 对象上允许的额外属性。
内置对象Number的方法
-
确定传递的值是否是 NaN。它是原来的全局
isNaN()
的更稳妥的版本。 -
确定传递的值类型及本身是否是有限数。
-
确定传递的值类型是“number”,且是整数。
-
确定传递的值是否为安全整数 ( -
(253 - 1)
至253 - 1之间
)。 -
计算传递的值并将其转换为整数 (或无穷大)。
-
和全局对象
parseFloat()
一样,解析一个参数(必要时先转换为字符串)并返回一个浮点数。 -
和全局对象
parseInt(str,[radix])
一样, 解析一个字符串为整数,radix
是2-36之间的整数,表示被解析字符串的基数(10不是默认值,需要手动指定)。当str无法解析为数字时,返回NaN。
字符串
从 ECMAScript 2015 开始,字符串字面量也可以称为模板字面量
// 字面量声明,得到基本字符串值
let str = 'abc'; // string
// 对象声明,得到String对象
var a = new String('abc'); // object
// 没有 new 操作符,String能被用来执行类型转换(例如:数字转换为字符串),转换后是基本字符串值,不是对象
var b = String(123); // string
字符串的属性
基本字符串值不是对象,但其拥有属性和方法,是因为在引用字符串属性的时候,js就会通过调用 new String(s)将其转换成对象,这个对象继承了内置字符串对象的属性和方法,但使用typeof检测该值仍为string而不是object。
-
可以为 String 对象增加新的属性。
-
String.prototype.constructor
用于创造对象的原型对象的特定的函数。
-
返回了字符串的长度。
字符串原型对象的方法
-
返回特定位置的字符。
-
返回表示给定索引的字符的Unicode的值。
-
String.prototype.codePointAt()
返回使用UTF-16编码的给定位置的值的非负整数。
-
连接两个字符串文本,并返回一个新的字符串。
-
判断一个字符串里是否包含其他字符串。
-
判断一个字符串的是否以给定字符串结尾,结果返回布尔值。
-
从字符串对象中返回首个被发现的给定值的索引值,如果没有找到则返回-1。
-
String.prototype.lastIndexOf()
从字符串对象中返回最后一个被发现的给定值的索引值,如果没有找到则返回-1。
-
String.prototype.localeCompare()
返回一个数字表示是否引用字符串在排序中位于比较字符串的前面,后面,或者二者相同。
-
使用正则表达式与字符串相比较。
-
返回调用字符串值的Unicode标准化形式。
-
在当前字符串尾部填充指定的字符串, 直到达到指定的长度。 返回一个新的字符串。
-
在当前字符串头部填充指定的字符串, 直到达到指定的长度。 返回一个新的字符串。
-
返回指定重复次数的由元素组成的字符串对象。
-
被用来在正则表达式和字符串直接比较,然后用新的子串来替换被匹配的子串。
-
对正则表达式和指定字符串进行匹配搜索,返回第一个出现的匹配项的下标。
-
摘取一个字符串区域,返回一个新的字符串。
-
通过分离字符串成字串,将字符串对象分割成字符串数组。
-
判断字符串的起始位置是否匹配其他字符串中的字符。
-
通过指定字符数返回在指定位置开始的字符串中的(可以指定长度的)字符。
-
返回在字符串中指定两个下标之间的字符。注意,两个参数如果为负数或者NaN,将被当做0,这点和数组的
slice()
不同。 -
String.prototype.toLocaleLowerCase()
根据当前区域设置,将符串中的字符转换成小写。对于大多数语言来说,
toLowerCase
的返回值是一致的。 -
String.prototype.toLocaleUpperCase()
根据当前区域设置,将字符串中的字符转换成大写,对于大多数语言来说,
toUpperCase
的返回值是一致的。 -
String.prototype.toLowerCase()
将字符串转换成小写并返回。
-
返回用字符串表示的特定对象。重写
Object.prototype.toString
方法。 -
String.prototype.toUpperCase()
将字符串转换成大写并返回。
-
从字符串的开始和结尾去除空格。参照部分 ECMAScript 5 标准。
-
从字符串的左侧去除空格。
-
从字符串的右侧去除空格。
-
返回特定对象的原始值。重写
Object.prototype.valueOf
方法。 -
String.prototype[@@iterator]()
返回一个新的迭代器对象,该对象遍历字符串值的索引位置,将每个索引值作为字符串值返回。
const iterator = str[Symbol.iterator]();
,返回的iterator就是一个迭代器,使用next()
来依次访问里面的元素。
模板字符串
模板字符串相当于加强版的字符串,用反引号 `,除了作为普通字符串,还可以用来定义多行字符串,还可以在字符串中加入变量和表达式。
现在有了 ES6 语法,字符串拼接可以这样写:
var name = 'smyhvae';
var age = '26';
console.log('name:'+name+',age:'+age); //传统写法
console.log(`name:${name},age:${age}`); //ES6 写法
注意,上方代码中,倒数第二行用的符号是单引号,最后一行用的符号是反引号(在tab键的上方)。
布尔值
注意不要在应该使用基本类型布尔值的地方使用 Boolean
对象。
// 字面量声明
let bool = true;
// 对象声明
var x = new Boolean(expression);
// 没有 new 操作符,Boolean能被用来执行类型转换,转换后是基本类型布尔值,不是对象
var x = Boolean(expression); // 推荐
var x = !!(expression); // 推荐,可能使用双重非运算符的一个场景,是显式地将任意值强制转换为其对应的布尔值。
注意:其值不是undefined
或null
的任何对象(包括其值为false
的布尔对象)在传递给条件语句时都将计算为true
。 例如,以下if
语句中的条件评估为true
:
var x = new Boolean(false);
if (x) {
// 这里的代码会被执行
}
创建值为 true
的 Boolean
对象
// 对于任何对象,即使是值为 false 的 Boolean 对象,当将其传给 Boolean 函数时,生成的 Boolean 对象的值都是 true。
var btrue = new Boolean(true);
var btrueString = new Boolean('true');
var bfalseString = new Boolean('false');
var bSuLin = new Boolean('Su Lin');
var bArrayProto = new Boolean([]);
var bObjProto = new Boolean({});
创建值为 false
的 Boolean
对象
var bNoParam = new Boolean();
var bZero = new Boolean(0);
var bNull = new Boolean(null);
var bEmptyString = new Boolean('');
var bfalse = new Boolean(false);
布尔值的方法
-
根据对象的值返回字符串
"true"
或"false"
。 重写Object.prototype.toString()
方法。 -
返回
Boolean
对象的原始值。 重写Object.prototype.valueOf()
方法。
数组
数组, set , map用[] ,对象用{}
数组构造函数的方法:
-
从类数组对象或者可迭代对象中创建一个新的数组实例。
-
用来判断某个变量是否是一个数组对象。
-
根据一组参数来创建新的数组实例,支持任意的参数数量和类型。
数组实例对象的修改器方法(即会改变数组本身的方法):
-
删除数组的最后一个元素,并返回这个元素。
-
在数组的末尾增加一个或多个元素,并返回数组的新长度。
-
删除数组的第一个元素,并返回这个元素。
-
在数组的开头增加一个或多个元素,并返回数组的新长度。
-
颠倒数组中元素的排列顺序,即原先的第一个变为最后一个,原先的最后一个变为第一个。
-
对数组元素进行排序,并返回当前数组。
注意:如果是对string类型排序直接使用;如果是对number类型从小到大排序需要写成
arr.sort((a, b) => a - b)
;该方法在不同浏览器中的实现方式不一样,v8中数组元素个数<=10就使用插入排序,否则使用快速排序。
-
在任意的位置给数组添加或删除任意个元素。
-
在数组内部,将一段元素序列拷贝到另一段元素序列上,覆盖原有的值。(实验性api)
-
将数组中指定区间的所有元素的值,都替换成某个固定的值。(实验性api)
实例对象的访问方法(即不会改变数组本身的方法):
-
返回一个由当前数组和其它若干个数组或者若干个非数组值组合而成的新数组。
-
判断当前数组是否包含某指定的值,如果是返回
true
,否则返回false
。(实验性api) -
连接所有数组元素组成一个字符串。
-
切片,即抽取当前数组中的一段元素组合成一个新数组。
-
返回数组中第一个与指定值相等的元素的索引,如果找不到这样的元素,则返回 -1。
-
返回数组中最后一个(从右边数第一个)与指定值相等的元素的索引,如果找不到这样的元素,则返回 -1。
-
返回一个由所有数组元素组合而成的字符串。遮蔽了原型链上的
Object.prototype.toString()
方法。 -
Array.prototype.toLocaleString()
返回一个由所有数组元素组合而成的本地化后的字符串。遮蔽了原型链上的
Object.prototype.toLocaleString()
方法。
实例对象的部分迭代方法:
-
为数组中的每个元素执行一次回调函数,无返回值,且无法跳出和终止循环(除抛出异常外)。
-
返回一个由回调函数的返回值组成的新数组。
-
如果数组中的每个元素都满足测试函数,则返回
true
,否则返回false。
-
如果数组中至少有一个元素满足测试函数,则返回 true,否则返回 false。
-
将所有在过滤函数中返回
true
的数组元素放进一个新数组中并返回。 -
从左到右为每个数组元素执行一次回调函数,并把上次回调函数的返回值放在一个暂存器中传给下次回调函数,并返回最后一次回调函数的返回值。最常用的应用是计算数组和。
-
找到第一个满足测试函数的元素并返回那个元素的值,如果找不到,则返回
undefined
。(实验性api) -
找到第一个满足测试函数的元素并返回那个元素的索引,如果找不到,则返回
-1
。(实验性api) -
返回一个数组迭代器对象,该迭代器会包含所有数组元素的键值对。
还有其他迭代方法,具体可参考 mdn-array。
函数
arguments对象
arguments
是一个对应于传递给函数的参数的类数组对象。
function func1(a, b, c) {
console.log(arguments[0]);
// expected output: 1
console.log(arguments[1]);
// expected output: 2
console.log(arguments[2]);
// expected output: 3
}
func1(1, 2, 3);
arguments对象的属性:
-
arguments.callee:指向参数所属的当前执行的函数。它可以用于引用该函数的函数体内当前正在执行的函数。这在函数的名称是未知时很有用,例如在没有名称的函数表达式 (也称为“匿名函数”)内。
-
arguments.length:传递给函数的参数数量。
-
arguments[@@iterator]:返回一个新的Array 迭代器 对象,该对象包含参数中每个索引的值。
普通函数
-
常见的函数
-
匿名函数:没有实际名字的函数
IIFE(立即调用函数表达式):如果需要执行匿名函数,在匿名函数后面加上一个括号即可立即执行,此时匿名函数也要用括号括起来。倘若需要传值,直接将参数写到括号内即可。注意:立即调用函数是由window调用的,所以它的this指向window。
// IIFE(function (){ //此时会输出 hello console.log("hello");})()
应用场景:
- 给dom元素的某个事件绑定一个匿名函数
- 将匿名函数赋值对象的某个属性
- 将匿名函数赋值给某个变量
- 将匿名函数作为回调函数中的参数
- 将匿名函数作为返回值
作用:
-
通过匿名函数可以实现闭包,若要创建一个闭包,往往都需要用到匿名函数。
-
模拟块级作用域,减少全局变量。执行完匿名函数,存储在内存中相对应的变量会被销毁,从而节省内存。再者,在大型多人开发的项目中,使用块级作用域,会大大降低命名冲突的问题,从而避免产生灾难性的后果。自此开发者再也不必担心搞乱全局作用域了。
-
箭头函数:(ES6)更简短的函数
- 箭头函数是匿名函数
- 箭头函数的this指向外层作用域的this的值,不能用call方法修改里面的this
- 箭头函数不能使用 arguments 对象,而用剩余参数…rest解决
- 箭头函数不能用作构造函数,即不能使用new
- 箭头函数没有原型,即没有prototype属性
// 当只有一个参数时,圆括号是可选的:(singleParam) => { statements }singleParam => { statements }// 没有参数的函数应该写成一对圆括号。() => { statements }// 箭头函数默认不会使用自己的this,而是会和外层的this保持一致,最外层的this就是window对象const obj = { a: () => { console.log(this) }}obj.a() //打出来的是window// 不能用call()修改thisconst obj = { a: () => { console.log(this) }}obj.a.call('123') //打出来的结果依然是window对象
- 箭头函数适合于无复杂逻辑或者无副作用的纯函数的场景。比如定时器
setTimeOut
,setInterval
; 数组对象的方法forEach
,filter
,findIndex
,map
,reduce
等。 - 尽量不要在最外层定义箭头函数。因为箭头函数的
this
指向的是外层作用域,这个作用域可能是全局作用域。函数里面如果需要用到this
的话,会污染全局作用域。这时候应该先使用普通函数,里面再根据情况是否使用箭头函数,如果使用了箭头函数又在里面使用了this
,this
指向的就是外面普通函数的作用域了。 - 使用箭头函数不要过于追求简短,需要保证代码的可读性
-
回调函数:一个函数作为参数传递给另一个函数(一般用于异步函数中,但回调函数和异步函数没有直接的关系)
同步回调函数的意义在于:你可以灵活的指定回调函数的内容,同步回调函数会在最后把你指定的函数执行了。
异步回调函数的意义在于, 你希望你的回调函数的内容是跟在异步代码后面的执行的,而不是早于异步代码执行(他们将在同一时序里)。
// 定义同步回调函数function synchronous_callback(s,callback){ alert("我将执行"+s); callback();}// 定义异步回调函数function asynchronous_callback(s,callback){ setTimeout(function(){ alert("我将在"+s+"毫秒后执行,执行完之后我后稍带上callback"); callback(); },s);}// 执行同步回调函数synchronous_callback("同步callback",function(){ alert("执行完了捎带着我奥,我在同步callback函数里");})// 执行异步回调函数asynchronous_callback(1000,function(){ alert("执行完了捎带着我奥,我在异步callback函数里");});
应用场景:
- 资源加载:动态加载js文件后执行回调,加载iframe后执行回调,ajax操作回调,图片加载完成执行回调,AJAX等等。
- DOM事件及Node.js事件基于回调机制(Node.js回调可能会出现多层回调嵌套的问题)。
- setTimeout的延迟时间为0,这个hack经常被用到,settimeout调用的函数其实就是一个callback的体现。
- 链式调用:链式调用的时候,在赋值器(setter)方法中(或者本身没有返回值的方法中)很容易实现链式调用,而取值器(getter)相对来说不好实现链式调用,因为你需要取值器返回你需要的数据而不是this指针,如果要实现链式方法,可以用回调函数来实现。
- setTimeout、setInterval的函数调用得到其返回值。由于两个函数都是异步的,即:他们的调用时序和程序的主流程是相对独立的,所以没有办法在主体里面等待它们的返回值,它们被打开的时候程序也不会停下来等待,否则也就失去了setTimeout及setInterval的意义了,所以用return已经没有意义,只能使用callback。callback的意义在于将timer执行的结果通知给代理函数进行及时处理。
构造函数
构造函数用于构造对象
与普通函数相比,构造函数有以下明显特点:
-
用new关键字调用。
-
不需要用return显式返回值的,默认会返回this,也就是新的实例对象。
-
建议函数名的首字母大写,与普通函数区分开。
function Foo(name, age) { this.name = name; this.age = age; //retrun this; //默认有这一行。new一个构造函数,返回一个对象}var fn1 = new Foo('smyhvae', 26);var fn2 = new Foo('vae',30); //new 多个实例对象
new操作符干了哪几件事?
- 创建一个新对象;
- 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象) ;
- 执行构造函数中的代码(为这个新对象添加属性) ;
- 新对象的
__proto__
指向 构造函数的prototype
。 - 返回这个新对象(构造函数不需要显式return)
参数默认值
ES6可以设置参数的默认值,如下:
如果调用时不传入参数,就使用’hello’默认值(设置默认值的参数要放在最后面):
function fn(param1, param2 = 'hello') { console.log(param1,param2);}
对象
创建对象的方式:
-
对象字面量
var obj = {};
-
内置的构造函数
var obj = new Object();
-
自定义的构造函数
function Person(){ this.name = "jack"; this.age = "30";}person.prototype.getAge = function() { return this.name;}// 属性应该定义在构造函数里面,这样每个实例对象都有自己的属性,彼此不会影响;// 方法应该定义在原型对象里面,这样每个实例对象都有相同的方法。var p1 = new Person()
删除对象属性:
你可以用 delete 操作符删除一个不是继承而来的属性。下面的例子说明如何删除一个属性:
//Creates a new object, myobj, with two properties, a and b.var myobj = new Object;myobj.a = 5;myobj.b = 12;//Removes the a property, leaving myobj with only the b property.delete myobj.a;
如果一个全局变量不是用 var
关键字声明的话,你也可以用 delete
删除它:
g = 17;delete g;
Object
构造函数的方法:
-
通过复制一个或多个对象来创建一个新的对象。
-
使用指定的原型对象和属性创建一个新对象。
-
给对象添加一个属性并指定该属性的配置。(通过给对象配置某个属性和该属性的get、set方法,可以监听对象的该属性)
-
给对象添加多个属性并分别指定它们的配置。
-
返回给定对象自身可枚举属性的
[key, value]
数组。 -
冻结对象:其他代码不能删除或更改任何属性。
-
Object.getOwnPropertyDescriptor()
返回对象指定的属性配置。
-
返回一个数组,它包含了指定对象所有的可枚举或不可枚举的属性名。
-
Object.getOwnPropertySymbols()
返回一个数组,它包含了指定对象自身所有的符号属性。
-
返回指定对象的原型对象。
-
比较两个值是否相同。所有 NaN 值都相等(这与和=不同)。
-
判断对象是否可扩展。
-
判断对象是否已经冻结。
-
判断对象是否已经密封。
-
返回一个包含所有给定对象自身可枚举属性名称的数组。
-
防止对象的任何扩展。
-
防止其他代码删除对象的属性。
-
设置对象的原型(即内部
[[Prototype]]
属性)。 -
返回给定对象自身可枚举值的数组。
Object
实例的方法:
-
Object.prototype.hasOwnProperty()
返回一个布尔值 ,表示某个对象是否含有指定的属性,而且此属性非原型链继承的。
-
Object.prototype.isPrototypeOf()
返回一个布尔值,表示指定的对象是否在本对象的原型链中。
-
Object.prototype.propertyIsEnumerable()
判断指定属性是否可枚举,内部属性设置参见 [ECMAScript [Enumerable]] attribute 。
-
Object.prototype.toLocaleString()
直接调用
toString()
方法。 -
返回对象的字符串表示。
-
返回指定对象的原始值。
类型转换
ToString
显式转换:
String(arg)
xx.toString()
隐式转换:
-
num + "str"
涉及到字符串与其他的类型的加法运算,一律转为字符串
转换规则,先对其他类型调用valueOf()返回值一个值,再对这个值调用toString()
转换规则:
-
null 转换成 “null”
-
undefined 转换成 “undefined”
-
true 转换为 “true” (false同)
-
Number的字符串化则遵循通用规则,但极小和极大的数字使用指数形式
// 1.07连续乘以7个1000var a = 1.07 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000;a.toString(); // "1.07e21"
-
普通对象,除非自行定义,否则toString()(Object.prototype.toString())返回内部属性[[Class]]的值,如"[object Object]"
var obj = { a: 1 };obj.toString(); // '[object Object]'
-
数组的默认toString()方法经过了重新定义,将所有单元字符串化以后再用", "连接起来
var a = [1, 2, 3];a.toString(); // "1,2,3"
-
Json字符串化,工具函数JSON.stringify(…)在将JSON对象序列化为字符串时也用到了ToString。
对大多数简单值来说,JSON字符串化和toString()的效果基本相同,只不过序列化的结果总是字符串:
JSON.stringify(42); // "42"JSON.stringify("42"); // ""42""JSON.stringify(null); // "null"JSON.stringify(true); // "true"
ToNumber
显式转换:
-
Number("str")
-
parseInt("str")
-
parseFloat("str")
隐式转换:
-
num - "str"
涉及到减法运算(其实乘除同样生效)并且有操作数不是数字的一律转为数字
转换规则,先对其他数据类型调用toString()返回一个字符串,再将这个字符串转为数字
-
+"str"
,在字符串前面加+
(一元操作符),可以将其转换为Number类型 -
+date
,在Date实例对象前面加+
(一元操作符),返回相应的时间戳。 -
比较操作(>, <, <=, >=)
-
按位操作(| & ^ ~)
-
算数操作(- + * / %), 注意,当 + 操作存在任意的操作数是 string 类型时,不会触发 number 类型的隐式转换
转换规则:
-
boolean:true转换为1, false转换为0。
-
undefined:转换为NaN
-
null:转换为0
-
string:ToNumber对字符串的处理基本遵循数字常量的相关规则/语法。处理失败时返回NaN(处理数字常量失败时会产生语法错误)。不同之处是ToNumber对以0开头的十六进制数并不按十六进制处理(而是按十进制,参见第2章)
复杂数据类型:
-
Object:会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型值,则再遵循以上规则将其强制转换为数字
if(X.valueOf){ X.valueOf()返回简单数据类型 ? 返回 : X.toString()返回简单数据类型 ? 返回 : 报错}else{ if(X.toString){ X.toString()返回简单数据类型 ? 返回 : 报错 }else{ 报错 }}// 为了将值转换为相应的基本类型值,抽象操作ToPrimitive(参见ES5规范9.1节)会首先(通过内部操作DefaultValue,参见ES5规范8.12.8节)检查该值是否有valueOf()方法。如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用toString()的返回值(如果存在)来进行强制类型转换。如果valueOf()和toString()均不返回基本类型值,会产生TypeError错误
从ES5开始,使用Object.create(null)创建的对象[[Prototype]]属性为null,并且没有valueOf()和toString()方法,因此无法进行强制类型转换。
ToBoolean
显式转换:
Boolean(arg)
!!arg
隐式转换:
- if (…)语句中的条件判断表达式。
- for ( … ; … ; … )语句中的条件判断表达式(第二个)。
- while (…)和do…while(…)循环中的条件判断表达式。
- ? :中的条件判断表达式。
- 逻辑运算符||(逻辑或)和&&(逻辑与)左边的操作数(作为条件判断表达式)。
- 对于||来说,如果条件判断结果为true就返回第一个操作数的值,如果为false就返回第二个操作数的值。
- &&则相反,如果条件判断结果为true就返回第二个操作数的值,如果为false就返回第一个操作数的值。
转换规则:
JavaScript中的值可以分为以下两类:
-
可以被强制类型转换为false的值
-
undefined
-
null
-
false
-
+0、-0和NaN
-
""
-
document.all
(以前是真正意义上的对象,现在是一个假值对象)
-
-
其他(被强制类型转换为true的值)
==(宽松相等)的类型转换
-
null和undefined,相等。
-
数字和字符串,转化为数字再比较。
-
如果有true或false,转换为1或0,再比较。
-
如果有引用类型,使用ToPrimitive算法。
toPrimitive(input,preferedType?)
- 如果输入的值已经是原始类型,直接返回这个值。
- 输入的值调用
toString()
方法,如果结果是原始类型,则返回。(转换类型是String会先执行这条,再执行下面这条) - 输入的值调用
valueOf()
方法(返回对象的初始值),如果结果是原始类型,则返回。(转换类型是Number会先执行这条,再执行上面这条) - 如果上面 3 个步骤之后,转换后的值仍然不是原始类型,则抛出 TypeError 错误。
实际上在操作符中,==,排序运算符,加减乘除,在对非原始值进行操作时,都会调用内部的toPrimitive()方法
-
其余都不相等。
闭包
原型/原型链(封装)
封装:JS用构造函数(Constructor)从原型对象生成实例对象。不同实例之间不共享(即每个实例对象之间相互独立)的属性和方法放在Constructor中,共享的属性和方法放在构造函数的原型对象(prototype)中。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vRw67KnB-1629475321529)(assets/image-20210318110120762.png)]
简单理解:原型链中只有两个东西:对象和构造函数。
原型链描述:每个对象都有一个私有属性__proto__
,指向它的构造函数的prototype
(原型对象,本质上也是对象)。prototype
自己也是对象,所以它也有一个私有属性__proto__
,指向指向它的构造函数的prototype
,层层向上查找直到一个对象的原型对象为 null
。
// 举例说明let num = new Number();// num是通过它的构造函数Number() new出来的一个对象,所以num.__proto__ === Number.prototype; Number.prototype本身也是一个对象,而对象都是构造函数Object() new出来的,所以Number.prototype.__proto__ === Object.prototype;而Object.prototype.__proto__ === null// 另外,构造函数的prototype有一个属性为constructor,它指向构造函数本身,即Number.prototype.constructor === Number
原型规则:
(js中只区分对象和函数类型,数组也归在对象类型里面)
- 所有的引用类型(数组、对象、函数),都具有对象特性,都可以自由扩展属性。null除外。
- 所有的引用类型(数组、对象、函数),都有一个
_proto_
属性,属性值是一个普通的对象。_proto_
的含义是隐式原型。 - 所有的引用类型(数组、对象、函数),
_proto_
属性指向它的构造函数的prototype
值。 - 所有的函数(不包括数组、对象),都有一个
prototype
属性,属性值是一个普通的对象。prototype
的含义是显式原型。(实例对象没有这个属性) - 当试图获取一个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的
_proto_
中寻找(即它的构造函数的prototype
)。
Function()
和 Object()
:
- 函数就是对象,代表函数的对象就是函数对象。所有的函数对象是被 Function 这个函数对象构造出来的。也就是说,Function 是最顶层的构造器。它构造了系统中所有的对象,包括用户自定义对象,系统内置对象,甚至包括它自已。(构造函数的构造函数是Function,普通函数的构造函数也是Function)
- Object 是最顶层的对象,所有的对象都将继承 Object 的原型,Object 也是一个函数对象,所以说 Object 是被 Function 构造出来的,但要注意:
Object.prototype.__proto__ === null
。
Number.constructor === Function;Object.constructor === Function;// Object本身是构造函数,所以有:Object.__proto__ === Function.prototype// Function.prototype本身也是对象(函数对象),所以有:Function.prototype.__proto__ === Object.prototype
typeof
, instanceof()
和 isPrototypeOf()
,in
和 hasOwnProperty()
:
-
typeof
:一元运算符,放在一个运算数之前,运算数可以是任意类型。它返回值是一个字符串,该字符串说明运算数的类型。typeof 可以用来检测给定变量的数据类型。typeof 的结果并不与js的数据类型一一对应,特例有:
typeof [1,2,3] === 'object';typeof null === 'object';// 最新的symbol类型也能检测typeof Symbol() === 'symbol'
-
instanceof
:二元运算符,用于判断对象是否某个构造函数的实例,会沿着原型链往上找。 -
isPrototypeOf()
方法:用于判断一个原型对象是否在另一个对象的原型链上,会沿着原型链往上找。 -
in
:二元运算符,用于判断一个属性是否在指定的对象里面,会沿着原型链往上找。 -
hasOwnProperty()
方法:用于判断一个对象是否拥有某个属性,不会沿着原型链往上找。function Dog() { this.name = 'wang';}Dog.prototype.age = 1let d = new Dog();console.log(d.__proto__ === Dog.prototype); // trueconsole.log(Dog.prototype.isPrototypeOf(d)); // trueconsole.log(Object.prototype.isPrototypeOf(d)); // trueconsole.log(d instanceof Dog); // trueconsole.log(d instanceof Object); // trueconsole.log('name' in d); // trueconsole.log('age' in d); // trueconsole.log(d.hasOwnProperty('name')); // trueconsole.log(d.hasOwnProperty('age')); // false
其他prototype模式的验证方式
-
hasOwnProperty()
每个实例对象都有一个
hasOwnProperty()
方法,用来判断某一个属性到底是本地属性,还是继承自prototype
对象的属性。alert(cat1.hasOwnProperty("name")); // truealert(cat1.hasOwnProperty("type")); // false
-
in
:in
运算符可以用来判断,某个实例是否含有某个属性,不管是不是本地属性。alert("name" in cat1); // truealert("type" in cat1); // true
in
运算符还可以用来遍历某个对象的所有属性。for(var prop in cat1) { alert("cat1["+prop+"]="+cat1[prop]); }
继承
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JY1RBcvX-1629475321532)(assets/image-20210312145410948.png)]
构造函数的继承
-
构造函数继承(构造函数绑定):使用 apply() 或 call() 方法将父对象的构造函数绑定在子对象上,即在子对象构造函数中加一行:
function Super() { this.colors = ["red", "green", "blue"];}function Sub() { // (有必要的话)在这里还可以给超类型构造函数传参 Super.call(this);}let instance1 = new Sub();instance1.colors.push("purple");console.log(instance1.colors); // "red,green,blue,purple"let instance2 = new Sub();console.log(instance2.colors); // "red,green,blue"
缺点:
-
在超类的原型对象中定义的方法,对子类型而言也是不可见的。
-
超类的方法都必须在在构造函数中定义,否则子类就继承不到,而这样就导致函数复用无法实现了。
-
-
原型链继承(prototype模式):使用prototype属性,让新实例对象的原型等于父类的实例对象
function Super() { this.property = true;}Super.prototype.getSuper = function() { return this.property;}function Sub() { this.subProperty = false;}// 用Super的一个实例来重写Sub的prototypeSub.prototype = new Super();// 上面代码会导致Sub.prototype.constructor指向Super,所以需要重新指向SubSub.prototype.constructor = Sub;Sub.prototype.getSub = function() { return this.subproperty;}let instance = new Sub();console.log(instance.getSuper()); // true;
缺点:
-
在超类型构造函数中定义的实例对象属性,会在子类型原型上变成原型对象属性被所有子类型实例对象所共享
-
在创建子类型的实例时,不能向超类型的构造函数中传递参数
-
-
组合继承:结合 原型链+借用构造函数 方式,实现组合继承。
function Super(name) { this.name = name; this.colors = ["red", "green", "blue"];}Super.prototype.getName = function() { console.log(this.name);}function Sub(name, age) { // 借用构造函数方式继承属性 Super.call(this, name); this.age = age;}// 原型链方式继承方法Sub.prototype = new Super();Sub.prototype.constructor = Sub;Sub.prototype.getAge = function() { console.log(this.age);}let instance1 = new Sub("ming", 22);instance1.colors.push("purple");console.log(instance1.colors); // "red, green, blue, purple"instance1.getName();instance1.getAge();let instance2 = new Sub("tom", 33);console.log(instance2.colors); // "red, green, blue"instance2.getName();instance2.getAge();
优点:
- 组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 javascript 中最常用的继承模式。
- 使用 instanceof 操作符和 isPrototype() 方法也能够用于识别基于组合继承创建的对象。
缺点:
- 无论在什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。
-
寄生继承:创建一个用于封装继承过程的函数,在函数内部增强对象,并返回对象。
function createPerson(original) { let clone = Object.create(original); // 通过 Object.create() 函数创建一个新对象 clone.sayGood = function() { // 增强这个对象 console.log("hello World!!!"); } return clone; // 返回这个对象 }
在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式
缺点:
- 做不到函数复用
-
寄生组合式继承:借用构造函数来继承属性,通过原型链混成形式来继承方法。不必在子类原型中调用超类的构造函数。使用寄生继承来继承超类的原型,再将结果指定给子类的原型。
function Super(name) { this.name = name; this.colors = ["red", "green", "blue"];}Super.prototype.getName = function(){ console.log(this.name);};function Sub(name, age) { Super.call(this, name); this.age = age;}// 创建超类型原型的一个副本let anotherPrototype = Object.create(Super.prototype);// 重设因重写原型而失去的默认的 constructor 属性anotherPrototype.constructor = Sub;// 将新创建的对象赋值给子类型的原型Sub.prototype = anotherPrototype;Sub.prototype.getAge = function() { console.log(this.age);};let instace1 = new Sub("ming", 22);instace1.colors.push("purple");console.log(instance1.colors); // "red, green, blue, purple"instance1.getName();instance1.getAge();let instace2 = new Sub("tom", 33);console.log(instance2.colors); // "red, green, blue"instance2.getName();instance2.getAge();
优点:
- 只调用一次 SuperType 构造函数,提高效率
- 避免了在 SubType.prototype 上面创建不必要,多余的属性。
- 原型链还能保持不变,因此还能够正常使用
instanceof
操作符和isPrototypeOf()
方法
非构造函数的继承
-
object()方法
json格式的发明人Douglas Crockford,提出了一个object()函数,可以做到这一点。
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
这个object()函数,其实只做一件事,就是把子对象的prototype属性,指向父对象,从而使得子对象与父对象连在一起。
使用的时候,第一步先在父对象的基础上,生成子对象:
var Doctor = object(Chinese);
然后,再加上子对象本身的属性:
Doctor.career = ‘医生’;
这时,子对象已经继承了父对象的属性了。
alert(Doctor.nation); //中国
-
浅拷贝
除了使用"prototype链"以外,还有另一种思路:把父对象的属性,全部拷贝给子对象,也能实现继承。
下面这个函数,就是在做拷贝:
function extendCopy§ {
var c = {};
for (var i in p) {
c[i] = p[i];
}c.uber = p;
return c;
}使用的时候,这样写:
var Doctor = extendCopy(Chinese);
Doctor.career = ‘医生’;
alert(Doctor.nation); // 中国
但是,这样的拷贝有一个问题。那就是,如果父对象的属性等于数组或另一个对象,那么实际上,子对象获得的只是一个内存地址,而不是真正拷贝,因此存在父对象被篡改的可能。
请看,现在给Chinese添加一个"出生地"属性,它的值是一个数组。
Chinese.birthPlaces = [‘北京’,‘上海’,‘香港’];
通过extendCopy()函数,Doctor继承了Chinese。
var Doctor = extendCopy(Chinese);
然后,我们为Doctor的"出生地"添加一个城市:
Doctor.birthPlaces.push(‘厦门’);
发生了什么事?Chinese的"出生地"也被改掉了!
alert(Doctor.birthPlaces); //北京, 上海, 香港, 厦门
alert(Chinese.birthPlaces); //北京, 上海, 香港, 厦门
所以,extendCopy()只是拷贝基本类型的数据,我们把这种拷贝叫做"浅拷贝"。这是早期jQuery实现继承的方式。
-
深拷贝
所谓"深拷贝",就是能够实现真正意义上的数组和对象的拷贝。它的实现并不难,只要递归调用"浅拷贝"就行了。
function deepCopy(p, c) {
var c = c || {};
for (var i in p) {
if (typeof p[i] === ‘object’) {
c[i] = (p[i].constructor === Array) ? [] : {};
deepCopy(p[i], c[i]);
} else {
c[i] = p[i];
}
}return c;
}使用的时候这样写:
var Doctor = deepCopy(Chinese);
现在,给父对象加一个属性,值为数组。然后,在子对象上修改这个属性:
Chinese.birthPlaces = [‘北京’,‘上海’,‘香港’];
Doctor.birthPlaces.push(‘厦门’);
这时,父对象就不会受到影响了。
alert(Doctor.birthPlaces); //北京, 上海, 香港, 厦门
alert(Chinese.birthPlaces); //北京, 上海, 香港
目前,jQuery库使用的就是这种继承方法。
事件循环
浏览器中的事件循环
以浏览器加载完JS文件执行JS代码这个过程来解释,浏览器加载完JS文件后如果是立即执行代码的话,会将这个执行JS代码的任务放入到消息队列中,消息队列中的一个个任务称为宏任务(一个宏任务就是一次循环,称为tick),浏览器对这个消息队列进行一个for循环,不断读取这个消息队列中的宏任务来执行,直到消息队列为空,开始执行这个宏任务,大部分代码以同步执行的,但是会出现部分代码需要异步执行,例如setTimeout,promise等任务,这些异步任务不会立即执行,而是等待某些条件满足时才可以运行,遇到setTimeoute这类计时类的任务时,在渲染进程中有一个计时器线程,时间的计算由该线程进行计算后将回调放入到消息队列中,而promise这类异步任务比较特殊,属于微任务,在执行中的每个宏任务中都关联着一个微任务队列,这个微任务队列用于存放当前宏任务执行过程中产生的微任务,因此promise这类微任务就存放到了微任务队列中,当这个宏任务里面的主函数执行结束之后,当前宏任务结束之前,会遍历微任务队列按序运行微任务,直到微任务队列为空,才结束当前宏任务的执行,从消息队列中读取下一个宏任务继续运行。
console.log('start')setTimeout(() => { console.log('setTimeout')}, 0)new Promise((resolve) => { console.log('promise') resolve()}) .then(() => { console.log('then1') }) .then(() => { console.log('then2') })console.log('end')// 理解:上面整段代码是一个宏任务,放进消息队列,遇到setTimeout(()=>{},0),计时后(0s后)把里面的回调函数作为另一个宏任务放入消息队列,遇到Promise(),把resolve()之后的then()作为微任务放入第一个宏任务的微任务队列中。宏任务结束前会把该宏任务对应的微任务队列里面的微任务执行完。所以输入顺序是:// start // promise// end// then1// then2// setTimeout
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-42o6I7Gj-1629475321538)(assets/image-20210304153659598.png)]
NodeJS中的事件循环
Node.js是运行在服务端的js,虽然用到也是V8引擎,但由于服务目的和环境不同,导致了它的API与原生JS有些区别,其Event Loop还要处理一些I/O,比如新的网络连接等,所以与浏览器Event Loop不太一样。
各大阶段
-
poll阶段
等待新的I/O事件,node在一些特殊情况下会阻塞在这里。
-
check阶段
check阶段专门用来执行setImmediate()方法的回调,当poll阶段进入空闲状态,并且setImmediate queue中有callback时,事件循环进入这个阶段。
-
close阶段
当一个socket连接或者一个handle被突然关闭时(例如调用了socket.destroy()方法),close事件会被发送到这个阶段执行回调。否则事件会用process.nextTick()方法发送出去。
-
timer阶段
计时器,这个阶段以先进先出的方式执行所有到期的timer加入timer队列里的callback,一个timer callback指得是一个通过setTimeout或者setInterval函数设置的回调函数。
-
I/O callback阶段
主要执行大部分I/O事件的回调,包括一些为操作系统执行的回调。例如一个TCP连接生错误时,系统需要执行回调来获得这个错误的报告。
执行顺序
当个v8引擎将js代码解析后传入libuv引擎后,循环首先进入poll阶段。poll阶段的执行逻辑如下: 先查看poll queue中是否有事件,有任务就按先进先出的顺序依次执行回调。 当queue为空时,会检查是否有setImmediate()的callback,如果有就进入check阶段执行这些callback。但同时也会检查是否有到期的timer,如果有,就把这些到期的timer的callback按照调用顺序放到timer queue中,之后循环会进入timer阶段执行queue中的 callback。 这两者的顺序是不固定的,受代码运行的环境的影响。如果两者的queue都是空的,那么loop会在poll阶段停留,直到有一个i/o事件返回,循环会进入i/o callback阶段并立即执行这个事件的callback。
流程图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YaeZ7Zih-1629475321539)(assets/image-20210304151720374.png)]
Nodejs中的非IO异步API提供了四种方法:
-
setTimeOut()
-
setInterval()
-
setImmediate()
setImmediate()方法从意义上将是立刻执行的意思,但是实际上它却是在一个固定的阶段才会执行回调,即poll阶段之后。有趣的是,这个名字的意义和process.nextTick()方法才是最匹配的。node的开发者们也清楚这两个方法的命名上存在一定的混淆,他们表示不会把这两个方法的名字调换过来—因为有大量的node程序使用着这两个方法,调换命名所带来的好处与它的影响相比不值一提。
setTimeout()和不设置时间间隔的setImmediate()表现上及其相似。猜猜下面这段代码的结果是什么?
setTimeout(() => { console.log('timeout');}, 0);setImmediate(() => { console.log('immediate');});
实际上,答案是不一定。没错,就连node的开发者都无法准确的判断这两者的顺序谁前谁后。这取决于这段代码的运行环境。运行环境中的各种复杂的情况会导致在同步队列里两个方法的顺序随机决定。
执行顺序为:
- timers: 执行setTimeout和setInterval的回调
- pending callbacks: 执行延迟到下一个循环迭代的 I/O 回调
- idle, prepare: 仅系统内部使用
- poll: 检索新的 I/O 事件;执行与 I/O 相关的回调。事实上除了其他几个阶段处理的事情,其他几乎所有的异步都在这个阶段处理。
- check: setImmediate在这里执行
- close callbacks: 一些关闭的回调函数,如:socket.on(‘close’, …)
但是,在一种情况下可以准确判断两个方法回调的执行顺序,那就是在一个I/O事件的回调中。下面这段代码的顺序永远是固定的:
const fs = require('fs');fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); });});/*答案永远是:immediatetimeout因为在I/O事件的回调中,setImmediate方法的回调永远在timer的回调前执行。*/
其执行顺序为:
- 外层是一个setTimeout,所以执行它的回调的时候已经在timers阶段了
- 处理里面的setTimeout,因为本次循环的timers正在执行,所以其回调其实加到了下个timers阶段
- 处理里面的setImmediate,将它的回调加入check阶段的队列
- 外层timers阶段执行完,进入pending callbacks,idle, prepare,poll,这几个队列都是空的,所以继续往下
- 到了check阶段,发现了setImmediate的回调,拿出来执行
- 然后是close callbacks,队列是空的,跳过
- 又是timers阶段,执行
console.log('setTimeout')
-
process.nextTick()
process.nextTick()
是一个微任务。process.nextTick()
是一个特殊的异步API,其不属于任何的Event Loop阶段。事实上Node在遇到这个API时,Event Loop根本就不会继续进行,会马上停下来执行process.nextTick(),这个执行完后才会继续Event Loop。所以,nextTick
和Promise
同时出现时,肯定是nextTick
先执行,原因是nextTick
的队列比Promise
队列优先级更高。尽管没有提及,但是实际上node中存在着一个特殊的队列,即
nextTick queue
。这个队列中的回调执行虽然没有被表示为一个阶段,当时这些事件却会在每一个阶段执行完毕准备进入下一个阶段时优先执行。当事件循环准备进入下一个阶段之前,会先检查nextTick queue
中是否有任务,如果有,那么会先清空这个队列。与执行poll queue
中的任务不同的是,这个操作在队列清空前是不会停止的。这也就意味着,错误的使用process.nextTick()
方法会导致nodejs进入一个死循环,直到内存泄漏。那么合适使用这个方法比较合适呢?下面有一个例子:
const server = net.createServer(() => {}).listen(8080);server.on('listening', () => {});// 这个例子中当,当listen方法被调用时,除非端口被占用,否则会立刻绑定在对应的端口上。这意味着此时这个端口可以立刻触发listening事件并执行其回调。然而,这时候on('listening)还没有将callback设置好,自然没有callback可以执行。为了避免出现这种情况,node会在listen事件中使用process.nextTick()方法,确保事件在回调函数绑定后被触发。
JS的代码运行过程
虽然js是单线程,但在js执行过程中并不是只有一个线程。其实有四个线程,包括:JS引擎线程,事件触发线程,定时器触发线程,HTTP异步请求线程,但永远只有 JS引擎线程在执行js脚本,其他三个只是协助,不参与脚本解析和执行。
js引擎执行分三个阶段:语法分析,预编译阶段,执行阶段。
(注意:浏览器首先按顺序加载由 <script>
标签分割的js代码块,加载js代码块完毕后,立刻进入三个阶段,然后再按顺序查找下一个代码块,再继续执行这三个阶段,无论是外部脚本文件(不异步加载)还是内部脚本代码块,都是一样的原理,并且都在同一个全局作用域中。)
语法分析
分析脚本代码块语法是否正确,正确则进入「预编译阶段」,否则抛出 SyntaxError
, 停止该代码块代码继续执行,然后继续查找下一个代码块。
预编译阶段(执行上下文的创建阶段)
执行栈(执行上下文栈),又称调用栈,用来存贮代码执行期间创建的所有执行上下文。是一个遵循后进先出(LIFO)的结构。JS代码首次运行,都会先创建一个全局执行上下文并压入到执行栈中,之后每当有函数被调用,都会创建一个新的函数执行上下文并压入栈内。所以栈底永远是全局执行上下文,栈顶是当前执行上下文。
预编译阶段其实就是创建执行上下文的过程,执行上下文分为三种,但第三种一般不考虑:
- 全局执行上下文: 默认的代码运行环境,一旦代码被载入执行,引擎最先创建的就是这个环境. 不写在函数内的代码, 被执行时处于全局执行上下文。全局执行上下文只有一个,在客户端中一般由浏览器创建,也就是我们熟知的window对象。
- 函数执行上下文: 写在函数内的代码运行时, 处于函数执行上下文.
- eval 执行上下文: 作为 eval 函数参数的代码, 运行时处于 eval 执行上下文. 这里略过不讲.
执行上下文的创建分为三个阶段,分别是创建变量对象,建立作用域链,以及确定this的指向。
创建变量对象
-
检索当前上下文中的参数。该过程生成 Arguments 对象,并建立以形参变量名为属性名,形参变量值为属性值的属性;
// arguments对象使用例子function add() { var sum =0, len = arguments.length; for(var i=0; i<len; i++){ sum += arguments[i]; } return sum;}add() // 0add(1) // 1add(1,2,3,4); // 10
-
检索当前上下文中的函数声明。该过程建立以函数名为属性名,函数所在内存地址引用为属性值的属性;(也就是函数提升)
注意:
-
函数的声明、初始化、赋值都会提升。
-
函数表达式,也即立即执行函数
var a = function b(){}
不会提升; -
块级的函数声明也是有提升的,但是只提升了声明和初始化(即在全局把这个函数初始化为undefined),后面执行了块级代码后,才执行赋值操作(因为块级代码有可能不执行,所以才把这几步分开),将块级内创建的函数对象赋给全局的,这样全局中也可以调用该函数了。
console.log(foo); // undefinedif(true) { function foo() { console.log('foo') }}foo(); // 'foo'
-
-
检索当前上下文中的变量声明。该过程建立以变量名为属性名,undefined 为属性值的属性(如果变量名跟已声明的形参变量名或函数名相同,则该变量声明不会干扰已经存在的这类属性,例如var重复声明,后面的var会直接忽略)。(也就是变量提升,注意:var的变量提升会声明并初始化为undefined,但let 和 const 的提升仅声明但不初始化,表现为let 和const声明的变量不能在声明前使用,具体原理见此处)
当执行上下文进入执行阶段后,变量对象会变为活动对象(Active Object)。此时原先声明的变量会被赋值。变量对象和活动对象都是指同一个对象,只是处于执行上下文的不同阶段。
创建作用域链
作用域链是指由当前上下文和上层上下文的一系列变量对象组成的层级链。它保证了当前执行环境对符合访问权限的变量和函数的有序访问。
-
每一个函数都会包含一个
[[scope]]
(作用域) 内部属性,在函数被声明的时候,该函数的[[scope]]
属性会保存其上层上下文的变量对象,形成包含上层上下文变量对象的层级链。[[scope]]
属性的值是在函数被声明的时候确定的。当函数被调用的时候,其执行上下文会被创建并入栈。 -
在创建阶段生成其变量对象后,会将该变量对象添加到作用域链的顶端并将
[[scope]]
添加进该作用域链中。而在执行阶段,变量对象会变为活动对象,其相应属性会被赋值。所以,作用域链是由当前上下文变量对象及上层上下文变量对象组成的。
确定this指向
this的指向在函数定义的时候是确定不了的,只有函数调用的时候(注意对象没有作用域,对象调用的时候不改变this)才能确定this到底指向谁(即每个函数内的this指向必定一样),实际上哪个对象调用函数,this就指向那个对象。没有调用时指window(window对象调用)。
-
函数赋值给变量时,this指向window
let a = 1;let obj = { a: 2; b: function() { console.log(this.a); }}let c = obj.b;c(); // 1// 因为此时只是把obj.b这个函数复制给c,调用c的时候this指向的还是window
-
以函数形式调用时,this永远都是window
let a = 2;let b = function() { console.log(this.a);}b(); // 2
-
以方法的形式调用时,this是调用方法的对象
let a = 1;let obj = { a: 2; b: function() { console.log(this.a); }}obj.b(); // 2
-
构造函数调用时,this指向实例对象
-
严格模式下函数是没有绑定到 this 上,这时候 this 是 undefined。
扩展:改变this的指向的方法:call()
apply()
bind()
:
call()
:第一个参数是this的指向对象,后面的参数是原函数的参数(可选),直接放进去apply()
:第一个参数是this的指向对象,后面的参数是原函数的参数(可选),必须放在一个数组中传进去bind()
:参数和call()
一样,但它返回的是一个函数,需要马上执行则在后面加()
,不兼容IE6~8。
var name = '小王', age = 17;var obj = { name: '小张', objAge: this.age; // this指向window 17 mfFun: function(fm, t) { console.log(this.name+' 年龄 '+this.age,'来自'+ fm + '去往' + t); // 对象方法里面的this指向obj }}var db = { name: '德玛', age: 99}// 三个函数的作用和区别obj.myFun.call(db,'成都','上海'); // 德玛 年龄 99 来自成都去往上海obj.myFun.apply(db,['成都','上海']); // 德玛 年龄 99 来自成都去往上海 obj.myFun.bind(db,'成都','上海')(); // 德玛 年龄 99 来自成都去往上海
执行阶段(执行上下文的执行阶段)
https://zhuanlan.zhihu.com/p/99269362看这个总结
js进入执行阶段(执行上下文的代码执行阶段)后,会开始完成变量赋值,函数引用,以及执行其他代码,这就和事件循环挂钩了。
js任务:
- 同步任务:在JS引擎主线程上按顺序执行的任务,只有前一个任务执行完毕后,才能执行后一个任务,形成一个执行栈(函数调用栈)
- 异步任务:不直接进入JS引擎主线程,而是满足触发条件时,相关的线程将该异步任务推进任务队列(
task queue
),等待JS引擎主线程上的任务执行完毕,空闲时读取执行的任务,例如异步Ajax
,DOM
事件,setTimeout
等。- 宏任务:宏任务执行完前,需要把属于当前宏任务的微任务执行完,才能执行下一个宏任务。
- 微任务:是在
es6
和node
环境中出现的一个任务类型。微任务的API
主要有:Promise
,process.nextTick
等。
事件循环:
在JS引擎主线程执行过程中:
- 首先执行同步任务,在主线程上形成一个执行栈,可理解为函数调用栈;
- 当执行栈中的函数调用到一些异步执行的
API
(例如异步Ajax
,DOM
事件,setTimeout
等API
),则会开启对应的线程(Http
异步请求线程,事件触发线程和定时器触发线程)进行监控和控制 - 当异步任务的事件满足触发条件时,对应的线程则会把该事件的处理函数推进任务队列(
task queue
)中,等待主线程读取执行 - 当JS引擎主线程上的任务执行完毕,则会读取任务队列中的事件,将任务队列中的事件任务推进主线程中,按任务队列顺序执行
- 当JS引擎主线程上的任务执行完毕后,则会再次读取任务队列中的事件任务,如此循环,这就是事件循环(
Event Loop
)的过程。
垃圾回收机制(执行上下文的回收阶段)
执行上下文执行后,出栈,等待垃圾回收机制回收。(闭包除外)
JavaScript 中内存管理的主要概念是”可达性“。简单地说,“可达性” 值就是那些以某种方式可访问或可用的值,它们被保证存储在内存中,并且有以下特点:
- 有一组基本的固有可达值,由于显而易见的原因无法删除。例如:
-
本地函数的局部变量和参数
-
当前嵌套调用链上的其他函数的变量和参数
-
全局变量
-
还有一些其他的,内部的
这些值称为根。
-
如果引用或引用链可以从根访问任何其他值,则认为该值是可访问的。
例如,如果局部变量中有对象,并且该对象具有引用另一个对象的属性,则该对象被视为可达性, 它引用的那些也是可以访问的。
JavaScript 引擎中有一个后台进程称为垃圾回收器,它监视所有对象,并删除那些不可访问(不再具有“可达性”)的对象。
js基本的垃圾回收算法称为**“标记-清除”**,定期执行以下“垃圾回收”步骤:
- 垃圾回收器获取根并**“标记”**(记住)它们。
- 然后它访问并“标记”所有来自它们的引用。
- 然后它访问标记的对象并标记它们的引用。所有被访问的对象都被记住,以便以后不再访问同一个对象两次。
- 以此类推,直到有未访问的引用(可以从根访问)为止。简单来说就是标记所有可访问的对象。
- 除标记的对象外,所有对象都被删除(清除)。
JavaScript引擎应用了许多优化,使“标记-清除”算法运行得更快,并且不影响执行:
- 分代回收——对象分为两组:“新对象”和“旧对象”。许多对象出现,完成它们的工作并迅速结 ,它们很快就会被清理干净。那些活得足够久的对象,会变“老”,并且很少接受检查。
- 增量回收——如果有很多对象,并且我们试图一次遍历并标记整个对象集,那么可能会花费一些时间,并在执行中会有一定的延迟。因此,引擎试图将垃圾回收分解为多个部分。然后,各个部分分别执行。这需要额外的标记来跟踪变化,这样有很多微小的延迟,而不是很大的延迟。
- 空闲时间收集——垃圾回收器只在 CPU 空闲时运行,以减少对执行的可能影响。
闭包和柯里化
闭包和柯里化都是JavaScript经常用到而且比较高级的技巧,所有的函数式编程语言都支持这两个概念。闭包事实上更是柯里化所不可缺少的基础。
闭包
闭包是指其(闭包)所在函数对应的执行上下文已经出栈,但仍访问了其所在执行上下文变量对象的函数。闭包就是能够读取其他函数内部变量的函数。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。
闭包的特点:
-
让外部访问函数内部变量成为可能;
-
局部变量会常驻在内存中;
-
可以避免使用全局变量,防止全局变量污染;
-
会造成内存泄漏(有一块内存空间被长期占用,而不被释放)
结论:闭包是函数里面的函数,闭包找到的是统一地址中父级函数中对应变量最终的值
// 实际例子function isFirstLoad(){ var list=[]; // 将函数暴露出来 return function(option){ if(list.indexOf(option)>=0){ //检测是否存在于现有数组中,有则说明已存在 (函数内部访问函数外部) console.log('已存在') }else{ list.push(option); console.log('首次传入'); //没有则返回true,并把这次的数据录入进去 } }}// ifl 就是上面函数里面的函数(闭包函数) (函数外部访问函数内部)var ifl=isFirstLoad();ifl("zhangsan"); ifl("lisi");ifl("zhangsan");
闭包的理解:
-
闭包能够让我们从一个函数内部访问到其外部函数(即父函数)的作用域中的变量,即使外部函数已经执行完毕;从外部函数的角度来看,相当于把一个函数的作用域链往下延长一段(实际上是引用了该函数的变量对象,导致其不被垃圾回收机制回收),可以让另外一个函数来访问该函数的内部变量。
-
要使用闭包,只需要简单地将一个函数定义在另一个函数内部,并将它暴露出来。要暴露一个函数,可以将它返回或者传给其他函数。
-
在 JavaScript 中,闭包是用来实现数据私有的原生机制(面向对象中通常用闭包来封装)
-
对象不是唯一的产生私有数据的方式。闭包还可以被用来创建有状态的函数,这些函数的执行过程可能由它们自身的内部状态所决定
闭包的应用:
-
模块化
(function(){ var a=1; var addOne=function(x){ return x+a; } window.addOne=addOne;})()console.log(addOne(2)); // 3
如上是通过函数自执行以及闭包实现模块化的一个例子,通过将函数添加到全局对象的方式将方法(这里是闭包)暴露出来,addOne 闭包访问了自执行函数中的变量 a 。
-
柯里化
柯里化
在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。
柯里化就是预先将函数的某些参数传入,得到一个简单的函数。但是预先传入的参数被保存在闭包中,因此会有一些奇特的特性。比如
// 普通的add函数function add(x, y) { return x + y}// Currying后function curryingAdd(x) { return function (y) { return x + y }}add(1, 2) // 3curryingAdd(1)(2) // 3
柯里化的好处:(参数复用、提前确认、延迟执行)
-
参数复用
// 正常正则验证字符串 reg.test(txt)// 函数封装后function check(reg, txt) { return reg.test(txt)}check(/\d+/g, 'test') //falsecheck(/[a-z]+/g, 'test') //true// Currying后function curryingCheck(reg) { return function(txt) { return reg.test(txt) }}var hasNumber = curryingCheck(/\d+/g)var hasLetter = curryingCheck(/[a-z]+/g)hasNumber('test1') // truehasNumber('testtest') // falsehasLetter('21212') // false
上面的示例是一个正则的校验,正常来说直接调用check函数就可以了,但是如果我有很多地方都要校验是否有数字,其实就是需要将第一个参数reg进行复用,这样别的地方就能够直接调用hasNumber,hasLetter等函数,让参数能够复用,调用起来也更方便。
-
提前确认
var on = function(element, event, handler) { if (document.addEventListener) { if (element && event && handler) { element.addEventListener(event, handler, false); } } else { if (element && event && handler) { element.attachEvent('on' + event, handler); } }}// 柯里化后var on = (function() { if (document.addEventListener) { return function(element, event, handler) { if (element && event && handler) { element.addEventListener(event, handler, false); } }; } else { return function(element, event, handler) { if (element && event && handler) { element.attachEvent('on' + event, handler); } }; }})();//换一种写法可能比较好理解一点,上面就是把isSupport这个参数给先确定下来了var on = function(isSupport, element, event, handler) { isSupport = isSupport || document.addEventListener; if (isSupport) { return element.addEventListener(event, handler, false); } else { return element.attachEvent('on' + event, handler); }}
我们在做项目的过程中,封装一些dom操作可以说再常见不过,上面第一种写法也是比较常见,但是我们看看第二种写法,它相对一第一种写法就是自执行然后返回一个新的函数,这样其实就是提前确定了会走哪一个方法,避免每次都进行判断。
-
延迟执行(重要)
Function.prototype.bind = function (context) { var _this = this var args = Array.prototype.slice.call(arguments, 1) return function() { return _this.apply(context, args) }}
柯里化通用封装方法:
// 支持多参数传递function progressCurrying(fn, args) { var _this = this var len = fn.length; var args = args || []; return function() { var _args = Array.prototype.slice.call(arguments); Array.prototype.push.apply(args, _args); // 如果参数个数小于最初的fn.length,则递归调用,继续收集参数 if (_args.length < len) { return progressCurrying.call(_this, fn, _args); } // 参数收集完毕,则执行fn return fn.apply(this, _args); }}
JS 正则表达式
正则表达式知识点
正则表达式是一个对象,用来描述字符串的模式
格式:/正则表达式主体/修饰符(可选)
var patt = /runoob/i
修饰符 可以在全局搜索中不区分大小写:
修饰符 | 描述 |
---|---|
i | 执行对大小写不敏感的匹配。 |
g | 执行全局匹配(查找所有匹配而非在找到第一个匹配后停止)。 |
m | 执行多行匹配(当字符串分成多行时可以使用)。 |
表达式主体:
-
表达式主体可以拆分成一个或多个取值范围+量词的组合,针对每个组合根据JS正则表达式翻译一遍,然后拼接起来就好了。
-
表达式主体用
^
和$
指定起止位置(根据需要使用,使用的情况下可以确定完全匹配)。
方括号[]
用于查找某个**取值范围(字符集)**内的字符,圆括号()
用来表示一个整体,
表达式 | 描述 |
---|---|
[abc] | 查找方括号之间的任何字符。 |
[0-9] | 查找任何从 0 至 9 的数字。 |
(x|y) | 查找任何以 | 分隔的选项。 |
花括号 {}
用来表示量词。
量词 | 描述 |
---|---|
n+ | 匹配任何包含至少一个 n 的字符串。 |
n* | 匹配任何包含零个或多个 n 的字符串。 |
n? | 匹配任何包含零个或一个 n 的字符串。 |
n{X} | 匹配包含X个n的序列的字符串 |
n{X,Y} | 匹配包含X至Y个n的序列的字符串 |
n{X,} | 匹配至少包含 X 个 n 的序列的字符串。 |
元字符是拥有特殊含义的字符:
元字符 | 描述 |
---|---|
. | 句号匹配任意单个字符除了换行符。 |
[ ] | 字符种类。匹配方括号内的任意字符。 |
[^ ] | 否定的字符种类。匹配除了方括号里的任意字符 |
* | 匹配>=0个重复的在*号之前的字符。 |
+ | 匹配>=1个重复的+号前的字符。 |
? | 标记?之前的字符为可选. |
{n,m} | 匹配num个大括号之前的字符或字符集 (n <= num <= m). |
(xyz) | 字符集,匹配与 xyz 完全相等的字符串. |
| | 或运算符,匹配符号前或后的字符. |
\ | 转义字符,用于匹配一些保留的字符 [ ] ( ) { } . * + ? ^ $ \ | |
^ | 从开始行开始匹配. |
$ | 从末端开始匹配. |
常用的简写字符集:
简写 | 描述 |
---|---|
. | 除换行符外的所有字符 |
\w | 匹配所有字母数字,等同于 [a-zA-Z0-9_] |
\W | 匹配所有非字母数字,即符号,等同于: [^\w] |
\d | 匹配数字: [0-9] |
\D | 匹配非数字: [^\d] |
\s | 匹配所有空格字符,等同于: [\t\n\f\r\p{Z}] |
\S | 匹配所有非空格字符: [^\s] |
\f | 匹配一个换页符 |
\n | 匹配一个换行符 |
\r | 匹配一个回车符 |
\t | 匹配一个制表符 |
\v | 匹配一个垂直制表符 |
\p | 匹配 CR/LF(等同于 \r\n ),用来匹配 DOS 行终止符 |
零宽度断言(前后预查),在匹配过程中不占用字符,所以叫“零宽度”,注意定义一个断言要使用 ()
括起来:
符号 | 描述 |
---|---|
(?=pattern) | 正先行断言-存在,紧接该位置之后的字符序列能够匹配pattern |
(?!pattren) | 负先行断言-排除,紧接该位置之后的字符序列不能够匹配pattern |
(?<=pattern) | 正后发断言-存在,紧接该位置之前的字符序列能够匹配pattern |
(?<!pattern) | 负后发断言-排除,紧接该位置之前的字符序列不能够匹配pattern |
注意:正则表达式默认采用贪婪匹配模式,在该模式下意味着会匹配尽可能长的子串。我们可以使用 ?
将贪婪匹配模式转化为惰性匹配模式。
// 匹配到 The fat cat sat on the mat"/(.*at)/" => The fat cat sat on the mat. // 匹配到 The fat"/(.*?at)/" => The fat cat sat on the mat.
JS 正则表达式相关
1.定义JS正则表达式:
// 第一种,使用字面量定义,注意不需要加引号var reg1 = /hello/i; // 第二种,使用RegExp对象定义var reg2 = new RegExp('hello','i')
2.RegExp对象中的方法:
-
test() 方法
可以用于检测一个字符串是否匹配某个模式,返回true或false
const reg = /test/g; //也可以用new方法来声明正则表达式const str = '_test_test';console.log(reg.test(str));
-
exec() 方法
在一个指定字符串中执行一个搜索匹配。返回一个结果数组或
null
。/e/.exec("The best things in life are free!");// ["e", index: 2, input: "The best things in life are free!", groups: undefined]
-
compile() 方法
用于改变RegExp。既可以改变表达式主体,也可以添加或删除第二个参数。
其实直接重新赋值也可以,但是调用compile方法可以有效的提高代码的执行速度,如果该正则表达式只能被使用一次,则不会有明显的效果
var reg = /hello/;console.log(reg.exec('hellojs')); //['hello']reg.compile('Hello');console.log(reg.exec('hellojs')); // nullreg.compile('Hello/i');console.log(reg.exec('hellojs')); //['hello']
String对象的如下方法,是支持正则表达式的:
方法 | 描述 | 备注 |
---|---|---|
split() | 将字符串拆分成数组 | |
search() | 搜索字符串中是否含有指定内容,返回索引 index。 | |
match() | 根据正则表达式,从一个字符串中将符合条件的内容提取出来,返回一个数组 | |
replace() | 将字符串中的指定内容,替换为新的内容。原字符串不会改变。 |
用途实例:
-
检查一个字符串是否是一个合法手机号
var phoneStr = "13067890123";var phoneReg = /^1[3-9][0-9]{9}$/;console.log(phoneReg.test(phoneStr));
-
去掉字符串开头和结尾的空格
str = str.replace(/^\s*|\s*$/g,"");
-
判断字符串是否为电子邮件
var emailReg = /^\w{3,}(\.\w+)*@[A-z0-9]+(\.[A-z]{2,5}){1,2}$/;var email = "abchello@163.com";console.log(emailReg.test(email));
ES6
ES6详解可参考 阮一峰-es6入门
ECMAScript 是 JS 的语言标准。而 ES6 是新的 JS 语法标准。
很多人在做业务选型的时候,会倾向于选jQuery。其实jQuery的语法是偏向于ES3的。而现在主流的框架 Vue.js 和React.js的语法,是用的ES6。
ES6中增加了很多功能上的不足。比如:常量、作用域、对象代理、异步处理、类、继承等。这些在ES5中想实现,比较复杂,但是ES6对它们进行了封装。
但是浏览器对es6的语法支持不是很好,运行在浏览器的时候需要把es6转成es5,NodeJS平台是可以支持绝大部分的ES6的新内容。
ES的几个重要版本
- ES5 : 09年发布。
- ES6(ES2015) : 2015年发布,也称为ECMA2015。
- ES7(ES2016) : 2016年发布,也称为ECMA2016 (变化不大)。
ES5的严格模式
理解:除了正常运行模式(混杂模式),ES5添加了第二种运行模式:“严格模式”(strict mode)。
顾名思义,这种模式使得Javascript在更严格的语法条件下运行。
目的:
- 消除Javascript语法的一些不合理、不严谨之处,减少一些怪异行为
- 消除代码运行的一些不安全之处,为代码的安全运行保驾护航
- 为未来新版本的Javascript做好铺垫
实际上,ES5严格模式里面的很多东西在ES6后面的版本都会逐步实现,可以近似理解成ES6之后默认采用严格模式。
ES5的严格模式主要有以下限制:
- 变量必须声明后再使用;
- 函数的参数不能有同名属性,否则报错;
- 不能使用with语句;
- 不能对只读属性赋值,否则报错;
- 不能使用前缀0表示八进制数,否则报错;
- 不能删除不可删除的属性,否则报错;
- 不能删除变量delete prop,会报错,只能删除属性delete global[prop];
- eval不会在它的外层作用域引入变量;
- eval和arguments不能被重新赋值;
- arguments不会自动反映函数参数的变化;
- 不能使用arguments.callee;
- 不能使用arguments.caller;
- 禁止this指向全局对象;
- 不能使用fn.caller和fn.arguments获取函数调用的堆栈;
- 增加了保留字(比如protected、static和interface)。
变量声明
ES6 中新增了 let 和 const 来定义变量:
-
var
:ES5 和 ES6中,定义全局变量(是varia ble的简写)。 -
let
:定义局部变量,替代 var。(块级作用域) -
const
:定义常量(定义后,不可修改)。用 const 声明的变量,只在局部(块级作用域内)起作用。
总结:我们要习惯用 let 声明,减少var声明带来的污染全局空间,不再建议使用var。
为了进一步说明 let 不会带来污染,需要说明的是:当我们定义了let a = 1
时,如果我们在同一个作用域内继续定义let a = 2
,是会报错的,但var可以重复声明。
类和继承
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6UZeW3Ye-1629475321542)(assets/image-20210312145348290.png)]
迭代器 和 for…of 循环
具有[Symbol.iterator]
属性的对象,即为可迭代对象。执行[Symbol.iterator]()
函数返回的对象是一个迭代器对象。
原生的可迭代对象包括 Array
,Map
,Set
,String
,TypedArray
,arguments对象,NodeList对象(不包括Object)。
// 可迭代对象interface Iterable { [Symbol.iterator]() : Iterator,}// 迭代器对象interface Iterator { next(value?: any) : IterationResult,}// 每一次迭代的结果interface IterationResult { value: any, // 当前数据结构成员的值 done: boolean, // 表示遍历是否结束}
获取可迭代对象的迭代器的方法:
let arr = ['a', 'b', 'c'];let iter = arr[Symbol.iterator]();iter.next() // { value: 'a', done: false }iter.next() // { value: 'b', done: false }iter.next() // { value: 'c', done: false }iter.next() // { value: undefined, done: true }
可迭代对象的应用场景:
- 解构赋值
- 扩展运算符
yield*
- 任何以数组为参数的遍历的场景:
for...of
Array.from()
Map()/Set()/WeakMap()/WeakSet()
Promise.all()/Promise.race()
for…of 循环的运行原理:
- 首先调用可迭代对象的
[Symobo.iterator]()
方法,拿到迭代器对象; - 每次循环,调用迭代器对象
next()
方法,得到{value: ..., done: ... }
对象;
需要注意的是:**对象(Object)没有默认 Iterator 接口,因为对象属性遍历顺序不确定,需开发者手动指定。**所以我们不能用for…of来遍历对象,遍历对象有专用的for…in循环。
for…in 遍历得到对象的key,它的遍历顺序是:先遍历排序属性(对象里面是可以转换成正整数数字的字符串,升序排序),后遍历常规属性(其它字符串,按设置顺序排序,原型链上的属性会排在后面)
生成器
generator function 返回一个生成器对象,并且它符合可迭代协议和迭代器协议,所以它实际上可以理解成一个迭代器。最初调用时,生成器函数不执行任何代码,而是返回一种称为Generator的迭代器。 通过调用生成器的下一个方法(即 next()
)消耗值时,Generator函数将执行,直到遇到yield关键字。
可以根据需要多次调用该函数,并且每次都返回一个新的Generator,但每个Generator只能迭代一次。
// 使用生成器对象生成迭代器(改写上面迭代器的例子)function* makeRangeIterator(start = 0, end = Infinity, step = 1) { for (let i = start; i < end; i += step) { yield i; }}var a = makeRangeIterator(1,10,2)a.next() // {value: 1, done: false} // 每个迭代器对象都有value和done两个值a.next() // {value: 3, done: false}a.next() // {value: 5, done: false}a.next() // {value: 7, done: false}a.next() // {value: 9, done: false}a.next() // {value: undefined, done: true}
生成器对象的方法:
-
返回一个由
yield
表达式生成的值。 -
返回给定的值并结束生成器。
-
向生成器抛出一个错误。
Set
都是内置可迭代对象,因为它们的原型对象都拥有一个 Symbol.iterator
方法。
运算符
扩展运算符和剩余(rest)运算符
扩展运算符和rest运算符是逆运算:扩展运算符:数组=>分割序列;rest运算符:分割序列=>数组
-
使用了扩展运算符的参数,不能再加别的参数
-
rest运算符的参数是表示“剩余的参数”,必须在最后面一位定义。
扩展运算符
-
用于数组的解构
// ES6 扩展运算符 写法var array = [1,2,3,4,3];var max2 = Math.max(...array); console.log(max2);//4
-
代替数组的
push
,concat
等方法// ES6 扩展运算符 写法var arr1 = [0, 1, 2];var arr2 = [3, 4, 5];arr1.push(...arr2);//arr1 [0, 1, 2, 3, 4, 5]var arr1 = ['a', 'b'];var arr2 = ['c'];var arr3 = ['d', 'e'];// ES5的合并数组arr1.concat(arr2, arr3) // [ 'a', 'b', 'c', 'd', 'e' ]// ES6的合并数组[...arr1, ...arr2, ...arr3] // [ 'a', 'b', 'c', 'd', 'e' ]
-
拷贝数组或对象
//拷贝数组var array0 = [1,2,3];var array1 = [...array0];console.log(array1);//[1, 2, 3]//拷贝对象var obj = { age:1, name:"lis", arr:{ a1:[1,2] }}var obj2 = {...obj};console.log(obj2);//{age: 1, name: "lis", arr: {…}}
-
将伪数组转化为数组
//伪数组转换为数组var nodeList = document.querySelectorAll('div');console.log([...nodeList]); // [div, div, div ... ]
上面代码中,querySelectorAll 方法返回的是一个 nodeList 对象。它不是数组,而是一个类似数组的对象。这时,扩展运算符可以将其转为真正的数组,原因就在于 NodeList 对象实现了 Iterator。
注意:使用扩展运算符将伪数组转换为数组有局限性,这个类数组必须得有默认的迭代器且伪可遍历的。(建议使用
Array.from
方法)
剩余运算符
-
rest参数代替arguments变量
// arguments变量的写法function sortNumbers() { return Array.prototype.slice.call(arguments).sort();}// rest参数的写法const sortNumbers = (...numbers) => numbers.sort();
-
与解构赋值组合使用
var array = [1,2,3,4,5,6];var [a,b,...c] = array;console.log(a);//1console.log(b);//2console.log(c);//[3, 4, 5, 6]
变量的解构赋值
ES6允许我们,通过数组或者对象的方式,对一组变量进行赋值,这被称为解构。
解构赋值在实际开发中可以大量减少我们的代码量,并且让程序结构更清晰。
数组的解构赋值
举例:
通常情况下,我们在为一组变量赋值时,一般是这样写:
let a = 0;let b = 1;let c = 2;
现在我们可以通过数组解构的方式进行赋值:
let [a, b, c] = [1, 2, 3];
二者的效果是一样的。
- undefined:相当于什么都没有。
- null:相当于有值,但值为 null。
字符串的解构赋值
字符串也可以解构,这是因为,此时字符串被转换成了一个类似数组的对象。举例如下:
const [a, b, c, d] = 'smyhvae';console.log(a); //sconsole.log(b); //mconsole.log(c); //yconsole.log(d); //hconsole.log(typeof a); //输出结果:string
对象的解构赋值
通常情况下,我们从接口拿到json数据后,一般这么赋值:
var a = json.a;var b = json.b;var c = json.c;
上面这样写,过于麻烦了。
现在,我们同样可以针对对象,进行结构赋值。
举例如下:
let { foo, bar } = { bar: '我是 bar 的值', foo: '我是 foo 的值' };console.log(foo + ',' + bar); //输出结果:我是键 foo 的值,我是键 bar 的值
上方代码可以看出,对象的解构与数组的结构,有一个重要的区别:数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,是根据键来取值的。
圆括号的作用:
如果变量 foo 在解构之前就已经定义了,此时你再去解构,就会出现问题,编译会报错。要解决报错,只要在整个解构的语句外边,加一个圆括号即可:
let foo = "ee";let bar = "ww"; ({ foo, bar } = { bar: '我是 bar 的值', foo: '我是 foo 的值' });console.log(foo + ',' + bar); //输出结果:我是键 foo 的值,我是键 bar 的值
Symbol 类型
背景:ES5中对象的属性名都是字符串,容易造成重名,污染环境。
概念:ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。
特点:
- Symbol属性对应的值是唯一的,解决命名冲突问题
- Symbol值不能与其他数据进行计算,包括同字符串拼串
- for in、for of 遍历时不会遍历Symbol属性。
创建Symbol属性值:
Symbol是函数,但并不是构造函数。创建一个Symbol数据类型:
let mySymbol1 = Symbol();let mySymbol2 = Symbol('foo'); // 也可以输入描述用的字符串console.log(typeof mySymbol1); //打印结果:symbolconsole.log(mySymbol1); //打印结果:Symbol()
BigInt 类型
BigInt
是一种内置对象,它提供了一种方法来表示大于 253 - 1
的整数。这原本是 Javascript中可以用 Number
表示的最大数字。BigInt
可以表示任意大的整数。
可以用在一个整数字面量后面加 n
的方式定义一个 BigInt
,如:10n
,或者调用函数BigInt()
。
const theBiggestInt = 9007199254740991n;const alsoHuge = BigInt(9007199254740991);// ↪ 9007199254740991nconst hugeString = BigInt("9007199254740991");// ↪ 9007199254740991nconst hugeHex = BigInt("0x1fffffffffffff");// ↪ 9007199254740991nconst hugeBin = BigInt("0b11111111111111111111111111111111111111111111111111111");// ↪ 9007199254740991n
JavaScript 标准内置对象
具体可参考:js标准内置对象-mdn
JSON
JSON的表现形式类似于js对象(当然也可以将其转换为json字符串),但其不同于js对象,其属性名称必须是双括号括起来的字符串,且最后一个属性后不能有逗号。
我们平时看到的json经常是被压缩,甚至被转义的,这时候可以通过一些工具或网站来将其转换。
注意:json在js里面就是以字符串的形式表示的,通常是类似'{"x":5,"y":6}'
的形式。
-
解析JSON字符串并返回对应的原生javascript的值(包括js对象、数组、字符串等),可以额外传入一个转换函数,用来将生成的值和其属性, 在返回之前进行某些修改。
-
返回与指定值对应的JSON字符串,可以通过额外的参数, 控制仅包含某些属性, 或者以自定义方法来替换某些key对应的属性值。
Map
Map 对象保存键值对。任何值(对象或者原始值) 都可以作为一个键或一个值。
Map 和 Object的区别:
- 一个 Object 的键只能是字符串或者 Symbols,但一个 Map 的键可以是任意值(包括Number)。
- Map 中的键值是有序的(FIFO 原则),而添加到对象中的键则不是。
- Map 的键值对个数可以从 size 属性获取,而 Object 的键值对个数只能手动计算。
- Object 都有自己的原型,原型链上的键名有可能和你自己在对象上的设置的键名产生冲突。
- Object可以进行对象解构,Map不可以。
使用对象字面量(object)的一般场景有:
- 不需要去遍历对象字面量(object)的所有属性的时候
- 我们知道了里面有多少个属性和对象的属性是什么的时候
- 需要去
JSON.stringify
和JSON.parse
时候(map不可以)
其他的情况用map,其他的情况包括:
- key不是字符串或者symbol的时候
- 需要去遍历的时候
- 要得到长度的时候
- 遍历的时候对顺序有要求的(对象字面量(object)可能不是按照你写的顺序)
Map的实例属性:
-
返回Map对象的键/值对的数量。
Map的实例方法:(除增查改以外,其他方法在Set对象中也适用)
-
移除Map对象的所有键/值对 。
-
如果
Map
对象中存在该元素,则移除它并返回true
;否则如果该元素不存在则返回*false*
。随后调用Map.prototype.has(key)
将返回false
。 -
返回一个新的
Iterator
对象,它按插入顺序包含了Map对象中每个元素的[key, value]
**数组**
。 -
Map.prototype.forEach(callbackFn[, thisArg])
按插入顺序,为
Map
对象里的每一键值对调用一次callbackFn函数。如果为forEach提供了thisArg,它将在每次回调中作为this值。 -
返回键对应的值,如果不存在,则返回undefined。
-
返回一个布尔值,表示Map实例是否包含键对应的值。
-
返回一个新的
Iterator
对象, 它按插入顺序包含了Map对象中每个元素的键 。 -
设置Map对象中键的值。返回该Map对象。
-
返回一个新的
Iterator
对象,它按插入顺序包含了Map对象中每个元素的值 。 -
返回一个新的
Iterator
对象,它按插入顺序包含了Map对象中每个元素的[key, value]
**数组**
。
WeakMap
WeakMap
对象是一组键/值对的集合,其中的键是弱引用的。
WeakMap
与Map
的区别有两点:
WeakMap
只接受对象作为键名(null
除外),不接受其他类型的值作为键名。WeakMap
的键名所指向的对象,可以被垃圾回收机制回收。只要其他地方没有对这个对象的引用,这个对象就会被回收,这也是两者最大的区别。
WeakMap
的专用场合就是,它的键所对应的对象,可能会在将来消失。WeakMap
结构有助于防止内存泄漏。因此,WeakMap
的key是不可枚举的(没有办法能给出所有的key)。
const wm1 = new WeakMap(), wm2 = new WeakMap(), wm3 = new WeakMap();const o1 = {}, o2 = function(){}, o3 = window;wm1.set(o1, 37);wm1.set(o2, "azerty");wm2.set(o1, o2); // value可以是任意值,包括一个对象或一个函数wm2.set(o3, undefined);wm2.set(wm1, wm2); // 键和值可以是任意对象,甚至另外一个WeakMap对象wm1.get(o2); // "azerty"wm2.get(o2); // undefined,wm2中没有o2这个键wm2.get(o3); // undefined,值就是undefinedwm1.has(o2); // truewm2.has(o2); // falsewm2.has(o3); // true (即使值是undefined)wm3.set(o1, 37);wm3.get(o1); // 37wm1.has(o1); // truewm1.delete(o1);wm1.has(o1); // false
WeakMap的实例方法:
-
移除key的关联对象。执行后
WeakMap.prototype.has(key)返回false。
-
返回
key关联对象
, 或者undefined
(没有key关联对象时)。 -
根据是否有key关联对象返回一个Boolean值。
-
WeakMap.prototype.set(key, value)
在WeakMap中设置一组key关联对象,返回这个
WeakMap
对象。
Set
Set 对象允许你存储任何类型的唯一值,无论是原始值或者是对象引用。
Set
本身是一个构造函数,用来生成 Set 数据结构。
let mySet = new Set(); mySet.add(1); // Set(1) {1}mySet.add(5); // Set(2) {1, 5}mySet.add(5); // Set(2) {1, 5} 这里体现了值的唯一性mySet.add("some text"); // Set(3) {1, 5, "some text"} 这里体现了类型的多样性var o = {a: 1, b: 2}; mySet.add(o);mySet.add({a: 1, b: 2}); // Set(5) {1, 5, "some text", {…}, {…}} // 这里体现了对象之间引用不同不恒等,即使值相同,Set 也能存储
Set的实例属性:
-
返回 Set 对象中的值的个数
Set的实例方法:
-
在
Set
对象尾部添加一个元素。返回该Set
对象。 -
移除
Set
对象内的所有元素。 -
移除
Set
中与这个值相等的元素,返回Set.prototype.has(value)
在这个操作前会返回的值(即如果该元素存在,返回true
,否则返回false
)。Set.prototype.has(value)
在此后会返回false
。 -
返回一个新的迭代器对象,该对象包含
Set
对象中的按插入顺序排列的所有元素的值的[value, value]
数组。为了使这个方法和Map
对象保持相似, 每个值的键和值相等。 -
Set.prototype.forEach(*callbackFn*[, *thisArg*])
按照插入顺序,为Set对象中的每一个值调用一次callBackFn。如果提供了
thisArg
参数,回调中的this
会是这个参数。 -
返回一个布尔值,表示该值在
Set
中存在与否。 -
与**
values()
**方法相同,返回一个新的迭代器对象,该对象包含Set
对象中的按插入顺序排列的所有元素的值。 -
返回一个新的迭代器对象,该对象包含
Set
对象中的按插入顺序排列的所有元素的值。 -
返回一个新的迭代器对象,该对象包含
Set
对象中的按插入顺序排列的所有元素的值。
Set中的特殊值
Set 对象存储的值总是唯一的,所以需要判断两个值是否恒等。有几个特殊值需要特殊对待:
- +0 与 -0 在存储判断唯一性的时候是恒等的,所以不重复;
- undefined 与 undefined 是恒等的,所以不重复;
- NaN 与 NaN 是不恒等的,但是在 Set 中只能存一个,不重复。
类型转换:
// Array 转 Setvar mySet = new Set(["value1", "value2", "value3"]);// 用...操作符,将 Set 转 Arrayvar myArray = [...mySet];// String 转 Setvar mySet = new Set('hello'); // Set(4) {"h", "e", "l", "o"}// 注:Set 中 toString 方法是不能将 Set 转换成 String
Set 对象作用
数组去重
var mySet = new Set([1, 2, 3, 4, 4]); [...mySet]; // [1, 2, 3, 4]
并集
var a = new Set([1, 2, 3]); var b = new Set([4, 3, 2]); var union = new Set([...a, ...b]); // {1, 2, 3, 4}
交集
var a = new Set([1, 2, 3]); var b = new Set([4, 3, 2]); var intersect = new Set([...a].filter(x => b.has(x))); // {2, 3}
差集
var a = new Set([1, 2, 3]); var b = new Set([4, 3, 2]); var difference = new Set([...a].filter(x => !b.has(x))); // {1}
WeakSet
WeakSet
对象允许你将弱保持对象存储在一个集合中。
和WeakMap
一样,里面存储的是对象的弱引用,如果对象的引用消失了,垃圾回收机制会回收这个对象,所以不会造成内存的泄露。这也导致他是无法遍历的,而且没有size
属性。
var ws = new WeakSet();var foo = {};var bar = {};ws.add(foo);ws.add(bar);ws.has(foo); // truews.has(bar); // truews.delete(foo); // 从set中删除 foo 对象ws.has(foo); // false, foo 对象已经被删除了ws.has(bar); // true, bar 依然存在
Set的实例方法:
-
在该
WeakSet
对象中添加一个新元素value
. -
WeakSet.prototype.delete(value)
从该
WeakSet
对象中删除value
这个元素, 之后WeakSet.prototype.has(value)
方法便会返回false
. -
返回一个布尔值, 表示给定的值
value
是否存在于这个WeakSet
中.
Proxy
ES6原生提供Proxy构造函数,可以直接生成Proxy实例。
const p = new Proxy(target, handler)
target
:要使用Proxy
包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。handler
:一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理p
的行为。
监听对象的方法:
// proxy 监听对象(相比于Object.defineProperty更好用)const objProxy = new Proxy(obj, { get: function(target, prop) { // target是目标对象,prop是被获取的属性名。 console.log({ type: 'get', target, prop }); return Reflect.get(target, prop); // Reflect 和 proxy 的handler对应,此处Reflect.get方法用来读取对象的属性。 }, set: function(target, prop, newValue) { // value是新属性值 console.log({ type: 'set', target, prop, newValue }); return Reflect.set(target, prop, newValue); // 此处Reflect.set方法用来设置对象的属性。 }});objProxy.value = 1; // setobjProxy.value; // get// Object.defineProperty 方法 监听对象Object.defineProperty(obj,'data', { // 注意:Object.defineProperty定义的属性是不可枚举的,除非设置enumerable: true get: function () { console.log({type: 'get'}); return data; }, set: function (newValue) { data = newValue; // 这里要用一个另外定义的属性,而不能使用obj.data。 console.log({type: 'set', newValue: data}); }});obj.data = 1; // setobj.data; // get
Proxy构造函数的方法:
-
创建一个可撤销的
Proxy
对象。
hander对象的方法:
handler
对象是一个容纳一批特定属性的占位符对象。它包含有 Proxy
的各个捕获器(trap)。
所有的捕捉器是可选的。如果没有定义某个捕捉器,那么就会保留源对象的默认行为。
-
Object.getPrototypeOf
方法的捕捉器。 -
Object.setPrototypeOf
方法的捕捉器。 -
Object.isExtensible
方法的捕捉器。 -
Object.preventExtensions
方法的捕捉器。 -
handler.getOwnPropertyDescriptor()
Object.getOwnPropertyDescriptor
方法的捕捉器。 -
Object.defineProperty
方法的捕捉器。 -
in
操作符的捕捉器。 -
属性读取操作的捕捉器。
-
属性设置操作的捕捉器。
-
delete
操作符的捕捉器。 -
Object.getOwnPropertyNames
方法和Object.getOwnPropertySymbols
方法的捕捉器。 -
函数调用操作的捕捉器。
-
new
操作符的捕捉器。
Reflect
Reflect (反射) 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与proxy handlers的方法相同。Reflect
不是一个函数对象,因此它是不可构造的。
与大多数全局对象不同Reflect
并非一个构造函数,所以不能通过new运算符对其进行调用,或者将Reflect
对象作为一个函数来调用。Reflect
的所有属性和方法都是静态的(就像Math
对象)。
Reflect
对象提供了以下静态方法,这些方法与proxy handler methods的命名相同。其中的一些方法与 Object
相同, 尽管二者之间存在 某些细微上的差别 。
Reflect对象的静态的方法:
-
Reflect.apply(target, thisArgument, argumentsList)
对一个函数进行调用操作,同时可以传入一个数组作为调用参数。和
Function.prototype.apply()
功能类似。 -
Reflect.construct(target, argumentsList[, newTarget])
对构造函数进行
new
操作,相当于执行new target(...args)
。 -
Reflect.defineProperty(target, propertyKey, attributes)
和
Object.defineProperty()
类似。如果设置成功就会返回true
-
Reflect.deleteProperty(target, propertyKey)
作为函数的
delete
操作符,相当于执行delete target[name]
。 -
Reflect.get(target, propertyKey[, receiver])
获取对象身上某个属性的值,类似于
target[name]。
-
Reflect.getOwnPropertyDescriptor(target, propertyKey)
类似于
Object.getOwnPropertyDescriptor()
。如果对象中存在该属性,则返回对应的属性描述符, 否则返回undefined
. -
Reflect.has(target, propertyKey)
判断一个对象是否存在某个属性,和
in
运算符 的功能完全相同。 -
返回一个包含所有自身属性(不包含继承属性)的数组。(类似于
Object.keys()
, 但不会受enumerable影响
). -
Reflect.preventExtensions(target)
类似于
Object.preventExtensions()
。返回一个Boolean
。 -
Reflect.set(target, propertyKey, value[, receiver])
将值分配给属性的函数。返回一个
Boolean
,如果更新成功,则返回true
。 -
Reflect.setPrototypeOf(target, prototype)
设置对象原型的函数. 返回一个
Boolean
, 如果更新成功,则返回true。
Promise 和 异步
需要使用异步的场景:
- 定时任务:setTimeout、setInterval
- 网络请求:ajax请求、动态加载
- 事件监听
- 事件的发布和订阅
Ajax
Asynchronous Javascript And XML(异步 JavaScript 和 XML)。它并不是凭空出现的新技术,而是对于现有技术的结合。Ajax 的核心是 js 对象:XMLHttpRequest。早期网络请求一般都使用Ajax,现在可以用更方便的fetch api。
在浏览器中,我们可以在不刷新页面的情况下,通过ajax的方式去获取一些新的内容。
我们在访问一个普通的网站时,当浏览器加载完HTML、CSS、JS
以后,网站的内容就固定了。如果想让网站内容发生更改,就必须刷新页面才能够看到更新的内容可如果用到异步更新,情况就大为改观了。比如,我们在访问新浪微博时,看到一大半了,点击底部的加载更多,会自动帮我们加载更多的微博,同时页面并没有刷新。试想一下,如果没有异步刷新的话,每次点击“加载更多”,网页都要刷新,体验就太不好了。
发送 Ajax 请求的五个步骤
其实也就是 使用 XMLHttpRequest 对象的五个步骤。
-
创建异步对象。即
XMLHttpRequest
对象。var xmlhttp = new XMLHttpRequest();
-
使用open方法设置请求的参数。open(method, url, async)。参数解释:请求的方法、请求的url、是否异步。
xmlhttp.open("GET","ajax_info.txt",true);
-
发送请求。
xmlhttp.send();
-
注册事件。 注册onreadystatechange事件,状态改变时就会调用。如果要在数据完整请求回来的时候才调用,我们需要手动写一些判断的逻辑。
// 当使用 async=true 时,请规定在响应处于 onreadystatechange 事件中的就绪状态时执行的函数:xmlhttp.onreadystatechange=function(){ if (xmlhttp.readyState==4 && xmlhttp.status==200) { document.getElementById("myDiv").innerHTML=xmlhttp.responseText; }}
-
获取返回的数据。
// 如果来自服务器的响应并非 XML,请使用 responseText 属性document.getElementById("myDiv").innerHTML=xmlhttp.responseText;//如果来自服务器的响应是 XML,而且需要作为 XML 对象进行解析,请使用 responseXML 属性:xmlDoc=xmlhttp.responseXML;txt="";x=xmlDoc.getElementsByTagName("ARTIST");for (i=0;i<x.length;i++){ txt=txt + x[i].childNodes[0].nodeValue + "<br>";}document.getElementById("myDiv").innerHTML=txt;
Promise
早期实现异步一般使用回调函数,而ES6中的promise对象, 可以将异步操作以同步的流程表达出来,很好地解决了回调地狱的问题。
但其本质上还是回调函数,只是一个语法糖而已。(回调函数的底层思想就是手动构造一个Continuation)
new Promise(function (resolve, reject) { console.log(1111); resolve(2222); // 传入2222给下一个then // reject(2222);}).then(function (value) { // 接受上面传来的2222并打印,同时返回3333(传入3333个下一个then) console.log(value); return 3333;}).then(function (value) { // 接受上面传来的3333并打印,抛出错误,会直接跳到catch console.log(value); throw "An error";}).catch(function (err) { // 接受上面传来的错误并打印。 console.log(err);});
promise对象的3个状态和回调函数里的两个参数
-
初始化状态(等待状态):pending
等待状态下promise的成功回调函数(then第一个参数)和失败回调函数(then第二个参数、catch的参数)不会马上放到微任务队列中,而是暂时存在Promise本身的成功回调队列和失败回调队列中。等到状态转变成fullfilled或rejected,再将相应的回调队列中的函数拿出放到微任务队列中。
-
成功状态:fullfilled
执行
resolve()
方法来改变promise状态为fullfilled,resolve() 中可以放置一个参数用于向下一个 then 传递一个值;then 中的函数也可以返回一个值传递给 then,但是,如果 then 中返回的是一个 Promise 对象,那么下一个 then 将相当于对这个返回的 Promise 进行操作。
Promise回调函数中的第一个参数
resolve()
,会对Promise执行"拆箱"动作(这个拆箱动作是异步的,放到微任务队列中);但第二个参数reject()
不会。 -
失败状态:rejected
执行
reject()
方法来改变promise状态为rejected,reject() 的参数一般会传递一个异常给之后的 catch 函数用于处理异常。注意:
-
resolve 和 reject 的作用域只有起始函数,不包括 then 以及其他序列;resolve 和 reject 并不能够使起始函数停止运行,then方法中别忘了 return。
-
构造函数中的 resolve 或 reject 只有第一次执行有效,多次调用没有任何作用,即promise 状态一旦改变则不能再变。
-
promise原型对象的方法
-
Promise.prototype.then(onFulfilled, onRejected)
添加解决(fulfillment)和拒绝(rejection)回调到当前 promise, 返回一个新的 promise, 将以回调的返回值来resolve.(即可以将参数中的函数添加到当前 Promise 的正常执行序列,.then() 传入的函数会按顺序依次执行,有任何异常都会直接跳到 catch 序列)
两个回调函数(参数)的返回值可以是以下三种情况中的一种:
return
一个同步的值 ,或者undefined
(当没有返回一个有效值时,默认返回undefined),then
方法将返回一个resolved状态的Promise对象,Promise对象的值就是这个返回值。return
另一个 Promise,then
方法将根据这个Promise的状态和值创建一个新的Promise对象返回。throw
一个同步异常(如果代码执行中遇到错误,也是相当于throw一个异常),then
方法将返回一个rejected状态的Promise, 值是该异常。
-
Promise.prototype.catch(onRejected)
添加一个拒绝(rejection) 回调到当前 promise, 返回一个新的promise。当这个回调函数被调用,新 promise 将以它的返回值来resolve,否则如果当前promise 进入fulfilled状态,则以当前promise的完成结果作为新promise的完成结果.(用来设定 Promise 的异常处理序列)实际上,
catch
就是then
的第二个参数的简便写法,正是有了catch
,then
一般都不会写第二个参数。 -
Promise.prototype.finally(onFinally)
添加一个事件处理回调于当前promise对象,并且在原promise对象解析完毕后,返回一个新的promise对象。回调会在当前promise运行完毕后被调用( Promise 执行的最后一定会执行),无论当前promise的状态是完成(fulfilled)还是失败(rejected),使用这个方法避免了在
then()
和catch()
中各写一次的情况。例如:当数据库操作时,无论操作成功还是失败,最后都需要断开数据库连接,否则会导致数据库链接被占用,这种情况就可以用finally()
。finally()
虽然与.then(onFinally, onFinally)
类似,它们不同的是:- 调用内联函数时,不需要多次声明该函数或为该函数创建一个变量保存它。
- 由于无法知道
promise
的最终状态,所以finally
的回调函数中不接收任何参数,它仅用于无论最终结果如何都要执行的情况。 - 与
Promise.resolve(2).then(() => {}, () => {})
(resolved的结果为undefined
)不同,Promise.resolve(2).finally(() => {})
resolved的结果为2
。 - 同样,
Promise.reject(3).then(() => {}, () => {})
(resolved 的结果为undefined
),Promise.reject(3).finally(() => {})
rejected 的结果为3
。
Promise.all() 和 Promise.race()
-
Promise.all可以将多个Promise实例包装成一个新的Promise实例。所有的promise都resolve,promise.all的状态才为fullfill,否则为reject。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。
let p1 = new Promise((resolve, reject) => { resolve('成功了')})let p2 = new Promise((resolve, reject) => { resolve('success')})let p3 = Promse.reject('失败')Promise.all([p1, p2]).then((result) => { console.log(result) //['成功了', 'success']}).catch((error) => { console.log(error)})Promise.all([p1,p3,p2]).then((result) => { console.log(result)}).catch((error) => { console.log(error) // 失败了,打出 '失败'})
Promse.all在处理多个异步处理时非常有用,比如说一个页面上需要等两个或多个ajax的数据回来以后才正常显示,在此之前只显示loading图标。需要特别注意的是,Promise.all获得的成功结果的数组里面的数据顺序和Promise.all接收到的数组顺序是一致的,即p1的结果在前,即便p1的结果获取的比p2要晚。这带来了一个绝大的好处:在前端开发请求数据的过程中,偶尔会遇到发送多个请求并根据请求顺序获取和使用数据的场景,使用Promise.all毫无疑问可以解决这个问题。
-
顾名思义,Promse.race就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。
let p1 = new Promise((resolve, reject) => { setTimeout(() => { resolve('success') },1000)})let p2 = new Promise((resolve, reject) => { setTimeout(() => { reject('failed'); }, 500)})Promise.race([p1, p2]).then((result) => { console.log(result)}).catch((error) => { console.log(error) // 打印的是 'failed'})
Promise.resolve():
Promise.resolve(value)
方法返回一个以给定值解析后的Promise
对象。此函数将类promise对象的多层嵌套展平。
-
如果这个值是一个 promise ,那么将返回这个 promise ;
-
如果这个值是thenable(即带有 then 方法),返回的promise会“跟随”这个thenable的对象,采用它的最终状态;
-
否则返回的promise将以此值完成(fullfill状态)。不传值就是没有返回值的fullfill状态。
Fetch 方法
Fetch 就是ES6提供的一个异步接口,这样省的自己封装了。fetch是基于Promise设计的,可以结合async和await使用,脱离了XHR。umi-request就是基于fetch封装的。
let url = "http://jsonplaceholder.typicode.com/posts ";fetch(url) .then(response => response.json()) /*解析数据流*/ .then(data => console.log(data)) .catch(err => console.log("error" + err));async function main(){ let respone = await fetch(url); console.log(response);}
Generator
generator同样是es6的新特性,但它并非为解决回调而存在的,只是它恰好拥有这个能力,koa1就是基于generator/yield实现异步的。
生成器对象是由一个 generator function 返回的,并且它符合可迭代协议和迭代器协议。
function* gen() { yield 1; // 注意 * 和 yield yield 2; yield 3;}let g = gen(); // "Generator { }"console.log(g.next()); // {value: 1, done: false}console.log(g.next()); // {value: 1, done: false}console.log(g.next()); // {value: 1, done: false}console.log(g.next()); // {value: undefined, done: true}
async /await
async 函数是在 ES2017 引入的,是官方的回调解决方案。koa2就是基于async/await实现异步的。
概念:真正意义上去解决异步回调的问题,同步流程表达异步操作,其底层已经不再是回调函数。
语法:
// 我们在普通的函数前面加上 async 关键字,就成了 async 函数。async function foo() { await 异步操作; // await 操作符用于等待一个 Promise 对象, 它只能在异步函数 async function 内部使用。 console.log('foo end')}// 上面这段代码相当于Promise.resolve(异步操作).then(()=>console.log('foo end')),这个异步操作是马上执行,但不是马上返回,返回之前会阻塞住后面的代码执行,然后执行权交回全局作用域。
async的特点:(与Promise、Generator的对比)
- 不需要像Generator去调用next方法,遇到await等待,当前的异步操作完成就往下执行。
- async返回的总是Promise对象,可以用then方法进行下一步操作。
- async取代Generator函数的星号*,await取代Generator的yield。
- 语意上更为明确,使用简单,经临床验证,暂时没有任何副作用。
本质: async是Generator 的语法糖。简单的说async函数就相当于自执行的Generator函数,相当于自带一个状态机,在 await 的部分等待返回, 返回后自动执行下一步。而且相较于Promise,async 的优越性就是把每次异步返回的结果从 then 中拿到最外层的方法中,不需要链式调用,只要用同步的写法就可以了。
async 比 promise 直观,但是 async 必须以一个 Promise 对象开始 ,所以 async 通常是和Promise 结合使用的。(注意:async/await处理异常情况一般使用try/catch,不同于promise的then/catch)
Axios
第三方库,不属于原生js,需要安装, 官方文档。
Axios是一个基于Promise ,用于浏览器和 nodejs 的 HTTP 客户端(更适合于mvvm框架),它本身具有以下特征:
- 从浏览器中创建 XMLHttpRequest
- 从 node.js 发出 http 请求
- 支持 Promise API
- 拦截请求和响应
- 转换请求和响应数据
- 取消请求
- 自动转换JSON数据
- 客户端支持防止 CSRF/XSRF
部分使用方法:
-
执行
GET
请求// 为给定 ID 的 user 创建请求axios.get('/user?ID=12345') .then(function (response) { console.log(response); }) .catch(function (error) { console.log(error); });// 上面的请求也可以这样做axios.get('/user', { params: { ID: 12345 } }) .then(function (response) { console.log(response); }) .catch(function (error) { console.log(error); });
-
执行
POST
请求axios.post('/user', { firstName: 'Fred', lastName: 'Flintstone' }) .then(function (response) { console.log(response); }) .catch(function (error) { console.log(error); });
-
执行多个并发请求,使用axios.all();
function getUserAccount() { return axios.get('/user/12345');}function getUserPermissions() { return axios.get('/user/12345/permissions');}axios.all([getUserAccount(), getUserPermissions()]) .then(axios.spread(function (acct, perms) { // 两个请求现在都执行完成 }));
-
创建实例
import axios from 'axios';
会导出默认实例,可以直接使用,但是如果需要有不同的服务请求和响应结构,就需要使用axios.create
来使用自定义配置创建多个实例。const instance = axios.create({ baseURL: 'https://some-domain.com/api/', timeout: 1000, headers: {'X-Custom-Header': 'foobar'}});
-
响应结构
{ // `data` 由服务器提供的响应 data: {}, // `status` 来自服务器响应的 HTTP 状态码 status: 200, // `statusText` 来自服务器响应的 HTTP 状态信息 statusText: 'OK', // `headers` 服务器响应的头 headers: {}, // `config` 是为请求提供的配置信息 config: {}, // 'request' // `request` is the request that generated this response // It is the last ClientRequest instance in node.js (in redirects) // and an XMLHttpRequest instance the browser request: {}}
DOM
DOM节点
节点的访问关系都是属性;
节点的操作都是方法(函数)。
根据 W3C 的 HTML DOM 标准,HTML 文档中的所有内容都是节点,下面是常见的其中几种节点:
-
整个文档是一个文档节点(document)
在一个浏览器窗口中可能有多个 document,例如,通过 iframe 加载的页面,每一个都是一个 document。
-
每个 HTML 元素是元素节点(document.createElement())
-
HTML 元素内的文本是文本节点(document.createTextNode())
-
每个 HTML元素的属性是属性节点(document.createAttribute())
获取属性值(元素节点. 属性名,例如:myimg.src)或(元素节点. getAttribute(“xxx”))
-
注释是注释节点(comment())
浏览器提供一个原生的节点对象Node
,上面提到的节点都继承了Node
,因此具有一些共同的属性和方法。
节点树:
一个文档的所有节点,按照所在的层级,可以抽象成一种树状结构。这种树状结构就是 DOM 树。它有一个顶层节点,下一层都是顶层节点的子节点,然后子节点又有自己的子节点,就这样层层衍生出一个金字塔结构,又像一棵树。
浏览器原生提供document
节点,代表整个文档。
文档的第一层有两个节点,第一个是文档类型节点(<!doctype html>
),第二个是 HTML 网页的顶层容器标签<html>
。后者构成了树结构的根节点(root node),其他 HTML 标签节点都是它的下级节点。
除了根节点,其他节点都有三种层级关系。
- 父节点关系(parentNode):直接的那个上级节点
- 子节点关系(childNodes):直接的下级节点
- 同级节点关系(sibling):拥有同一个父节点的节点
DOM 提供操作接口,用来获取这三种关系的节点。比如,子节点接口包括firstChild
(第一个子节点)和lastChild
(最后一个子节点)等属性,同级节点接口包括nextSibling
(紧邻在后的那个同级节点)和previousSibling
(紧邻在前的那个同级节点)属性。
DOM节点的属性
属性是节点(HTML 元素)的值,您能够获取或设置。
每个dom元素节点有自己的属性,例如img标签的src属性,除此之外dom对象也有一些公有的属性:
-
innerHTML属性
获取元素内容的最简单方法是使用 innerHTML 属性。
innerHTML 属性对于获取或替换 HTML 元素的内容很有用,会获取到双闭合标签里面的所有内容,包括子标签的标签名。
区别于
innerText
,该属性只会获取到双闭合标签里面的不包括子标签的标签名的内容。<script> var txt=document.getElementById("intro").innerHTML; document.write(txt); </script>
-
nodeName属性
nodeName 属性可依据节点的类型返回其名称。
如果节点是一个元素节点 , nodeName 属性将返回标签名。
如果节点是一个属性节点, nodeName 属性将返回属性名。
其他节点类型, nodeName 属性将返根据不同的节点类型返回不同的节点名称。
-
nodeValue 属性
nodeValue 属性规定节点的值。
- 元素节点的 nodeValue 是 undefined 或 null
- 文本节点的 nodeValue 是文本本身
- 属性节点的 nodeValue 是属性值
<script> x=document.getElementById("intro"); document.write(x.firstChild.nodeValue); </script>
-
nodeType属性
nodeType 属性返回节点的类型。nodeType 是只读的。
比较重要的节点类型有:
元素类型 NodeType 元素节点 1 属性节点 2 文本节点 3 注释 8 文档 9
三个属性的区别:
var element = document.getElementById("box1"); //获取元素节点(标签)var attribute = element.getAttributeNode("id"); //获取box1的属性节点var txt = element.firstChild; //获取box1的文本节点//获取nodeNameconsole.log(element.nodeName); //DIVconsole.log(attribute.nodeName); //idconsole.log(txt.nodeName); //#textconsole.log("--------------");//获取nodeValueconsole.log(element.nodeValue); //nullconsole.log(attribute.nodeValue); //box1console.log(txt.nodeValue); //生命壹号console.log("--------------");//获取nodeTypeconsole.log(element.nodeType); //1console.log(attribute.nodeType); //2console.log(txt.nodeType); //3
DOM节点的方法
方法 | 描述 |
---|---|
getElementById() | 返回带有指定 ID 的元素。(这四个查找方法都是仅属于document节点的方法) |
getElementsByTagName() | 返回包含带有指定标签名称的所有元素的节点列表(集合**/**节点数组)。 |
getElementsByClassName() | 返回包含带有指定类名的所有元素的节点列表(集合/节点数组)。 |
getElementsByName() | 返回包含带有指定name属性的所有input元素的节点列表。 |
appendChild() | 把新的子节点添加到指定节点。 |
removeChild() | 删除子节点。 |
replaceChild() | 替换子节点。 |
insertBefore() | 在指定的子节点前面插入新的子节点。 |
createAttribute() | 创建属性节点。 |
createElement() | 创建元素节点。 |
createTextNode() | 创建文本节点。 |
getAttribute() | 返回指定的属性值。 |
setAttribute() | 把指定属性设置或修改为指定的值。 |
removeAttribute() | 删除节点的指定属性。 |
DOM事件
DOM 事件允许Javascript在HTML文档元素中注册不同事件处理程序。
EventTarget 接口
DOM 的事件操作(监听和触发),都定义在EventTarget
接口。所有节点对象都部署了这个接口,其他一些需要事件通信的浏览器内置对象(比如,XMLHttpRequest
、AudioNode
、AudioContext
)也部署了这个接口。
该接口主要提供三个实例方法。
addEventListener
:绑定事件的监听函数removeEventListener
:移除事件的监听函数dispatchEvent
:触发事件
DOM操作流程:
- 获取事件源(即各种引发后续事件的html标签)
- 绑定事件源的某个事件的监听函数
- 书写事件驱动程序(也就是监听函数)
js 定义的常见的事件
-
鼠标事件。
**click:**单击鼠标按钮时触发;
dblclick:当用户双击主鼠标按钮时触发;
**mousedown:**当用户按下鼠标还未弹起时触发;
**mouseup:**当用户释放鼠标按钮时触发;
**mouseover:**当鼠标移到某个元素上方时触发;
**mouseout:**当鼠标移出某个元素上方时触发;
mousemove:当鼠标指针在元素上移动时触发;
**mouseenter:**在鼠标光标从元素外部首次移动至元素范围内触发,不参与冒泡;
mouseleave:鼠标移出;
鼠标事件触发顺序:
- 单击:mousedown,click,mouseup(实际上click和mouseup可以理解成同时触发)
- 双击:mousedown,click,mouseup,dblclick,mousedown,mouseup(DblClick是在第二次MouseDown前发出,而不是MouseUp,时间上和第二次MouseDown相同,并且第二次MouseUp之前不会再有Click事件)
-
键盘事件。
**keydown:**当用户按下键盘上任意键时触发,如果按住不放,会重复触发;
**keyup:**当用户释放键盘上的键触发;
**keypress:**当用户按下键盘上的字符键时触发,如果按住不放,会重复触发;
键盘事件触发顺序:
- 对于字符键来说:keydown,keypress,keyup
- 对于非字符键(如功能键或特殊键)来说:keydown,keyup
-
HTML事件。
**load:**当页面完全加载后在window上面触发,或当框架集加载完毕后在框架集上触发;(建议是:整个页面上所有元素加载完毕再执行js内容。window.onload可以预防使用标签在定义标签之前。)
注意
window.onload
和DOMContentLoaded
事件的区别:-
当
onload
事件触发时,页面上所有的DOM,样式表,脚本,图片,flash都已经加载完成了。 -
当
DOMContentLoaded
事件触发时,仅当DOM加载完成,不包括样式表,图片,flash。
**unload:**当页面完全卸载后在window上面触发,或当框架集卸载后在框架集上触发;
**select:**当用户选择文本框(input或textarea)中的一个或多个字符触发;
**change:**当文本框(input或textarea)内容改变且失去焦点后触发;
**input:**输入;
**focus:**当页面或者元素获得焦点时在window及相关元素上面触发;
**blur:**当页面或元素失去焦点时在window及相关元素上触发;
**submit:**当用户点击提交按钮在
<form>
元素上触发;**reset:**当用户点击重置按钮在
<form>
元素上触发;**resize:**当窗口或框架的大小变化时在window或框架上触发;
**scroll:**当用户滚动带滚动条的元素时触发;
-
移动端事件
-
click事件
单击事件,类似于PC端的click,但在移动端中,连续click的触发有200ms ~ 300ms的延迟
-
touch类事件,触摸事件
touchstart:手指触摸到屏幕会触发
touchmove:当手指在屏幕上移动时,会触发
touchend:当手指离开屏幕时,会触发
touchcancel:可由系统进行的触发,比如手指触摸屏幕的时候,突然alert了一下,或者系统中其他打断了touch的行为,则可以触发该事件
-
tap类事件,触碰事件,一般用于代替click事件,
tap: 手指碰一下屏幕会触发
longTap: 手指长按屏幕会触发
singleTap: 手指碰一下屏幕会触发
doubleTap: 手指双击屏幕会触发
-
swipe类事件,滑动事件
swipe:手指在屏幕上滑动时会触发
swipeLeft:手指在屏幕上向左滑动时会触发
swipeRight:手指在屏幕上向右滑动时会触发
swipeUp:手指在屏幕上向上滑动时会触发
swipeDown:手指在屏幕上向下滑动时会触发
绑定事件的监听函数的方法
浏览器的事件模型,就是通过**监听函数(listener)**对事件做出反应。事件发生后,浏览器监听到了这个事件,就会执行对应的监听函数。这是事件驱动编程模式(event-driven)的主要编程方式。
JavaScript 有三种方法,可以为事件绑定监听函数:
-
HTML 的
on-xxx
属性(on后面直接加事件名)<body onload="doSomething()"> // 这个方法与通过元素节点的setAttribute方法设置on-属性,效果是一样的<Element οnclick="doSomething()">el.setAttribute('onclick', 'doSomething()'); // 效果一样
使用这个方法指定的监听代码,只会在冒泡阶段触发。
-
元素节点的事件属性
window.onload = doSomething;div.onclick = function (event) { console.log('触发事件');};
使用这个方法指定的监听函数,也是只会在冒泡阶段触发。
注意,这种方法与 HTML 的
on-
属性的差异是,它的值是函数名(doSomething
),而不像后者,必须给出完整的监听代码(doSomething()
) -
EventTarget.addEventListener()
(推荐使用)所有 DOM 节点实例都有
addEventListener
方法,用来为该节点定义事件的监听函数。window.addEventListener('load', doSomething, false);
this 的指向
监听函数内部的this
指向触发事件的那个元素节点。
<button id="btn" onclick="console.log(this.id)">点击</button>
执行上面代码,点击后会输出btn
。
其他两种监听函数的写法,this
的指向也是如此。
// HTML 代码如下// <button id="btn">点击</button>var btn = document.getElementById('btn');// 写法一btn.onclick = function () { console.log(this.id);};// 写法二btn.addEventListener( 'click', function (e) { console.log(this.id); }, false);
上面两种写法,点击按钮以后也是输出btn
。
事件的传播(事件流)
一个DOM事件(专指 DOM2 级事件)发生后,在子元素和父元素之间的传播(propagation)会分成三个阶段:
- 第一阶段:从
window
对象传导到目标节点(上层传到底层),称为“捕获阶段”(capture phase)。 - 第二阶段:在目标节点上触发,称为“目标阶段”(target phase)。
- 第三阶段:从目标节点传导回
window
对象(从底层传回上层),称为“冒泡阶段”(bubbling phase)。
这种三阶段的传播模型,使得同一个事件会在多个节点上触发。
<div> <p>点击</p></div>
上面代码中,<div>
节点之中有一个<p>
节点。
如果对这两个节点,都设置click
事件的监听函数(每个节点的捕获阶段和冒泡阶段,各设置一个监听函数),共计设置四个监听函数。然后,对<p>
点击,click
事件会触发四次。
var phases = { 1: 'capture', 2: 'target', 3: 'bubble'};var div = document.querySelector('div');var p = document.querySelector('p');// 这里第三个参数是useCapture,true为捕获,false为冒泡,几乎所有浏览器都支持冒泡,所以这个参数默认为falsediv.addEventListener('click', callback, true); p.addEventListener('click', callback, true);div.addEventListener('click', callback, false);p.addEventListener('click', callback, false);function callback(event) { var tag = event.currentTarget.tagName; var phase = phases[event.eventPhase]; console.log("Tag: '" + tag + "'. EventPhase: '" + phase + "'");}// 点击以后的结果// Tag: 'DIV'. EventPhase: 'capture'// Tag: 'P'. EventPhase: 'target'// Tag: 'P'. EventPhase: 'target'// Tag: 'DIV'. EventPhase: 'bubble'
上面代码表示,click
事件被触发了四次:<div>
节点的捕获阶段和冒泡阶段各1次,<p>
节点的目标阶段触发了2次。
- 捕获阶段:事件从
<div>
向<p>
传播时,触发<div>
的click
事件; - 目标阶段:事件从
<div>
到达<p>
时,触发<p>
的click
事件; - 冒泡阶段:事件从
<p>
传回<div>
时,再次触发<div>
的click
事件。
其中,<p>
节点有两个监听函数(addEventListener
方法第三个参数的不同,会导致绑定两个监听函数),因此它们都会因为click
事件触发一次。所以,<p>
会在target
阶段有两次输出。
注意,浏览器总是假定click
事件的目标节点,就是点击位置嵌套最深的那个节点(本例是<div>
节点里面的<p>
节点)。所以,<p>
节点的捕获阶段和冒泡阶段,都会显示为target
阶段。
事件传播的最上层对象是window
,接着依次是document
,html
(document.documentElement
)和body
(document.body
)。也就是说,上例的事件传播顺序,在捕获阶段依次为window
、document
、html
、body
、div
、p
,在冒泡阶段依次为p
、div
、body
、html
、document
、window
。
事件的代理(事件委托)
由于事件会在冒泡阶段向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件。这种方法叫做事件的代理(delegation)。
var ul = document.querySelector('ul');ul.addEventListener('click', function (event) { if (event.target.tagName.toLowerCase() === 'li') { // some code }});
上面代码中,click
事件的监听函数定义在<ul>
节点,但是实际上,它处理的是子节点<li>
的click
事件。这样做的好处是,只要定义一个监听函数,就能处理多个子节点的事件,而不用在每个<li>
节点上定义监听函数。而且以后再添加子节点,监听函数依然有效。
取消默认事件
w3c的方法是e.preventDefault(),IE则是使用e.returnValue = false;
元素必须有默认行为才能被取消,如果元素本身就没有默认行为,调用就无效了。什么元素有默认行为呢?如链接<a>
,提交按钮<input type="submit">
等。当Event 对象的 cancelable为false时,表示没有默认行为,这时即使有默认行为,调用preventDefault也是不会起作用的。
阻止事件冒泡
阻止事件冒泡:如果希望事件到某个节点为止,不再传播,可以使用事件对象的stopPropagation
方法(IE则是使用e.cancelBubble = true)。
// 事件捕获到 p 元素后,就不再向下传播了p.addEventListener('click', function (event) { event.stopPropagation(); // 该事件后面捕获的将停止}, true);// 事件冒泡到 p 元素后,就不再向上冒泡了p.addEventListener('click', function (event) { event.stopPropagation(); // 该事件后面冒泡的将停止}, false);
上面代码中,stopPropagation
方法分别在捕获阶段和冒泡阶段,阻止了事件的传播。
但是,stopPropagation
方法只会阻止事件的传播,不会阻止该事件触发<p>
节点的其他click
事件的监听函数。也就是说,不是彻底取消click
事件。
p.addEventListener('click', function (event) { event.stopPropagation(); console.log(1);});p.addEventListener('click', function(event) { // 会触发 console.log(2);});
上面代码中,p
元素绑定了两个click
事件的监听函数。stopPropagation
方法只能阻止这个事件的传播,不能取消这个事件,因此,第二个监听函数会触发。输出结果会先是1,然后是2。
如果想要彻底取消该事件,不再触发后面所有click
的监听函数,可以使用stopImmediatePropagation
方法。
p.addEventListener('click', function (event) { event.stopImmediatePropagation(); console.log(1);});p.addEventListener('click', function(event) { // 不会被触发 console.log(2);});
上面代码中,stopImmediatePropagation
方法可以彻底取消这个事件,使得后面绑定的所有click
监听函数都不再触发。所以,只会输出1,不会输出2。
BOM
BOM
即浏览器对象模型(Browser Object Model)。BOM
对象是在Web中使用JavaScript的核心,该对象提供了与浏览器交互相关对象结构。BOM
对象是内容无关,主要用于管理浏览器窗口及窗口之间的通讯。
在BOM
对象中,window
对象是最顶层对象,在浏览器环境中它是一个Global全局对象,其它对象(如:DOM对象)对是这个对象的属性(子对象),一个变量如果未声明,那么默认就是顶层对象的属性(使用let,var,const声明的变量就不是)。其子对象可以通过window对象来使用,也可以直接使用。比如说,我可以使用 window.location.href
,也可以直接使用 location.href
,二者是等价的。每个对象都有自己的属性和方法,直接使用即可,不用实例化对象(可以理解为js已经帮我们实例化了)。下面是BOM
对象的组成结构:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xCrrEe8B-1629475321546)(assets\image-20210214135757272.png)]
BOM中的对象
window
对象
window
对象对象表示一个浏览器窗口或一个frame
框架,它处于对象层次的最顶端,它提供了处理浏览器窗口的方法和属性。
window
对象是浏览器对象中的默认对象,所以可以隐式地引用window
对象的属性和方法。在浏览器环境中,添加到window
对象中的方法、属性等,其作用域都是全局的。JavaScript中的标准内置对象,在浏览器环境中也是做为window
的方法和属性出现的。
下面提到的对象,均已在window中实例化,直接使用即可。
DOM
(document
)相关对象
DOM
可以认为是BOM
的一个子集,DOM
中文档操作相关对象,如:Node
、Document
、Element
等DOM
节点类型对象,都是做为window
对象的子属性出现的。
document
是window
对象的了个属性,它是一个Document
对象实例,表示当前窗口中文档对象。通过该对象,可以对文档和文档中元素、节点等进行操作。
frames
对象
frames
对象是一个集合,表示当前页面中使用的子框架。如果页面中使用了框架,将产生一个框架集合frames
,在集合中可以用数字下标(从0开始)或名字索引框架。集全中的每一个对象,包含了框架的页面布局信息,以及每一个框架所对应的window
对象。
navigator
对象
navigator
是指浏览器对象,该对象提供了当前正在使用的浏览器的信息。navigator
对象中的属性是只读的,在W3C在HTML5标准中,对该对象进行了规范。由于浏览器的同,该对象的具体值可能有所区别。
通过该对象可以识别不同的浏览器,一般我们只会使用navigator.userAgent
来获取浏览器的信息。
var ua = navigator.userAgent; // 获取当前浏览器的 userAgentif (/firefox/i.test(ua)) { //test()方法用于检测一个字符串是否匹配某个模式(一般是正则表达式) alert('是火狐浏览器');} else if (/chrome/i.test(ua)) { alert('是Chrome浏览器');} else if (/msie/i.test(ua)) { alert('是IE浏览器');} else if ('ActiveXObject' in window) { alert('是 IE11 浏览器');}
history
对象
history
对象来保存浏览器历史记录信息,也就是用户访问的页面。由于隐私原因,该对象不能获取到具体的历史记录,只能操作浏览器向前或向后翻页,而且该操作只在当次访问时有效。history
对象记录了用户浏览过的页面,通过该对象提供的API可以实现与浏览器前进/后退类似的导航功能。
属性:
-
History.length
:只读,返回一个整数,该整数表示会话历史中元素的数目,包括当前加载的页。例如,在一个新的选项卡加载的一个页面中,这个属性返回1。 -
History.scrollRestoration
:允许Web应用程序在历史导航上显式地设置默认滚动恢复行为。此属性可以是自动的(auto)或者手动的(manual)。 -
History.state
:只读,返回一个表示历史堆栈顶部的状态的值。这是一种可以不必等待popstate
事件而查看状态的方式。
方法:
-
在浏览器历史记录里前往上一页, 用户可点击浏览器左上角的返回,页面的后退按钮模拟此方法. 等价于
history.go(-1)
.注意:当浏览器会话历史记录处于第一页时调用此方法没有效果,而且也不会报错。
-
在浏览器历史记录里前往下一页,用户可点击浏览器左上角的前进,页面的前进按钮模拟此方法. 等价于
history.go(1)
.注意:当浏览器历史栈处于最顶端时( 当前页面处于最后一页时 )调用此方法没有效果也不报错。
-
通过当前页面的相对位置从浏览器历史记录( 会话记录 )加载页面。
比如:参数为-1的时候为上一页,参数为1的时候为下一页. 当整数参数超出界限时,例如:如果当前页为第一页,前面已经没有页面了,我传参的值为-1,那么这个方法没有任何效果也不会报错。调用没有参数的
go()
方法或者不是整数的参数时也没有效果。( 这点与支持字符串作为url参数的IE有点不同)。 -
按指定的名称和URL(如果提供该参数)将数据push进会话历史栈,数据被DOM进行不透明处理;你可以指定任何可以被序列化的javascript对象。注意到Firefox现在忽略了这个title参数,更多的信息,请看manipulating the browser history。
-
按指定的数据,名称和URL(如果提供该参数),更新历史栈上最新的入口。这个数据被DOM 进行了不透明处理。你可以指定任何可以被序列化的javascript对象。注意到Firefox现在忽略了这个title参数,更多的信息,请看manipulating the browser history。
history.back(); //回退到上一个页面(后退按钮)history.forward(); //跳转到下一个页面(前进按钮)history.go(1); // 跳转history中指定的一个页面,参数为0即为当前页面
location
对象
location
是一个静态对象,该对象是对当前窗口URL地址的解析。该对象提供了可以访问URL中不同部分的信息属性,通过location
对象可以获取地址栏信息,也可以实现页面或锚点跳转等功能。
详情请参考mdn-location
screen
对象
screen
对象中包含了用户显示器屏幕相关信息。通过该对象,可以访问用户显示器屏幕宽、高、色深等信息。
window对象常用的功能
定时器
-
setInterval()
循环调用。将一段代码,每隔一段时间执行一次。(循环执行)let num = 1; setInterval(function () { num ++; console.log(num); }, 1000);
假设定时器setInterval()的返回值是
参数1
,那么clearInterval(参数1)
就可以清除定时器。 -
setTimeout()
延时调用。将一段代码,等待一段时间之后再执行。(只执行一次)代码举例:
const timer = setTimeout(function() { console.log(1); // 3秒之后,再执行这段代码。 }, 3000); clearTimeout(timer);
代码举例:(箭头函数写法)
setTimeout(() => { console.log(1); // 3秒之后,再执行这段代码。 }, 3000);
打开窗口和关闭窗口
window.open(url,target,param);window.close();
代码举例:
<body><a href="javascript:;">点击我打开一个新的页面</a><a href="javascript:;">点击我关闭本页面</a><script> //新窗口 = window.open(地址,是否开新窗口,新窗口的各种参数); var a1 = document.getElementsByTagName("a")[0]; var a2 = document.getElementsByTagName("a")[1]; a1.onclick = function () {//举例1: window.open("http://www.jx.com","_blank"); var json = { "name": "helloworld", "fullscreen": "no", "location": "no", "width": "100px", "height": "100px", "top": "100px", "left": "100px" }; window.open("http://www.baidu.com", "_blank", json); //举例2 } //关闭本页面 a2.onclick = function () { window.close(); }</script></body>
新窗口相关:
- 新窗口.moveTo(5,5)
- 新窗口.moveBy()
- 新窗口.resizeTo()
- window.resizeBy()
代码举例:
var newWin = window.open("demo.html", "_blank", json);newWin.moveTo(500, 500);