约定
1. `dom`为JS对象,`$dom`为jQuery(或Zepto)对象。 2. 大部分情况下,jQuery内容适用于Zepto。
每个函数都是一个Function
对象,像普通对象一样拥有属性和方法。
函数拥有
length
:希望接收的命名参数个数(计数到默认参数
或剩余参数
之前的形参)name
:函数名prototype
:(函数独有)指向函数的原型对象ES6不推荐使用(部分情况下导致报错):
arguments.callee
是一个指针:其指向拥有arguments
对象的函数(函数自身)。函数对象.caller
:保存调用当前函数的函数(嵌套的外一层函数)的引用。arguments.caller
(值为undefined
,仅为分清arguments.caller
和函数对象.caller
)。函数总有返回值。
undefined
。new
):默认返回this
。创建函数的方式:
函数声明(函数语句)
function 名字 (多个参数) {/* 函数体 */}
函数名绑定在所在作用域。
若
function
(或箭头函数的()
)是声明中的第一个词,则是函数声明,否则是函数表达式。
函数表达式(function expression)
var 名字 = function 内部名字 (多个参数) {/* 函数体 */}
(或var 名字 = (多个参数) => {/* 函数体 */}
)(function 内部名字 (多个参数) {/* 函数体 */}())
(或((多个参数) => {/* 函数体 */})()
)函数名绑定在函数表达式内部函数体中。
- 通过函数声明、函数表达式创建的函数,在加载脚本时和其他代码一起解析(编译时);通过构造函数定义的函数,在构造函数被执行时(运行时)才解析函数体字符串。
- 不推荐通过
构造函数创建函数,因为作为字符串的函数体可能会阻止一些JS引擎优化,也会引起其他问题。
构造函数
var 名字 = new Function([多个参数, ]函数体字符串);
直接调用
Function
(不使用new
操作符)的效果与调用构造函数一样,区别是this
指向window
。
只能提供给其他变量引用、或自执行。
匿名函数:可以添加仅在匿名函数内部才能使用的函数名字,主要用于在执行栈中标记匿名函数的函数名(主要针对:自执行匿名函数),而不只是显示
e.g. ```javascript var func1 = function func2() { // 其中函数名`func2`只能在函数体内部使用。 console.log(typeof func1); // => function console.log(typeof func2); // => function }; func1(); console.log(typeof func1); // => function console.log(typeof func2); // => undefined ```anonymous
实例化(new
)一个构造函数
- 相对于单全局变量,构造函数更加灵活,可以生成多个对象进行互相独立的操作。
- 构造函数,其实就是一个普通函数,没有任何特殊。
new
得到的实例对象(若构造函数不是返回一个引用数据类型),拥有构造函数体内使用this
定义的属性和方法,且拥有构造函数的原型对象上的属性和方法(因为实例的[[Prototype]]
指向构造函数.prototype
);在构造函数体内var
的变量和function
无法被这个对象使用,只能在构造函数里使用(类似私有变量)。new 构造函数() instanceof 构造函数 === false
new Func
等价于:new Func()
。new Obj().func()
等价于:(new Obj()).func()
(先新建实例,再调用实例的方法/原型链上方法)。new Obj.func()
等价于:new (Obj.func)()
、new (Obj.func)
、new Obj.func
(新建实例)。
new
一个构造函数执行的步骤new
let
或const
再次声明同名参数(var
可以)。使用参数默认值时的特殊情况:
undefined
或不传,才进行参数默认值的表达式值计算(惰性求值),默认值是运行时执行而不是undefined
或不传参时才执行其他函数)。调用函数时,所有参数会形成一个单独作用域(context)进行初始化,初始化结束则作用域消失。此作用域中的参数进行类似let
定义,因此函数不能有同名参数。
e.g.
```javascript function f1(y1 = x1) { // 形成短暂的单独作用域,x1没有定义,向父级作用域取值,取不到则报错 let x1 = 'x1' console.log(y1) } f1() // ReferenceError: x1 is not defined let x2 = 'outer x2' function f2(y2 = x2) { // 形成短暂的单独作用域,x2没有定义,向父级作用域取值 let x2 = 'x2' console.log(y2) } f2() // => 'outer x2' let x3 = 'outer x3' function f3(x3, y3 = x3) { // 形成短暂的单独作用域,x3是传参定义的值,不向父级作用域取值 console.log(y3) } f3() // => undefined f3(3) // => 3 let x4 = 'outer x4' function f4(x4 = x4) { // 形成短暂的单独作用域,实际执行类似let x = x,暂时性死区导致报错 } f4() // ReferenceError: x is not defined ```
建议参数都用对象形式传递,且形参设置为解构赋值+默认参数。
e.g.
function func ({ para1 = 'default', para2 } = {}) {}
Math.pow(2, 16)
。变量、this
的取值
设计函数的参数时,有2种方式:①多参数;②单参数是对象。
可选的参数最好设计成对象形式,方便将来扩展。
// 方便扩展
function func1 (必填1, 必填2, { 可选1 = '默认值1' } = {}) {}
function func2 ({ 必填1, 必填2, 可选1 = '默认值1' } = {}) {}
// 不方便扩展
function func3 (必填1, 必填2, 可选1 = '默认值1') {}
this
变量
(词法作用域)
与
调用函数时所在的作用域、时机、以何种方式被调用无关(函数赋值给任何变量或属性也不改变变量取值)。
遍历嵌套作用域链,在当前执行的作用域开始(函数声明处),向上遍历查找直到全局作用域或找到;
- 对于
变量 = 值
赋值(LHS),若直到全局作用域也没查找到变量,则创建一个全局变量,用值
赋值给这个新建的全局变量。- 对于
变量
引用(RHS),若直到全局作用域也没查找到变量,则报错ReferenceError: 变量 is not defined
。
多层的嵌套作用域中,可以定义同名的标识符(变量、函数),遮蔽效应使内部的标识符「遮蔽」外部的标识符(若查找到第一个匹配的标识符则停止再向上查找)。
window.某变量
可以访问被同名变量某变量
遮蔽的全局变量。
产生闭包
- 变量命名规范(包含:方法名、类名等所有变量名)
只能由字母
、数字
、_
、$
,不能以数字开头
允许非英文字母,但不推荐。
不能使用关键字、保留字
区分大小写
一般推荐的多个单词隔断方式:小驼峰、大驼峰;(表示常数:)全大写+单词用
_
分割。
this
(函数调用方式)
非箭头函数
this
取值类似于动态作用域。
this
:调用函数的那个对象(与在什么作用域无关);this
的值:在函数被调用时才会指定。
直接函数调用(如:alert()
)、立即调用的函数表达式(如:(function () {}())
)
this
:全局对象
严格模式:
undefined
对象的方法调用(如:obj1.obj2.func()
)
this
:最后一层对象(obj2
)
间接调用(如:alert.call/apply/bind(传入的值)
等this替代)
this
:传入的值
非严格模式,传入的值:
- 若是引用数据类型,则传入什么给
this
,函数的this
就是什么。若是基本数据类型
- 则
this
成为那个值的基本包装类型(除了undefined/null
之外);- 若不传或传入
undefined/null
,则this
为全局对象。严格模式,传入的值:
传入什么给
this
,函数的this
就是什么。
构造函数实例化(如:new RegExp()
)
this
:新实例对象
new (func.bind(传入的值))()
的this
是新实例对象,而不是。bind
传入的值
以上规则实施顺序(优先->其次):构造函数实例化 -> 间接调用 -> 对象的方法调用 -> 直接函数调用。
某些挂在
```javascript // e.g. const a = { b: window.open } a.b() // TypeError: Illegal invocation a.b.call({}) // TypeError: Illegal invocation a.b.call(null) // 正常 ```window
下的方法,内部实现有使用到this
,且this
需要指向特殊对象(如:必须指向window
或null
),因此当新建对象指向某方法时,注意this。
箭头函数
不会创建自己的this
(所以不会使用非箭头函数的规则),根据词法作用域向上遍历查找直到非箭头函数定义的this
或全局对象(严格模式为undefined);若找到this
,则再根据非箭头函数的方式决定取值。
看上去就像:使用封闭执行上下文最近的一个
this
值。
e.g.
```javascript var a = { foo: () => { // 箭头函数 return () => { console.log(this) } }, bar () { // 非箭头函数(简写的方法) return () => { console.log(this) } } } a.foo()() // => 全局对象 a.bar()() // => a var b = a.bar b()() // => 全局对象 var foo1 = () => {console.log(this)} // 箭头函数 var foo2 = function () {console.log(this)} // 非箭头函数 var obj2 = { foo1: foo1, foo2: foo2 } var obj1 = { obj2: obj2 } obj1.obj2.foo1() // => 全局对象 obj1.obj2.foo2() // => obj2 var c = { d: { e: () => { console.log(this) } } } c.d.e() // => 全局对象 ```
因为JS是词法作用域,所以产生了闭包效果。
产生效果:
通过eval
、with
,能在JS运行期修改或创建书写期就确定的词法作用域。
运行期修改或创建词法作用域,会导致JS无法进行JIT优化(当JS引擎遇到这些代码时,只能谨慎地不进行JIT优化,因此可能导致所有优化都失效)。且极度不安全,最好不要使用。
eval
接受一个字符串作为参数,将其中的内容视为好像在书写时就存在于程序中这个位置的代码(用程序动态生成代码并运行,好像代码是写在这个位置一样)。
// e.g.
function foo (str, a) {
eval(str) // 改变词法作用域
console.log(a, b)
}
var b = 2
foo('var b = 3', 1) // => 1 3
setTimeout/setInterval
的第一个参数是字符串、new Function
的最后一个字符串参数,都能动态生成代码,都能在运行期修改书写期的词法作用域。动态执行JS脚本的方式:
eval
、new Function
、Node.js的vm
模块。
with
// e.g.
function foo (obj) {
with (obj) { // 为形参obj创建作用域
a = 2 // 作用域中查找变量a并赋值(若查找不到则向上查找,顶层也找不到则创建一个全局变量)
}
}
var o1 = { a: 1 }
foo(o1)
console.log(o1.a) // => 2
var o2 = {}
foo(o2)
console.log(o2.a, a) // => undefined 2
构造函数、原型对象、实例、原型链
只有函数有prototype
属性,指向函数的原型对象。
互相连接:函数拥有prototype
属性指向其原型对象,原型对象拥有constructor
属性指向函数。
构造函数通过
prototype
为实例存储要共享的属性和方法,可设置prototype
指向其他对象来继承其他对象。
当构造函数实例化(new
),该实例拥有[[Prototype]]
属性,指向构造函数的原型对象。
访问对象的
[[Prototype]]
属性:(非标准)对象.__proto__
、Object.getPrototypeOf(对象)/Object.setPrototypeOf(对象, 原型对象)
。
每个引用数据类型都有[[Prototype]]
属性,指向自己的构造函数.prototype
(原型对象)。
每个引用数据类型都显式或隐式由某个构造函数创建。
不断向上的[[Prototype]]
属性,构成了原型链。
访问一个引用数据类型的属性:若这个属性在对象自身中不存在,则向上查找其
[[Prototype]]
指向的对象;若依然找不到,则继续向上查找(其[[Prototype]]
指向的对象的)[[Prototype]]
指向的对象,直到原型终点。
原型链终点是null
,倒数第二是Object.prototype
。
Object或Function或任意函数.__proto_ === Function.prototype
Function.prototype.__proto__ === Object.prototype
Object.prototype.__proto__ === null
若重写原型的值(不是添加),可以给原型添加constructor
属性并指向构造函数
var A = function () {};
A.prototype = {
other: '...'
};
if (typeof Object.defineProperty === 'function') {
// 使属性:不可以改变描述符、不可以删除、不可以枚举、不可以被赋值运算符改变
Object.defineProperty(A.prototype, 'constructor', {
value: A
});
} else {
A.prototype.constructor = A;
}
通过原型链实现继承
纯粹的空对象(没有继承Object.prototype
的空对象,原子空对象):
Object.create(null)
或Object.setPrototypeOf({}, null)
{}
等价于:Object.create(Object.prototype)
继承原理
子类继承父类原型链:
Son.prototype = Object.create(Father.prototype)
class-extends
则自动实现继承方式
ES6:class-extends
能够继承原生构造函数:先新建父类的实例对象this
,然后再用子类的构造函数修饰this
,使得父类的所有行为都可以继承。
class Father {
constructor (...args) { // 可省略
// this.
}
父类方法 () {}
}
class Son extends Father {
constructor (...args) { // 可省略
super(...args)
// this.
}
子类方法 () {}
}
/* 使用测试 */
Son.__proto__ === Father // => true
Son.prototype.__proto__ === Father.prototype // => true
var father1 = new Father('父para')
var son1 = new Son('子para1', '子para2')
son1.__proto__.__proto__ === father1.__proto__ // => true
ES5模拟:寄生组合式继承
无法继承原生构造函数:先新建子类的实例对象this
,再将父类的属性添加到子类上,导致父类的内部属性无法获取。
/* 父类定义: */
function Father(fatherPara) {
// 私有属性用let;静态私有属性用const
/* 父类属性 */
this.父类属性 = fatherPara;
}
/* 父类原型链 */
Father.prototype.父类原型链方法 = function () {};
/* 子类定义: */
function Son(sonPara1, sonPara2) {
/* 子类继承父类属性 */
Father.call(this, sonPara1);
/* 子类属性 */
this.子类属性 = sonPara2;
}
/* 子类继承父类原型链 */
Son.prototype = new Father();
Son.prototype.constructor = Son;
// 或ES6:Son.prototype = Object.create(Father.prototype, {constructor: {value: Son}});
Son.__proto__ = Father; // 同步class的实现
/* 子类原型链 */
Son.prototype.子类原型链方法 = function () {};
/* 使用测试 */
Son.__proto__ === Father; // => true
Son.prototype.__proto__ === Father.prototype; // => true
var father1 = new Father('父para');
var son1 = new Son('子para1', '子para2');
son1.__proto__.__proto__ === father1.__proto__ // => true
「子类继承父类原型链」可改为的不使用
```javascript (function (subType, superType) { function _object(o) { function F() {} F.prototype = o; return new F(); } var prototype = _object(superType.prototype); prototype.constructor = subType; subType.prototype = prototype; }(Son, Father)); ```Object.create
方式
变量在内存中的存储
栈内存(stack):
先进后出,寄存速度快,栈数据可共享,系统自动分配,数据固定不灵活,空间大小有限制,超出则栈溢出。
存储内容:变量标识(指向基本数据类型、引用数据类型的地址)、基本数据类型、引用数据类型的地址。
堆内存(heap):
顺序随意,寄存速度慢,由程序申请,操作简单,存储空间较大(取决于系统有效虚拟内存)。
存储内容:引用数据类型。
变量的值传递
变量(包括函数的参数)都是值传递(变量指向的值复制给另一个变量去指向)。
引用数据类型的浅复制
引用数据类型的深复制
数组(对象)的值传递
对于数组
arr
或对象obj
。
清空数组(对象)
不改变原始数组(对象)
arr = [] // (新赋值一个空数组)
对象:
obj = {} // (新赋值一个空对象)
改变原始数组(对象)
arr.length = 0
或arr.splice(0, arr.length)
对象:遍历属性并
delete
改变传入函数的数组(对象),但不改变实参
不考虑原型链。
内存泄露:计算机内存逐渐丢失。当某个程序总是无法释放内存时,出现内存泄露。
不是内存泄漏,只是不会被垃圾回收:
DOM清空或删除时事件绑定未清除,引起的内存泄漏:删除DOM前,先移除事件绑定。
jQuery的
empty
和remove
会移除元素内部的一切,包括绑定的事件及与该元素相关的jQuery数据(data、promise等所有数据);detach
则保留所有jQuery数据。
null
。随着JS引擎的更新,原来会导致内存泄漏的bug已经慢慢被修复,因此写代码时不太需要注意内存泄漏问题(误);Node.js的内存泄漏比较严重、隐蔽、难根除。
todo: chrome如何查内存和内存泄漏,Node.js如何查隐蔽的内存泄漏和如何规避。console.profile()和console.profileEnd()
递归赋值(最全面方式)
深复制要处理的坑:循环引用、各种引用数据类型、执行性能。
针对仅能够被JSON直接表示的数据结构(对象、数组、数值、字符串、布尔值、null):
JSON.parse(JSON.stringify(obj));
不考虑原型链。
Proxy
强制转换
Number(参数)
基本数据类型:
e.g.
Number('123') // 123
Number('123a') // NaN
Number('') // 0
Number(true) // 1
Number(false) // 0
Number() // 0
Number(undefined) // NaN
Number(null) // 0
Number(Symbol(123)) // Uncaught TypeError: Cannot convert a Symbol value to a number
Number(123n) // 123
parseInt
与Number
均忽略字符串前后的不可见字符。parseInt
从前向后逐个解析字符,只要开头有数字则返回数值;Number
判断只要有一个字符无法转成数值,则返回NaN
。
引用数据类型:
若参数是单个数值的数组、空数组、单个数值为undefined的数组,则返回数值
、0
、0
;否则返回NaN
。
e.g.
Number([5]) // 5
Number([]) // 0
Number([undefined]) // 0
Number([1, 2, 3]) // NaN
Number({}) // NaN
Number(对象)
具体规则:
valueOf
,若返回值是基本数据类型,则再使用Number
,不再进行后续步骤;toString
,若返回值是基本数据类型,则再使用Number
,不再进行后续步骤;TypeError: Cannot convert object to primitive value
。String(参数)
与
`${参数}`
返回效果一致。
基本数据类型:
数值:
转为相应的字符串
字符串:
原来的值
布尔值:
String(true) // 'true'
String(false) // 'false'
undefined
:
String(undefined) // 'undefined'
null
:
String(null) // 'null'
Symbol
:
String(Symbol()) // 'Symbol()'
String(Symbol(1)) // 'Symbol(1)'
String(Symbol('a')) // 'Symbol(a)'
BigInt
:
String(1n) // '1'
引用数据类型:
若是数组,则返回该数组的字符串形式;否则返回一个类型字符串。
// e.g.
String([1, 2, 3]) // '1,2,3'
String([1, [2], [3, [4, [5, 6]]]]) // '1,2,3,4,5,6'
String({a: 1}) // '[object Object]''
String(对象)
具体规则:
toString
,若返回值是基本数据类型,则再使用String
,不再进行后续步骤;valueOf
,若返回值是基本数据类型,则再使用String
,不再进行后续步骤;TypeError: Cannot convert object to primitive value
。Boolean(参数)
数据类型 | 转换为true 的值 |
转换为false 的值 |
---|---|---|
Undefined | 无 | undefined |
Boolean | true |
false |
Number | 任何非零Number 类型的值(包括无穷大) |
0 、-0 、+0 、NaN |
String | 任何非空String 类型的值 |
'' |
Symbol | 任何Symbol 类型的值 |
无 |
BigInt | 任何非零BigInt 类型的值 |
0n 、-0n 、+0n |
Object(引用数据类型) | 任何对象(包括基本包装类型) | null |
自动转换
触发情况:
对非数值类型的数据使用一元运算符(+
、-
)
行为:
预期什么类型的值,就调用该类型的转换函数。
自动转换为Boolean
:
if
、while
、for
等条件语句。条件运算 ? 表达式1 : 表达式2
。!!条件运算
。自动转换为String
:
主要发生在加法运算时:当一个值为字符串,另一个值为非字符串,则后者转为字符串。
自动转换为Number
:
除了加法运算符有可能把运算数转为字符串,其他运算符都会把运算数自动转成数值(包括一元运算符)。
+
加法运算:
Symbol
不能进行加法运算;BigInt
只能和BigInt
、字符串、对象 进行加法运算。
ToPrimitive(对象)
转换为的基本数据类型,再按照下面方式继续自动转换或运算出结果;String(基本数据类型)
)进行;true
-> 1、false
-> 0)进行。==
与!=
进行的强制类型转换步骤
运算数若存在NaN
,则返回false
;
除了
!=
和!==
,只要对比(==
、===
、>
、>=
、<
、<=
)的操作数有一个是NaN
,则返回false
。
布尔值
,将布尔转换为数字(true
-> 1、false
-> 0);运算数若存在字符串
, 有三种情况:
对象
,则进行ToPrimitive(对象, 'string')
后进行对比;数字
,字符串转数字;字符串
,直接比较;false
。数字
与对象
,则对象进行ToPrimitive(对象, 'number')
,再比较;对象
,则比较它们的引用值
(若两个运算数指向同一对象,则返回true
,否则false
);null
与undefined
不会进行类型转换, 但相等。
ToPrimitive
:依次调用对象的valueOf
、toString
,将对象转化了基本数据类型。{ toString: () => {console.log('toString');return {}}, valueOf: () => {console.log('valueOf');return {}} }
对象来判断先调用哪个属性。
||
和&&
expr1 || expr2
:
赋值操作:若expr1能转换成true(Boolean(expr1)
)则返回expr1,否则返回expr2。
若多个||
,则取第一个为true的值(Boolean(expr)
)或最后一个值。
在Boolean环境(如:if的条件判断)中使用:两个操作结果中只要有一个为true,返回true;二者操作结果都为false时返回false。
expr1 && expr2
:
赋值操作:若expr1能转换成false(Boolean(expr1)
)则返回expr1,否则返回expr2。
若多个&&
,则取第一个为false的值(Boolean(expr)
)或最后一个值。
在Boolean环境(如:if的条件判断)中使用:两个操作结果都为true时返回true,否则返回false。
参考:阮一峰:再谈Event Loop、Help, I’m stuck in an event-loop.、Tasks, microtasks, queues and schedules。
不是在ECMAScript中定义,而是在HTML Standard中定义。
JS的主线程是单线程
原因:
结果:
弥补单线程计算量太大、事件耗时太久影响浏览器体验:
新增Web Worker标准,但不能操作DOM,完全受主线程控制。
- Worker与主线程通信,都通过
postMessage
传递信息、通过监听message
事件接受信息,信息通过结构化克隆算法(一种深复制)复制后传递。- 在浏览器tab没有被激活时(inactive),计时器间隔的最小值会提升、进程执行会变慢。用
Web Worker
不会受浏览器是否激活的影响。
多个异步线程分别处理:网络请求、定时器、读写文件、I/O设备事件、页面渲染等。
DOM的变动(尤其是涉及页面重新渲染的部分),通常不会立即执行,而是每16毫秒执行一次。
任务类型
同步任务(synchronous):
在主线程上排队执行的任务。
异步任务(asynchronous):
先挂起到(对应种类的)工作线程等待结果,主线程不会因此阻塞;有结果后,发起通知进入(对应种类的)「任务队列」(task queue);「执行栈」(execution context stack)为空后会读取并执行「任务队列」的通知对应的回调函数。
相同类型的异步工作线程是串行工作;不同类型的异步工作线程互不影响执行。
异步任务分为两种类型:macrotask(task)、microtask(job)
macrotask一般包括:
I/O
监听的事件。
requestAnimationFrame
新的<script>
setTimeout
、setInterval
、setImmediate
在当前「任务队列」的尾部,添加事件。
AJAX
microtask一般包括:
process.nextTick
(Node.js)
在当前「执行栈」的尾部——读取”任务队列”之前,添加事件。
Promise
(Promise.prototype.then/catch/finally(回调)
的回调)
new Promise(回调)
的回调、Promise.all/race/any/allSettled([回调])
的回调、Promise.resolve/reject()
,都是同步任务。
async-await
(只有await
是异步)
await
后若是方法则同步执行该方法,执行结果交给await
后才是microtask(无论结果如何)。
e.g.
```javascript async function func () { await // 同步执行,执行完无论未完成/完成/失败,都异步microtask执行后面代码 new Promise((resolve) => { console.log('a') // 同步 resolve() }) .then(() => {console.log('then1')}) // 异步microtask .then(() => {console.log('then2')}) // 异步microtask console.log('b') // 等待await完成(microtask) } func() console.log('c') // => a c then1 then2 b ``` ```javascript var obj = { a () { console.log('a') return obj }, b () { console.log('b') } } async function func () { await // 同步执行,执行完无论未完成/完成/失败,都异步microtask执行后面代码 obj.a() // 同步 .b() // 同步 console.log('d') // 等待await完成(microtask) } func() console.log('c') // =>a b c d ``` ```javascript var obj = { a () { console.log('a') return obj }, b () { console.log('b') return Promise.resolve() } } async function func () { await // 同步执行,执行完无论未完成/完成/失败,都异步microtask执行后面代码 obj.a() // 同步 .b() // 同步 .then(() => {console.log('then1')}) // 异步microtask .then(() => {console.log('then2')}) // 异步microtask console.log('d') // 等待await完成(microtask) } func() console.log('c') // =>a b c then1 then2 d ```
for-await-of
的方法体执行和迭代器遍历MutationObserver
macrotask、microtask的事件循环运行机制:
检查macrotask队列:
选择最早加入的任务X,设置为「目前运行的任务」并进入「执行栈」;若macrotask队列为空,则跳到第4步;
最初时刻:「执行栈」为空,
<script>
作为第一个macrotask被运行。
null
,从macrotask队列中移除任务X;检查microtask队列:
null
,从microtask队列中移除任务a;
macrotask和microtast选择
若想让一个任务立即执行,则把它设置为microtask,除此之外都用macrotask。因为虽然JS是异步非阻塞,但在一个事件循环中,microtask的执行方式基本上是用同步的。 - 空闲任务(线程空闲时才会执行,优先级非常低):
requestIdleCallback
IntersectionObserver
JS的事件循环运行机制:
「执行栈」进行:
一旦「执行栈」中的所有同步任务执行完毕,系统就会(按照macrotask、microtask的事件循环运行机制)读取「任务队列」,把一个通知对应的回调函数加入执行栈。跳回步骤1(「执行栈」又有内容可以执行)。
由于异步函数是立刻返回,异步事务中发生的错误是无法通过外层的:try-catch
、Promise
、async-await
来捕捉(但可以捕获await
的reject
)。
每个事件循环都是一个独立的调用栈,不同调用栈之间无法互相try-catch
错误,所以新的事件循环不会被原调用栈捕获错误,导致向上抛出错误(若新的事件循环内没有在自己的调用栈上设置try-catch
)。
try {
setTimeout(()=>{
错误 // 异步的无法捕获,错误会向全局抛
})
} catch (e) {}
try {
Promise.resolve().then(() => { 错误 }) // 异步的无法捕获,错误抛给Promise变成未捕获的失败Promise实例,再向全局抛出
} catch (e) {}
new Promise((resolve, reject) => {
Promise.resolve().then(() => { 错误 }) // 异步的无法捕获,错误抛给Promise变成未捕获的失败Promise实例,再向全局抛出
}).catch(()=>{})
async function func1 () {
Promise.resolve().then(() => { 错误 }) // 异步的无法捕获,错误抛给Promise变成未捕获的失败Promise实例,再向全局抛出
}
func1().catch(()=>{})
async function func2 () {
await Promise.resolve().then(() => { 错误 }) // 错误可以被func2捕获
}
func2().catch(()=>{})
try-catch
,或Promise实例后添加.catch
JS的异步编程方式:
定时器(setInterval
、setTimeout
)
不适合制作动画。
定时器触发后,会把定时器处理程序(回调函数)插入至等待执行的任务队列最后面。setInterval
和setTimeout
的内部运行机制完全一致。
setInterval/clearInterval
:
setTimeout/clearTimeout
:
用setTimeout模拟setInterval,可以提高性能(上一次执行完毕后延迟时间再执行下一次,而不是固定间隔时间都尝试执行),并且可以确保每次定时器处理程序执行间隔一定大于(或等于)设置的间隔时间。
setImmediate/clearImmediate
:
主流浏览器不支持,仅ie10+、Node.js支持。
大致类似于:setTimeout(func, 0)
。
setInterval
、setTimeout
有最短延迟时间、最长延迟时间:
- 最短延迟时间大概是>4ms(从其他地方调用
setInterval
,或在嵌套函数达到特定深度时调用setTimeout
)。- 若处于未激活窗口,则最短延迟时间可能扩大到>1000ms。
- 有最大延迟时间,若延迟时间设置>
Math.pow(2, 31) - 1
ms,则延时溢出导致立即执行回调函数。
重绘函数(requestAnimationFrame
)
递归调用。
浏览器重绘之前,进行一次:发起通知进入「任务队列」,等待「执行栈」调用。
大部分浏览器是1秒钟60帧,也就是16.67ms进行一帧重绘。
替代执行时机无法保证的setTimeout
、setInterval
进行动画操作,提升渲染性能:
空闲函数(requestIdleCallback
、cancelIdleCallback
)
在浏览器空闲时期依次调用函数。
- 只有当前帧的运行时间小于16.66ms时,回调函数才会执行。否则,就推迟到下一帧,若下一帧也没有空闲时间,则推迟到下下一帧,以此类推。
- 第二个参数表示指定的毫秒数。若在指定的这段时间之内,每一帧都没有空闲时间,那么回调函数将会强制执行。
Object.prototype.toString.call(值);
(或apply
)
放入内置对象,返回'[object 构造函数的名称]'
的字符串
特例:自定义类型返回
'[object Object]'
,undefined/null
返回对应名字。
undefined
或 不填 -> '[object Undefined]'
null
-> '[object Null]'
'[object Boolean]'
'[object Number]'
'[object String]'
'[object Symbol]'
'[object BigInt]'
'[object Object]'
'[object Object]'
'[object Array]'
'[object Function]'
'[object Date]'
'[object RegExp]'
'[object Error]'
'[object Map]'
'[object Set]'
'[object WeakMap]'
'[object WeakSet]'
'[object Promise]'
'[object GeneratorFunction]'
window
-> '[object Window]'
document
-> '[object HTMLDocument]'
参考:MDN:HTML 元素接口。
继承document
的各种DOM,返回'[object HTML继承类Element]'
。
'[object HTMLCollection]'
DOM.childNodes
或document.querySelectorAll
等返回) -> '[object NodeList]'
arguments
-> '[object Arguments]'
Math
-> '[object Math]'
JSON
-> '[object JSON]'
WebAssembly
-> '[object WebAssembly]'
'[object 构造函数名]'
'[object ArrayBuffer]'
'[object DataView]'
对于没有声明的变量,直接使用会报引用不存在变量的错误,可以用
`if (typeof 变量 !== 'undefined' && Object.prototype.toString.call(变量) === '[object 某]') {}`typeof
来使代码健壮
typeof 值
返回一个表示值类型的字符串。
'string'
'boolean'
'number'
'symbol'
'bigint'
undefined
-> 'undefined'
函数 -> 'function'
包括:类。
引用对象型 -> 'object'
所有基本包装类型都返回
'object'
。
null
-> 'object'
- 因为
typeof null
返回'object'
,因此typeof不能判断是否是引用数据类型。- ie8-的DOM节点的方法返回不是
function,而是object
,因此只能用方法名 in DOM
检测DOM是否拥有某方法。
对象 instanceof 构造函数或类
可用
构造函数或类.prototype.isPrototypeOf(对象)
代替。
不能跨帧(<iframe>
、window.open()
的新窗口)。
/* 跨帧:浏览器的帧(frame)里的对象传入到另一个帧中,两个帧都定义了相同的构造函数或类 */ A实例 instanceof A构造函数或类; // true A实例 instanceof B构造函数或类; // false
判断构造函数或类.prototype
是否存在于对象的整条原型链([[Prototype]]
)上。
若构造函数或类.prototype === 对象.__proto__/对象.__proto__.__proto__/.../Object.prototype
,则返回true
。否则返回false
。
e.g.
new Number() instanceof Object; // true
检测自定义类型的唯一方法。
属性名 in 对象
判断对象或对象的整条原型链([[Prototype]]
)上是否拥有属性名,不读取属性值。
对象.hasOwnProperty(属性名)
仅检查在当前实例对象,不检测其原型链。
对象.hasOwnProperty(属性名)
建议代替用:Object.prototype.hasOwnProperty.call(对象, 属性名)
。ie8-的DOM对象并非继承自Object对象,因此没有hasOwnProperty方法。
queryObjects(构造函数)
,返回当前执行环境的构造函数创建的所有实例。约定
`obj`为对象实例,`arr`为数组实例。
continue
应用在循环(while
、do-while
、for
、for-in
、for-of
),表示跳过当次循环;break
应用在循环、switch
,表示跳出整个循环。forEach
、map
、filter
、some
、every
无法中止循环(return
只结束回调函数)。$.each/$dom.each
跳出循环用return true
(功能等价于:continue
)、return false
(功能等价于:break
)。
尽量不要在for-in
、for-of
、Array遍历方法中改变原数组项或值
Array.prototype.filter
。Array.prototype.reduce
(最方便)或for
(需要创建一个新变量保存结果,可能性能好些)。原生JS
遍历对象的属性
Object.entries/values/keys/getOwnPropertyNames/getOwnPropertySymbols
、for-in
、JSON.stringify
、Reflect.ownKeys
可以获得属性描述(数据属性、访问器属性)。
e.g.
```javascript const obj = { a: 'obj\'s a', [Symbol('b')]: 'obj\'s b' } Object.defineProperties(obj, { c: { value: 'obj\'s c', enumerable: true }, d: { value: 'obj\'s d', enumerable: false }, [Symbol('e')]: { value: 'obj\'s e', enumerable: true }, [Symbol('f')]: { value: 'obj\'s f', enumerable: false } }) const arr = ['arr\'s a'] arr[Symbol('b')] = 'arr\'s b' Object.defineProperties(arr, { c: { value: 'arr\'s c', enumerable: true }, d: { value: 'arr\'s d', enumerable: false }, [Symbol('e')]: { value: 'arr\'s e', enumerable: true }, [Symbol('f')]: { value: 'arr\'s f', enumerable: false } }) // 输出:可枚举 && !属性名是Symbol类型 console.info('\n for-in') // for-in还会遍历整条原型链上的属性:可枚举 && !属性名是Symbol类型。 for (const i in obj) { console.log(i, obj[i]) } for (const i in arr) { console.log(i, arr[i]) } console.info('\n Object.entries') console.log(Object.entries(obj)) console.log(Object.entries(arr)) console.info('\n Object.values') console.log(Object.values(obj)) console.log(Object.values(arr)) console.info('\n Object.keys') console.log(Object.keys(obj)) console.log(Object.keys(arr)) // 输出:(可枚举 || 不可枚举) && !属性名是Symbol类型 console.info('\n Object.getOwnPropertyNames') console.log(Object.getOwnPropertyNames(obj)) console.log(Object.getOwnPropertyNames(arr)) // 输出:属性名是Symbol类型 console.info('\n Object.getOwnPropertySymbols') console.log(Object.getOwnPropertySymbols(obj)) console.log(Object.getOwnPropertySymbols(arr)) console.info('\n JSON.stringify') // 输出:可枚举 && !属性名是Symbol类型 console.log(JSON.stringify(obj)) // 输出:数组的项(当做对象添加的属性不返回) console.log(JSON.stringify(arr)) console.info('\n ...展开') // 输出:可枚举 console.log({ ...obj }) // 输出:数组的项(当做对象添加的属性不返回) console.log([...arr]) // 输出:所有键名 console.info('\n Reflect.ownKeys') console.log(Reflect.ownKeys(obj)) console.log(Reflect.ownKeys(arr)) console.info('\n Map数据类型的遍历') // 输出:Map的所有项 for (const i of new Map([['a', 'Map\'s a'], [Symbol('b'), 'Map\'s b']])) { console.log(i) } ```
while
、do-while
for
for-in
for-of
```javascript var asyncFunc = (ms = 1000) => new Promise((resolve) => setTimeout(resolve, ms)) // 模拟异步操作 async function func () { console.log('start') for (const i of [1, 2]) { // while、do-while、for、for-in、for-of 同理 await asyncFunc() // 一项完成才会进行下一项,全部完成才会执行for之后代码。 console.log(i) } console.log('end') } func() console.log('outside') // => start => outside =>(异步) => 1 => 2 => end ```
while
、do-while
、for
、for-in
、for-of
配合async-await
可以保证内部、外部的执行顺序按照预想结果进行。
Array方法
若在Array遍历的回调函数中改变原数组,则会影响遍历的项和顺序。且任何时候直接使用原数组会展示修改后的当前值
```javascript // e.g. var originArr = ['a', 'b', 'c'] originArr.forEach((item, index, arr) => { // 或其他所有Array.prototype.遍历方法 originArr.pop() console.log(index, arr, originArr) // => 0 ['a', 'b'] ['a', 'b'] => 1 ['a'] ['a'] }) ```
参数均为:回调函数(当前值, 索引, 数组整体)[, this替代]
。
Array.prototype.forEach()
对数组的每个元素执行一次提供的函数。
Array.prototype.map()
数组中的每个元素调用提供的函数,组成新的数组。
Array.prototype.filter()
使用提供的函数测试所有元素,并创建包含所有通过测试的元素的新数组。
Array.prototype.every()
测试数组中是否所有元素都通过提供的函数。
Array.prototype.some()
测试数组中是否有一个元素通过提供的函数。
Array.prototype.find()
查找数组中通过提供的函数的第一个元素。
Array.prototype.findIndex()
查找数组中通过提供的函数的第一个元素的索引。
向后/向前对数组应用提供的函数,累计处理返回最后一个结果
Array.prototype.reduce(回调函数(上一次调用返回的值, 当前值, 索引, 数组整体)[, 第一次调用回调函数的第一个参数])
Array.prototype.reduceRight(回调函数(上一次调用返回的值, 当前值, 索引, 数组整体)[, 第一次调用回调函数的第一个参数])
Array方法配合async-await
(或Promise
)的执行顺序:
Array遍历方法内的每一项按项的顺序执行但不互相依赖(后面的项不会等前面的执行完毕才开始执行),async-await
只能处理每一项自己方法内的执行顺序。
控制每一项之间的执行细节(并行、串行、等):async。
Array遍历方法没有返回Promise实例,因此直接在外部加上await
无法达到预期效果(内部加上async-await
仅影响单一项目的执行,不影响外部和其他项)。
改用while
、do-while
、for
、for-in
、for-of
等实现异步效果。
e.g.
```javascript var asyncFunc = (ms = 1000) => new Promise((resolve) => setTimeout(resolve, ms)) // 模拟异步操作 function func () { console.log('start'); ([1, 2]).forEach(async (item) => { // forEach、map、filter、every 同理 console.log('array start', item) await asyncFunc() console.log('array end', item) }) console.log('end') } func() console.log('outside') // => start => array start 1 => array start 2 => end => outside (异步) => array end 1 => array end 2 ```
jQuery
$.each
$dom.each
$.grep
类似Array.prototype.filter
浏览器同源策略(协议、域名、端口号,必须完全相同)限制
- 当一个资源从非同源中请求资源时,资源会发起一个跨域HTTP请求。出于安全原因,**浏览器**限制:从**脚本**内发起的跨源HTTP请求 或 拦截跨域HTTP响应。 1. 跨域请求(XMLHttpRequest、Fetch等)不一定是浏览器限制了发起跨站请求,也可能是跨站请求可以正常发起,但返回结果被浏览器拦截。 2. 现代浏览器会进行CORS处理,发起请求并与服务器协商(特例:有些浏览器不允许从HTTPS跨域访问HTTP,在请求还未发出时就拦截请求)。低版本浏览器可能不会发起跨域请求,直接拒绝发起请求。
CORS(服务端需要支持)
- 不受同源政策限制。
- ie9-的jQuery的非jsonp的AJAX跨域,要添加
jQuery.support.cors = true
。
jsonp(服务端需要支持)
- 不受同源政策限制。
- 只支持GET请求。
网页通过添加一个<script>
,向服务器发起文档请求(不受同源政策限制);服务器收到请求后,将数据放在一个指定名字的回调函数里传回网页直接执行。
jQuery在它的
```javascript $.ajax({ url: '接口地址', dataType: 'jsonp', jsonp: '与服务端约定的支持jsonp方法', // 前端唯一需要额外添加的内容 data: {}, success: function (data) { // data为跨域请求获得的服务端返回数据 } }) ```AJAX
方法中封装了jsonp
功能
不同源的文档间(文档与
document.domain
相同则可以文档间互相操作
把不同文档的document.domain
设置为一致的值(仅允许设置为上一级域),即可双向通信、互相操作(cookie
可以直接操作;localStorage
、IndexDB
只能通过postMessage
通信)。
与<iframe>
通信:
// 父窗口调用`<iframe>`的window对象
var newIframe = document.getElementById('new-iframe').contentWindow; // 或:window.frames[0]
// `<iframe>`调用父窗口的window对象
var father = parent;
与window.open()
的新窗口通信:
// 父窗口调用新打开窗口的window对象
var newWin = window.open('某URL');
// 新打开窗口调用父窗口的window对象
var father = window.opener;
postMessage
文档间通信
- 不受同源政策限制。
- ie8、ie9仅支持与
<iframe>
,ie10+支持与<iframe>
、window.open()
的新窗口。
// 发送方(允许自己发给自己接受)
目标window对象.postMessage(信息内容, '目标源地址或*');
// 监听的文档
window.addEventListener('message', function (e) {...}, false) // e.data === 信息内容
其他方式
父窗口改变<iframe>
的hash,<iframe>
通过监听hash变化的hashchange
事件获取父窗口信息
- 不受同源政策限制。
- ie8+支持。若只改变hash值,页面不会重新刷新。
// 父窗口改变`<iframe>`的hash值
document.getElementById('new-iframe').src = '除了hash值,url不变';
// `<iframe>`窗口监听hash变化,以hash变化当做信息的传递
window.onhashchange = function () {
var message = window.location.hash;
// ...
};
通过监听window.name
传递信息
同会话(tab窗口)前后跳转的页面都可以读取、设置同一个
window.name
值。
window.name
属性。window.name
遵循同源策略)。window.name
值作为信息的传递。图片地址
只能发送GET请求,无法访问服务器的响应文本。只能浏览器向服务器单向通信。
常用来统计。
通过代理服务器转发请求
通过成功访问代理服务器,利用代理服务器转发请求获得数据后(同源策略仅限于浏览器)再返回给浏览器。
解决浏览器显示
Script error
错误:①引用资源添加<script src="CDN地址" crossorigin="anonymous"></script>
,②CDN地址资源响应头包含Access-Control-Allow-Origin: 请求头的Origin值 或 *
。
- 因为HTTP请求都会携带cookie,因此cookie最好仅用于服务端判定状态。
- 浏览器数据存储方式:变量、cookie、Web Storage、IndexedDB、
Web SQL、、Service Workers。<html>
的Manifest
Web Storage(localStorage
、sessionStorage
)
toString
方法)。ie8+支持(ie及FF需在web服务器里运行)。
ie6/7可以用它们独有的
UserData
代替使用。
拥有方便的API
同步。
调用localStorage
、sessionStorage
对象会为每个源(每个tab)创建独立的Storage
对象,每个对象都拥有:
setItem
、getItem
、removeItem
、clear
、key
方法,length
属性。没有索引功能。
window
的storage
事件,会在其他tab的同源页面修改localStorage
值时触发(增、删、改,setItem
相同值时不触发)。
区别
localStorage
sessionStorage
cookie
客户端保存(JS添加或响应头设置),始终在HTTP请求中携带(同源同路径,或父域名、父路径),明文传递,服务端接收、操作客户端cookie。
- cookie中的
domain
(默认:当前域名),可设置为父域名或当前域名,不能设置为其他域名(设置失效)。- 当前域名可以访问
domain
为当前域名或父域名的cookie;浏览器发起HTTP请求时会向请求地址发送与请求域名相同或是其父域名的cookie。
名1=值1[; 名2=值2]
。不能包含任何,
、;
、
(使用encodeURIComponent/decodeURIComponent
处理属性名和属性值)。源生的cookie接口不友好,需要自己封装操作cookie。
同步。
JS的document.cookie
:
新建或更新cookie(一条命令只能设置一条cookie)类似于服务端Set-Cookie
响应头(一条命令可以设置多条cookie):
名=值[; expires=绝对时间(时间戳)][; max-age=相对时间(秒)][; domain=域名][; path=路径][; secure][; samesite=Strict或Lax][; priority=low或medium或high]
Set-Cookie
可以额外设置HttpOnly
属性;客户端不能设置、也不能查看和操作被设置为HttpOnly
的cookie。- 非安全站点(HTTP)不能在cookie中设置
secure
,若设置,则此条新建或更新cookie无效。priority
是Chrome的提案。
读取cookie等同于客户端Cookie
请求头:
展示所有cookie名1=值1[; 名2=值2]
(无法查看其他信息)。
cookie的名称
、domain
(默认当前完整域名)、path
(默认当前路由的path)一同唯一确定一个cookie,因此删除cookie项,需要关注这3个属性。
没有改变,只能轮询检测。cookie
的事件通知
同源同路径,或父域名、父路径 共享。
子域名可以访问(获取/修改/删除)主域名的cookie。如:
a.b.c.com
可以获取a.b.c.com
、b.c.com
、c.com
的cookie,但无法获取d.a.b.c.com
或d.com
的cookie。
僵尸cookie(zombie cookie)
是指那些删不掉的,删掉会自动重建的cookie。僵尸cookie是依赖于其他的本地存储方法(如:Flash的share object、HTML5的local storages等),当用户删除cookie后,自动从其他本地存储里读取出cookie的备份,并重新种植。
IndexedDB
所有类型的数据都可以直接存入。
异步。支持事务。
复杂的API。
隐身模式策略:存储API仍然可用,并且看起来功能齐全,只是无法真正储存(如:分配储存空间为0)。
- 当JS出现错误时,JS引擎会根据JS调用栈逐级寻找对应的
catch
(每个异步任务会创建各自独立的调用栈,不同调用栈之间不能互相catch
),若没有找到相应的catch handler或catch handler本身又有error或又抛出新的error,则会把这个error抛给全局(浏览器会用各自不同的方式显示错误信息,可以用window
的error
进行自定义操作)。- 在某个JS block(
<script>
或try-catch
的try
语句块)内,第一个错误触发后,当前JS block后面的代码会被自动忽略,不再执行,其他的JS block内代码不被影响。
原生错误类型
来自:MDN:Error。
错误类型:Error
、EvalError
、RangeError
、ReferenceError
、SyntaxError
、TypeError
、URIError
。
自定义错误
// ES5
function MyError(message) {
this.stack = (Error.call(this, message)).stack;
this.message = message || '默认信息';
this.name = 'MyError';
}
MyError.prototype = Object.create(Error.prototype, {constructor: {value: MyError}});
// ES6的class-extends
class MyError extends Error{}
手动抛出错误
throw 'Error' // 抛出字符串
throw 100 // 抛出数值
throw true // 抛出布尔值
throw { message: 'An Error' } // 抛出对象
throw new Error('An Error') // 抛出Error类型错误,参数是字符串
处理代码中抛出的错误
语法错误无法被各种方式捕获。
try-catch-finally
try-catch
或try-finally
或try-catch-finally
同时出现。catch
,则一旦try
中抛出错误以后就先执行catch
中的代码,然后执行finally
中的代码。catch
,try
中的代码抛出错误后,则先执行finally
中的语句,然后将try
中抛出的错误往上抛。try
中代码是以return
、continue
或break
终止的,则必须先执行完finally
中的语句后再执行相应的try
中的返回语句。catch
中接收的错误,不会再向上提交给浏览器。
try-catch
能捕获其中所有同步代码,同步调用外部的方法也包含。
try { setTimeout(() => { 错误 }, 0) } catch (e) {}
不会捕获异步操作中的错误(同理,在Promise
或async-await
等语法中的异步错误也无法被捕获,但可以捕获await
的reject
)。可以在异步回调内部再包一层try-catch
、或用window
的error
事件捕获。
window
的error
事件
try-catch
处理的错误都会触发window
的error
事件。传参
window.onerror
方法会传入多个参数:message
、fileName
、lineNumber
、columnNumber
、errorObject
。window.addEventListener('error', (event) => {})
只会传入一个参数。去掉控制台的异常显示
window.onerror
,若方法返回true
,则浏览器不再显示错误信息。window.addEventListener('error', (event) => {})
,若回调函数调用event.preventDefault()
,则浏览器不再显示错误信息。 window.addEventListener('error', (event) => {
/* code */
event.preventDefault() // 浏览器不再显示错误信息
})
/**
* window错误处理
* @param {String} msg - 错误信息提示
* @param {String} url - 错误出现url
* @param {Number} line - 错误出现行数字
* @param {Number} column - 错误出现列数字
* @param {Object} error - 错误对象
* @returns {Boolean} - true:不显示错误信息|false:显示
*/
window.onerror = function (msg, url, line, column, error) {
/* code */
return true // 浏览器不再显示错误信息
}
资源加载错误的监听
可以监听各资源的错误情况,包括:<script>
、<link>
、<img>
、媒体资源、等。
<iframe>
需要同源才能够监听事件。
window.addEventListener('error', function (event) { // 包含上面的:没有经过`try-catch`处理的错误都会触发`window`的`error`事件
// event.target 错误资源的DOM
}, true) // 必须要捕获
window
的unhandledrejection
事件
若失败的Promise实例未被处理,则触发window
的unhandledrejection
事件。
window.addEventListener('unhandledrejection', (event) => {
/* code */
event.preventDefault() // 浏览器不再显示错误信息
})
window.onunhandledrejection = function (event) {
/* code */
event.preventDefault() // 浏览器不再显示错误信息。或:return false
}
图像的onerror
事件
- 只要图像的src属性中的URL不能返回能被识别的图像格式,就会触发图像的
error
事件。- 错误不会提交到
。window
的error
Image
实例或<img>
的error
事件没有任何参数。
<img>
的error
事件
<img src="错误地址" onerror="func();">
Image
实例的属性
var img = new Image(); // 或document.createElement('img');
img.onerror = function () {
/* code */
};
img.src = '错误地址';
运用策略
window
的error
主要捕获预料之外的错误;try-catch
则用来在可预见情况下监控特定的错误。两者结合使用更加高效。window
的unhandledrejection
主要捕获未被处理的 失败的Promise实例;Promise.prototype.catch或then第二个参数
则用来在可预见情况下处理 失败的Promise实例。
非客户端页面
仅需在加载JS之前配置好window
的error
。
客户端内嵌页面
window
的error
。客户端回调函数嵌套一层try-catch
,提示哪个方法发生错误等额外信息。
因为Native调用WebView的方法是直接通过函数运行JS代码,抛出错误时
window.onerror
传入的参数仅有第一个message
参数。
try-catch
(服务端代码中添加),提示哪个方法发生错误等额外信息。捕获错误的目的在于避免浏览器以默认方式处理它们;而抛出错误的目的在于提供错误发生具体原因的消息。
尽量避免静默错误(try-catch
的catch
没有任何处理,也没有任何提示)
若添加try-catch
捕获了错误,在catch
中:要不然进行新的逻辑、要不然要把错误暴露出来。如:在catch
中添加console
或上报错误或其他方式能让开发者感知到出错了。
预加载,但不影响页面渲染,等待相关资源真正需要被使用时直接使用已预加载的资源 或 更快速的建立连接。
<link>
预加载
<link rel="preload" href="资源">
高优先级、页面渲染前。
请求、下载、缓存资源。
兼容性较差:
<link rel="prefetch" href="资源">
利用浏览器空闲时间去下载或预取用户在不久的将来可能访问的文档。
请求、下载、缓存资源。
<link rel="prerender" href="域名">
就像在后台打开了一个隐藏的tab,下载域名的所有资源、创建DOM、渲染页面、执行JS等等。
<link rel="dns-prefetch" href="域名">
解析DNS。
<link rel="preconnect" href="域名">
解析DNS,建立TCP握手连接、TLS协议。
实现预加载图片
JS
var img = new Image(); // 或document.createElement('img');
img.src = '图片地址';
img.onerror = function () {
console.log('加载失败');
};
if (img.complete) {
console.log('缓存');
} else {
img.onload = function () {
console.log('新加载');
};
}
CSS、HTML
background-image
当不触发页面渲染则不会加载,否则加载。
如:
display: none;
不会加载;opacity: 0;
或visibility: hidden/collapse;
等,会加载。
<img>
无论CSS设置什么,都会加载。
e.g. 预加载字体、图片、音频、视频,并显示进度条
```vueloading:/ 0123456789 <img v-for="img of imgArray" :key="img" :src="img" @load="loadHandler" > <audio v-for="audio of audioArray" :key="audio" :src="audio" preload="auto" @loadedmetadata="loadHandler" /> <video v-for="video of videoArray" :key="video" :src="video" preload="auto" @loadedmetadata="loadHandler" />```
判断对象方法是否可以执行
/* 对象已经定义 && 对象不等于null && 对象方法存在 */
if (typeof obj !== 'undefined' && obj !== null && typeof obj.func === 'function') {
/* 对象方法已定义 可执行 */
}
判断全局对象方法是否可以执行
/* window的子对象存在 && 对象方法存在 */
if (window.obj && typeof window.obj.func === 'function') {
/* 对象方法已定义 可执行 */
}
判断是否需要重新定义
/* 对象不存在 || 对象等于null || 对象方法不存在 */
if (typeof obj === 'undefined' || obj === null || typeof obj.func !== 'function') {
/* 对象或对象方法没有定义 需重新定义 */
}
变量已定义
/* 变量已定义 && 变量不等于null */
if (typeof a !== 'undefined' && a !== null) {
/* 对象已定义 可操作 */
}
数据缓存
因为保存在客户端,因此都不应该保存用户的隐私数据。
Web Storage(localStorage
、sessionStorage
)、cookie、IndexDB、等。
其他缓存机制(不推荐)
HTML的<meta>
设置缓存情况:
e.g. 设置不缓存:
<meta http-equiv="pragma" content="no-cache">
<meta http-equiv="cache-control" content="no-cache">
<meta http-equiv="expires" content="0">
<html>
的manifest
应用程序缓存:
<html manifest=".manifest文件/.appcache文件">
来自:Javascript代码压缩细节。
试着生成新的代码,对比后输出最短的内容。
压缩表达式
表达式预计算
将可预先计算的表达式替换成其计算结果,并比较原来表达式与生成后的结果的大小,取短的。
优化true/false
true
==/!=
运算 -> 1
!0
false
==/!=
运算 -> 0
!1
优化&&/||
true && 表达式
-> 表达式
false && 表达式
-> !1
true || 表达式
-> !0
false || 表达式
-> 表达式
缩短运算符
===/!==
的两个操作数都是String
类型或都是Boolean
类型的,缩短成==/!=
。缩短赋值表达式
参考:MDN:赋值运算符。
对于类似a = a + b
的赋值表达式(+
-
*
/
%
**
&&
||
??
>>
>>>
<<
&
|
^
),可以缩短成a += b
。
- 位运算:
>>
符号传播右移、>>>
无符号右移/零填充右移、<<
左移、|
按位或、&
按位与、^
按位异或。- 逻辑复制:逻辑与赋值
x &&= y
运算仅在x为真值时为其赋值;逻辑或赋值x ||= y
运算仅在x为假值时为其赋值;逻辑空赋值运算符x ??= y
仅在x是空值undefined/null
时对其赋值。
!
操作符的压缩
对于!(a>=b)
,若转换后a<b
得到更短的代码,则转换。
去除没用的声明
"use strict"
。break
。label
。toString
调用。压缩while
while
,如:while(false){}
。while(true){}
-> for(;;){}
条件判断 ? 表达式1 : 表达式2
条件判断
有!
,则去除!
且调换表达式前后位置。条件判断
为常数,则直接缩短为某一个表达式。压缩语句块
,
。var
声明可以压缩成一个var
声明。return
之后的非变量声明、非函数声明可以去除。return
语句及其前边的多条表达式语句。优化if
if/else
分支。if/else
分支,看看生成代码是否更短。if
块里边仅有一个if
语句,且else
块为空,则可以合并这两个if
。if
最后一个语句是跳出控制语句,则可以把else
块的内容提到else
外边,然后去掉else
。if/else
里各仅有一条return
语句,则可以合并这两句return
。if/else
里各仅有一条语句,则可以转换为三元运算符表达式。if/else
其中一个块为空,另一个块仅有一条语句,则可以转化成||/&&
表达式。参考:JavaScript混淆安全加固。
减少加密的成本、增加破解的成本,「当你采用的加密模式,使得攻击者为了破解所付出的代价 远远超过其所获得的利益之时,你的加密方案就是安全的」。
声明
因为JS的预编译。
变量声明(var
)
无论语句在何处,无论是否会真正执行到,所有的var
语句的声明都提升到作用域(函数体内或全局)顶部执行(hoisting),但具体赋值不会被提前。
e.g.
```javascript (function () { console.log(a); // undefined var a = 2; console.log(a); // 2 })(); // 等价于: (function () { var a; console.log(a); // undefined a = 2; console.log(a); // 2 })(); ```
建议:总是把所有变量声明都放在函数顶部,而不是散落在各个角落。
函数声明
函数表达式(Function expressions)声明
必须先声明:var a = function () {...};
才可以使用,声明会被提前,但赋值不会被提前。
e.g.
```javascript foo() // TypeError: foo is not a function bar() // ReferenceError: bar is not defined var foo = function bar () {} ```
同名的变量声明和函数声明(不是函数表达式)
同时声明的情况下(顺序不影响结果):
原因:
var
被忽略,重复声明的函数funtion
会覆盖之前同名的函数声明。e.g.
```javascript var a1 = 1; function a1() {} console.log(a1); // 1 function b2() {} var b2 = 1; console.log(b2); // 1 var c3; function c3() {} console.log(c3); // function function d4() {} var d4; console.log(d4); // function ```
建议:先声明再使用;把函数声明紧接着放在变量声明之后。
严格模式use strict
可用于全局,也可以用于局部(函数体内)。
- 不推荐在全局作用域中使用,因为当有JS文件合并时,一个文件的全局严格模式会导致整个文件都是严格模式。
- 可以用
(function () {'use strict';/* 执行内容 */}());
匿名函数方式使用严格模式。
全等===
(不全等!==
)与等号==
(不等!=
)的区别
==
和!=
都会强制类型转换,再进行转换后值的比较。===
和!==
则不会转换,若类型不同则直接返回false
(switch
语句比较值是全等模式比较)。
- 建议:都用
===
或!==
进行比较。>=
等价于:== || >
;<=
等价于:== || <
。
三元运算符应当仅仅用在条件赋值语句中,而不要作为if
语句的替代:
var a = condition ? '1' : '2';
condition ? func1() : func2();
命名
约定函数名:
can
、has
、is
开头的返回值是布尔型。get
开头的返回是非布尔型。set
开头的执行保存动作。MAX_COUNT
。构造函数用大驼峰命名法(Pascal Case),首字母大写(以非动词开头),单词首字母大写:
var a = new Person(); // 构造函数
var b = getPerson(); // 普通函数
不要用多行的字符串写法
e.g.
```javascript /* 不提倡的多行写法 */ var a = 'abc\ def'; /* 一般写法 */ var b = 'abc' + 'def'; ```
_
作为区分。使用字面量代替构造函数(普通函数)的数据创建方式
好处:
- 代码更少。
- 不需要构造函数的「作用域解析」(scope resolution),提高性能。
- 构造函数可以接收参数,返回的实例不可控、易混淆;字面量简单不出错。
对象
e.g.
/* 不提倡的构造函数写法 */
var a1 = new Object();
a.attr1 = '...';
var a2 = new Object({attr1: '...'});
/* 提倡的字面量写法 */
var b = {attr1: '...'};
数组
e.g.
/* 不提倡的构造函数写法 */
var arr1 = new Array('a', 'b');
/* 提倡的字面量写法 */
var arr2 = ['a', 'b'];
字符串
e.g.
/* 不提倡的构造函数写法 */
var str1 = new String('a');
/* 提倡的字面量写法 */
var str2 = 'a';
长字符串拼接使用Array.prototype.join()
,而不使用+
.join()
性能好(建议方式)
+
性能差(不推荐)
注释规范
单行注释://
后不空格
使用场景:
多行注释:/*
和*
后空一格
使用场景:
作用:
函数注释规范(使用部分JSDoc)
/**
* 方法描述
* @constructor - (是否构造函数)
* @param {Number} param1 - 参数描述,限制值为:1 描述|2 描述|3 描述
* @param {Object|String|Boolean|Function|Array} param2 - 参数描述
* @param {*} [param3] - 可选参数
* @param [param4 = 'default'] - 可选参数拥有默认值
* @param {Object} param5 - 对象
* @param {Object|String|Boolean|Function|Array} param5.param5_1 - 对象的属性描述
* @param {...Number} param6 - 可重复使用参数
* @returns {Object|Undefined} result - 参数描述
*/
function func(param1, param2, param3, param4, param5, param6) {
// param5.param5_1
return result;
}
JS编程风格总结(programming style)
with
语句。==
)运算符,只使用「严格相等」(===
)运算符(同理仅使用!==
,不使用!=
)。new
命令,改用Object.create()
命令。++
)和自减(--
)运算符,用+= 1
和-= 1
代替。JS编码规范
绝大部分同意fex-team:tyleguide。
可以设置为IDEs的Reformat Code的排版样式。
用户体验
平稳退化(优雅降级):当浏览器不支持或禁用了JS功能后,访问者也能完成最基本的内容访问。
为JS代码预留出退路(<a>
添加href
属性,用JS事件绑定去拦截浏览器默认行为)
<a href="真实地址" class="j-func">...</a>
href="真实地址"
也是为了SEO。
javascript伪协议
<a href="javascript: func();">...</a>
内嵌事件处理程序
<a href="#" onclick="func();return false;">...</a>
向后兼容:确保老版本浏览器基本可用,使之虽不能支持某些功能,但仍能基本访问。
if(func){func();}
(最适当方式)。try-catch
。navigator.userAgent
检测)。资源分离:把样式表、脚本分离出HTML。
性能优化从URL输入之后就开始考虑。
在解决页面性能瓶颈时,要从URL输入之后就进行网站性能优化;避免在处理网页瓶颈时进行微优化。
即时编译(just in time compile,JIT):JS引擎会在JS运行过程中逐渐重新编译部分代码为机器码,使代码运行更快。
运行前编译(ahead of time,AOT):将较高级别的编程语言或中间表示形式,编译为本机机器代码的行为,生成的二进制文件可以直接在本机上执行。
微优化(micro-optimizations):尝试写出认为会让浏览器稍微更快速运行的代码或调用更快的方法。
UI层的松耦合
不要用JS修改CSS样式,JS仅修改class(任何时刻,CSS中的样式都可以修改,而不必更新JS)。
特例:根据页面重新定位,可以用JS设定位置(如:
top
、left
等)。
将HTML从JS中抽离,避免增加跟踪文本和结构性问题的复杂度。可以使用模板引擎,如:handlebars.js。
避免使用全局变量
任何来自函数外的数据都应当以参数形式传进函数:将函数与其外部环境隔离开。
单全局变量
唯一全局对象是独一无二的(不与内置API冲突),并将所有功能代码都挂载到这个全局对象上,因此每个可能的全局变量都成为这个唯一全局对象的属性(或方法)。
零全局变量(立即调用的函数表达式)
隔离代码。
(function (win) {
'use strict'; // 严格模式可以避免创建全局变量
var doc = win.document;
/* 代码 */
}(window));
事件处理程序
不要分发事件:
让事件处理程序成为接触到event
对象的唯一函数,在event进入应用逻辑前完成用户相关操作(包括阻止默认事件或阻止冒泡等)。
将配置数据从代码中分离
配置数据:URL、展示内容、重复的值、设置、任何可能发生变更的值。
都是用来限制某个函数在一定时间内执行次数的技巧。
防抖(debounce)
一个函数被调用后不执行,在间隔时间内没有再次被调用才执行(或一调用就执行,在间隔时间内没有再次被调用才可以进行下一次执行);若在间隔时间内再次被调用,则刷新等待时间并且继续等待间隔时间结束后才执行。
节流(throttle)
一个函数无法在间隔时间内连续执行,当上一次函数执行后过了间隔时间后才能进行下一次该函数的调用。
立即调用的函数表达式(IIFE,Immediately Invoked Function Expression)。
ES6拥有了块级作用域之后,不再需要
自执行匿名函数。
写法:
function
关键字当作一个函数声明的开始,函数声明的后面不能跟圆括号;- 将函数声明包含在圆括号中,表示函数表达式,函数表达式的后面可以跟圆括号,表示执行此函数。
/* 建议方式 */
;(function () {}())
;(function () {})()
;(() => {})() // 箭头函数
/* 不推荐方式 */
;[function () {}()]
~function () {}()
!function () {}()
;+function () {}()
;-function () {}()
delete function () {}()
typeof function () {}()
void function () {}()
new function () {}()
new function () {}
var f = function () {}()
1, function () {}()
1 ^ function () {}()
1 > function () {}()
传值进自执行匿名函数可以避免闭包导致无法记录变量值的问题
e.g.
```javascript for (var i = 0; i < 3; i++) { // 不用匿名函数 setTimeout(function () { console.log(i); // => 3 => 3 => 3(闭包作用:每个结果都同一个值) }, 0); } for (var i = 0; i < 3; i++) { // 匿名函数 (function (para) { setTimeout(function () { console.log(para); // => 0 => 1 => 2(结果是传入进匿名函数的参数) }, 0); }(i)); } for (var i = 0; i < 3; i++) { // ES6的块级作用域(ES6拥有了块级作用域之后,不再需要~~自执行匿名函数~~) let num = i; setTimeout(function () { console.log(num); // => 0 => 1 => 2 }, 0); } // ES6的块级作用域(ES6拥有了块级作用域之后,不再需要~~自执行匿名函数~~) for (let i = 0; i < 3; i++) { setTimeout(function () { console.log(i); // => 0 => 1 => 2 }, 0); } ```
可以添加函数名:(function 内部名字 () { console.log(内部名字); 错误 }())
var a = b = c = 1;/* b、c没有var的声明。等价于:var a = 1; b = 1; c = 1; */
第二个以后的变量表示没有var的赋值。
var a = a || {};
执行顺序:
var a;
。a || {}
先执行:根据规则先判断a的值是否为真,若a为真,则返回a;若a不为真,则返回{}。a
。1. `var a = typeof b !== 'undefined' && b !== null ? b : {};` 2. `if (typeof c !== 'undefined' && c !== null) {}`
var a = b || {};
与if (c) {}
会因为b或c没有定义而报错ReferenceError: b is not defined
,可以用typeof
来使代码健壮
if
、while
之类的判断语句中用赋值操作:
(大部分是误用)赋值的内容Boolean后为假会导致条件判断为假:if(a = false){/* 不执行 */}
。
判断语句内只判断整体返回值是
true
还是false
,与里面执行内容无关(尽管对其语法有所限制)。
逗号操作符,
对每个操作对象求值(从左至右),然后返回最后一个操作对象的值。
(0, obj.func)()
相当于0; var tmp = obj.func; tmp();
const obj = {
func () {
console.log(this)
}
}
obj.func(); // obj
(0, obj.func)() // window
// 返回最后一个操作对象的值,返回一个方法
if(var a = 1, b = 2, c = 3, false){/* 不执行 */}
:
var
语句中的逗号不是逗号操作符,因为它不存在于一个表达式中。尽管从实际效果来看,那个逗号同逗号运算符的表现很相似。但它是var
语句中的一个特殊符号,用于把多个变量声明结合成一个。
var a = [10, 20, 30, 40][1, 2, 3]; // 40
:
[10, 20, 30, 40]
被解析为数组;[1, 2, 3]
被解析为属性调用,逗号操作符取最后一个值为结果。因此结果为数组[10, 20, 30, 40]
的[3]
属性值:40
。
{a: 'b'} + 1; // 1
:
大括号视为代码块,没有返回值。需要给大括号加上小括号,表明为一个值:({a: 'b'}) + 1; // [object Object]1
。
浮点数的计算:
浮点数值计算会产生舍入误差,因此永远不要用条件语句判断某个特定浮点数值,也不要用JS进行复杂的计算。
避免浮点数运算误差函数:用整数进行小数的四则运算。
判断DOM是否支持某属性:
若要判定一个属性是否被DOM所支持,新建一个DOM来判断:if('属性' in document.createElement('某标签')){...}
。
在DOM中随意添加一个属性,
此属性 in 此DOM
永远为真,不可以判断是否此DOM存在此属性(或方法)。
eval
中直接执行function
声明无效,必须用引号把function
声明包裹起来才有效(尽量不用eval
):
eval
的参数是字符串。
eval(function a() {}); // function a() {}(但没有声明)
eval('function b() {}'); // undefined(声明成功)
if()
中的代码对于function
的声明就是用eval
带入方法做参数,因此虽然返回true,但方法没有被声明。setTimeout
与setInterval
中第一个参数若使用字符串,也是使用eval
把字符串转化为代码。
获取数组中最大最小值:
Math.min(...[1, 2, 3])
或Math.min.apply(null, [1, 2, 3])
Math.max(...[1, 2, 3])
或Math.max.apply(null, [1, 2, 3])
获取CSS的样式:
dom.style或window.getComputedStyle(dom[, 伪元素字符串])
返回一个CSSStyleDeclaration对象dom.style
window.getComputedStyle(dom[, 伪元素字符串])
设置CSS的内嵌样式:
window.getComputedStyle(dom[, 伪元素字符串])
获取的是只读对象,不允许任何方式设置(包括)。setProperty
cssText
(ie8-返回时不包含最后一个;
):
dom.style.cssText += '; 样式: 属性; 样式: 属性'
dom.style.cssText = '样式: 属性; 样式: 属性'
若赋值错误,则去除无效样式、保留有效的赋值内容。若有相同CSS属性名,则后面的覆盖前面。
dom.style.某属性名 = '值'
或dom.style.setProperty('某属性名','值')
。dom.style.某属性名 = ''
或dom.style.setProperty('某属性名','')
或dom.style.removeProperty('某属性名')
。若赋值错误,则保持赋值前的值。
文档或一个子资源正在被卸载(关闭、刷新)时先触发beforeunload
、再触发unload
:
关闭前异步发送数据:
navigator.sendBeacon(地址, 数据)
(XMLHttpRequest
:异步会被忽略、同步影响体验)。
beforeunload
(可取消默认行为)
真
,则试图弹出对话框让用户选择是否继续操作。 window.onbeforeunload = function (e) {
// do sth.
var msg = needConfirm ? '信息' : '';
(e || window.event).returnValue = msg;
return msg;
};
unload
window.open
、alert
、confirm
等)页面的id值会动态地成为window
的属性(全局变量),值为这个id所在的Element,除非window
已存在这个属性名。
e.g.
```html ```
(非打开新窗口的、非history.pushState/replaceState
改变路由的)页面后退
if (document.referrer !== '') { history.back() } else { /* 后退到底的其他行为 */ }
- 若是新打开的窗口(
target="_blank"
),则会出现document.referrer
有值,但history.back()
已后退到底。- 若是
history.pushState/replaceState
改变路由,则不改变document.referrer
(可能初始document.referrer === ''
)。- 重新请求当前页面链接(如:
location.reload()
、或点击<a href="当前页面链接">
),会导致document.referrer === '当前页面链接'
。
使用encodeURIComponent/decodeURIComponent
仅处理URI中的query属性名和属性值;使用encodeURI/decodeURI
处理整个URI;不要使用 (已废弃)escape/unescape
encodeURIComponent
转义除了以下字符之外的所有字符:字母
数字
(
)
.
!
~
*
'
-
_
encodeURI
转义除了以下字符之外的所有字符:字母
数字
(
)
.
!
~
*
'
-
_
;
,
/
?
:
@
&
=
+
$
#
当一个<script>
被执行时,在它之前的标签可以访问,但在它之后的标签无法访问(还不存在、未被解析到)
<!-- document、document.documentElement、document.head 出现 -->
<html>
<head></head>
<body> <!-- document.body出现 -->
某id的DOM <!-- 某id的DOM出现 -->
</body>
</html>
判断浏览器标签是否在激活状态
document.hidden
:当前文档是否被隐藏document.visibilityState
:当前文档的可见情况('visible'
、'hidden'
、'prerender'
、'unloaded'
)document
的visibilitychange
事件:当前文档切换隐藏/显示时触发
window
的focus/blur
事件:当前文档获得焦点/失去焦点时触发(并不意味着被浏览器标签是否被隐藏)
document.hasFocus()
:当前文档是否获得焦点处理多次引入同个全局对象的冲突
对异步加载的功能,可以用push
的方式处理异步加载问题
<script src="a.js" async></script><!-- 可以在任意地方异步插入 -->
<script>
window.a = window.a || [] // 要在插入数据之前执行
window.a.push({ i: 1 }) // 插入数据1
window.a.push({ ii: 2 }) // 插入数据2
</script>
// a.js
(() => {
// 主要功能
const _a = {
push (val) { // 添加要处理的数据:_a.push(数据)
console.log(val)
}
}
if (window.a instanceof Array) { // 若是数组,则说明加载.js之前插入了数据,直接处理
for (let i = 0; i < window.a.length; i++) {
_a.push(window.a[i])
}
}
// 全局变量赋值
window.a = _a
})()
兼容特殊浏览器、PC与WAP加载不同资源的方案
前端无法获取电脑文件系统中文件的绝对路径
<input type="file">
只能获得C:\fakepath\文件名.文件类型
。
dom1.contains(dom2)
判断dom2是否为dom1的后代节点(若dom1 === dom2
,则返回true
)。
- 事件:用户或浏览器执行某种动作,如:
click
、load
。- 事件处理程序(事件处理函数、事件监听器):当某个事件触发之后响应的函数。
- 事件对象:触发DOM事件产生的对象,包含与事件有关的所有信息。
约定
1. 以点击事件`click`为例。 2. `funcAttr`、`func0`、`funcIe`、`func2`为已经定义的方法。
原生JS
HTML事件处理程序(冒泡)
<div onclick="funcAttr()"></div>
(不能同个事件监听多个处理程序)
本质上,DOM0级事件处理程序等于HTML事件处理程序。因此可以互相覆盖、移除绑定的事件处理程序。
DOM0级事件处理程序(冒泡)
dom.onclick = func0;
(不能同个事件监听多个处理程序)
IE事件处理程序(冒泡)
dom.attachEvent('onclick', funcIe);
(可监听多个,按绑定顺序的逆序触发;需参数完全对应才能解绑定;无法解绑匿名函数)
DOM2级事件处理程序(冒泡、捕获)
ie8-不兼容。
dom.addEventListener('click', func2, false);
(可监听多个,按绑定顺序触发,需参数完全对应才能解绑定;无法解绑匿名函数)
jQuery(冒泡)
来自:jQuery:Events。
on
(one
类似)
jQuery的事件系统需要DOM元素能够通过元素的属性附加数据,使事件可以被跟踪和传递。因为
<object>
、<embed>
、<applet>
不能绑定数据,所以它们不能进行jQuery的事件绑定。
移除绑定:
绑定事件接口
将回调函数压入该事件的回调函数队列,当事件发生时,回调函数队列会被遍历,其中的函数会被逐个执行。解除绑定事件接口
将回调函数从该事件的回调函数队列中移除,当事件发生时,队列中没有该函数,于是该函数便不会执行。
attachEvent/detachEvent
,必须一一对应具体的handle进行解绑。handle是匿名函数无法解绑。
ie的handle中
1. 处理**handle**(传入方法不改变) `function () {funcIe.apply(dom, arguments);}`(无法解绑)。 2. 修改`funcIe` 1. `funcIe.bind(dom)`(需要`bind`的[polyfill](https://github.com/realgeoffrey/knowledge/blob/master/网站前端/JS方法积累/废弃代码/README.md#原生jsfunctionprototypebind的polyfill))。 2. 使用`window.event`定义~~this~~ ```javascript function funcIe(e) { e = e || window.event; // 事件对象 var _this = e.srcElement || e.target; // 触发的DOM /* funcIe代码 */ } ```this
指向window
的兼容方式
addEventListener/removeEventListener
,必须一一对应具体的handle、布尔值进行解绑。handle是匿名函数无法解绑。jQuery的on/off
(以及其他绑定解绑方法):
若要移除元素上所有的代理事件,而不移除任何非代理事件,使用特殊值'**'
。
e.g. $dom.off('click', '**'); // 解绑$dom代理到子节点的click事件,但不解绑$dom自己的click事件
JS的自定义事件
原生JS:
来自:MDN:CustomEvent。
// 监听自定义事件
dom.addEventListener('事件名', function (e) {...}, false) // e.bubbles/cancelable/composed/detail
// 创建自定义事件
var event = new CustomEvent('事件名'[, 参数对象]) // 参数对象:bubbles、cancelable、composed、detail(自定义数据)
// 触发
dom.dispatchEvent(event)
或
来自:[MDN:Event](https://developer.mozilla.org/zh-CN/docs/Web/API/Event/Event)。 ```javascript // 监听自定义事件 dom.addEventListener('事件名', function (e) {...}, false) // e.bubbles/cancelable/composed // 创建自定义事件 var event = new Event('事件名'[, 参数对象]) // 参数对象:bubbles、cancelable、composed。无法传递自定义数据 // 触发 dom.dispatchEvent(event) ```Event
jQuery
:
$('选择器').on('自定义事件', function () {})
$('选择器').trigger('自定义事件')
类型:
捕获(capture):
从外层元素到目标元素的过程,
冒泡(bubbling):
事件都有
bubbles
属性,判断是否冒泡。W3定义的DOM-Level-3-Events可查冒泡情况:;media相关事件均不冒泡。
从目标元素到外层元素的过程。
DOM标准事件流触发顺序:
处于目标
DOM2规范要求:实际的目标不接收
捕获事件,仅接收冒泡事件。但浏览器的实现却都让实际目标接收捕获和冒泡事件。
ie10-的DOM事件流只有冒泡,没有
捕获。
WAP端点透bug
- PC端没有
事件。touch
- WAP端有
touchstart
、touchmove
、touchend
、touchcancel
等touch
事件。- Zepto用
touch
一系列事件封装成tap
事件。
点透现象:
使用Zepto的tap
事件绑定(或原生JS的touchstart
绑定)后,若此元素在触摸事件发生后离开原始位置(CSS或JS),同一位置正好有一个DOM元素绑定了click
事件或<a>
,则会出现「点透」bug(触发底下元素的click
事件)。
原因
历史原因:WAP端增加快速双击缩放和恢复功能。由于当用户一次点击屏幕之后,浏览器并不能立刻判断用户是单击还是双击操作。因此,就等待300ms左右,以判断用户是否再次点击了屏幕。
WAP端触摸事件顺序:touchstart
-> touchmove
-> touchend
-> click
,触摸一瞬间触发touchstart
,触摸结束后触发Zepto封装的tap
事件,触摸结束后300ms左右触发click
事件。
解决方法:
使用fastclick.js消除click
的延时(最佳方式)
用click
代替全部tap
事件,这样PC端和WAP端都可以一致用click
事件且不会出现WAP端点透bug。
fastclick.js原理:在检测到
touchend
事件时,通过DOM自定义事件立即触发一个模拟click
事件,并阻止浏览器在300ms之后真正触发的click
事件。
<meta name="viewport" content="initial-scale=1.0, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
。<meta name="viewport" content="width=device-width, initial-scale=1.0">
。touch-action: manipulation;
仅允许在元素上进行触屏操作的平移、缩放,忽略click
。WAP端使用:active
document.body.addEventListener('touchstart', function () {}, true);
。document.body.addEventListener('touchstart', function () {}, true);
即可满足大部分浏览器使用伪类。WAP端播放
大部分
<video>
、<audio>
同理。
播放事件
参考:MDN:媒体相关事件。
正在播放
timeupdate
事件:当媒体.currentTime
改变(拉动进度或播放中)。
iOS手动设置
currentTime
值之后,当播放时,可能会回溯一段时间开始播放。e.g.
dom.currentTime = 10 // currentTime先被设置为10;若开始播放,则倒退至9.n开始播放
开始播放
play
事件:初次播放、暂停后恢复。playing
事件:初次播放、暂停后恢复、结束后自动开始(loop
属性模式)。暂停播放
pause
事件
完成一轮播放
因为
loop
属性模式无法触发ended
事件,又timeupdate
事件触发时间不确定、无法和媒体.duration
取等判断成功,故无法在loop
属性模式中判定。
(非loop
属性模式下的)ended
事件(伴随pause
事件)。
timeupdate
、ended
、pause
事件处理需求。各机型/浏览器对视频事件、API的处理不同,甚至某些Android机型会要求:只有触发在视频区域内的事件才可执行视频的API。
自动播放
JS代码模拟
<video id="j-video">
您的浏览器不支持 video 标签
</video>
<script>
var video = document.getElementById('j-video')
document.addEventListener('DOMContentLoaded', function () {
video.setAttribute('src', '视频地址') // 代替preload="none"
video.play()
}, false)
// 根据需求选用
window.ontouchstart = function () {
video.play()
window.ontouchstart = null
}
</script>
属性模式autoplay
兼容性差。
循环播放
JS代码模拟
ended
事件中触发媒体.play()
。
loop
属性模式
内嵌播放
因为全屏播放完
<video>
会白屏,故可以在父级添加封面图片背景。
<video id="j-video"
webkit-playsinline
playsinline
x5-video-player-type="h5">
您的浏览器不支持 video 标签
</video>
<script>
var video = document.getElementById('j-video')
// QQ浏览器去除 x5-video-player-type 属性
if (/QQBrowser/.test(window.navigator.userAgent)) {
video.removeAttribute('x5-video-player-type')
}
</script>
播放控件(内嵌播放)
controls
属性模式 - 播放视频问题:
无法操作客户端自定义播放控件:
一般手机有两种播放方式:
内联模式(浏览器支持其中一种):
webkit-playsinline playsinline
内联模式。
针对iOS的UC或QQ浏览器,可以添加iphone-inline-video。
x5-video-player-type="h5"
在底部的全屏内联模式(同层播放)。
无法操作全屏模式
无法改变全屏播放方向以及控件内容
DOM属性x5-video-orientation="landscape或portraint"
、x5-video-player-fullscreen="true"
无法解决。
有些浏览器在全屏模式中,不触发任何video事件
无法自定义loading。
controls
模式,部分浏览器依然出现播放按钮<video>
,会把视频的层级放到最高,暂时没有直接解决方法。打开全屏会触发window的scroll
事件。
此时的屏幕可能高宽发生切换,页面内的DOM位置信息可能没有变化(如:getBoundingClientRect
还是在原界面的值,屏幕高宽却变了)。
.play()
)是异步行为。以纵轴为例。
DOM节点
节点高度:
height+padding:
dom.clientHeight
或
$dom.innerHeight()
height+padding+border:
dom.offsetHeight
或
$dom.outerHeight()
节点内容高度:
dom.scrollHeight
节点内滚动距离:
dom.scrollTop
或
$dom.scrollTop()
节点顶部相对视口顶部距离:
dom.getBoundingClientRect().top
或
$dom.offset().top + document.documentElement.clientTop - $(window).scrollTop()
document.documentElement.clientTop
:<html>
的border-top-width
数值。
节点顶部相对文档顶部距离(不包括文档的border):
dom.getBoundingClientRect().top + (document.body.scrollTop || document.documentElement.scrollTop) - document.documentElement.clientTop
或
$dom.offset().top
文档和视口
视口高度:
window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
或
$(window).height()
文档内容高度:
document.body.scrollHeight
或
$(document).height()
文档滚动高度:
document.body.scrollTop || document.documentElement.scrollTop
或
$(window).scrollTop()
- 还可以使用
IntersectionObserver
判断节点和视口(或祖先节点)相交程度。- Zepto没有
innerHeight
和outerHeight
,改为height
。
jQuery
节点顶部在视口底部以上
$dom.offset().top <= $(window).scrollTop() + $(window).height()
节点底部在视口顶部以下
$dom.offset().top + $dom.outerHeight() >= $(window).scrollTop()
节点在视口内
以上&&
结合。
原生JS
视口高度
`window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight` 或 `$(window).height()`
节点顶部在视口底部以上
dom.getBoundingClientRect().top <= 视口高度
节点底部在视口顶部以下
dom.getBoundingClientRect().bottom >= 0
节点在视口内
以上&&
结合。
IntersectionObserver
判断节点和视口(或祖先节点)相交程度。某dom跟随屏幕滚动而相对静止
1. 判断:要处理的节点顶部相对视口顶部距离(`dom.getBoundingClientRect().top`)。 1. 若≤0,则处理dom(具体实现:[节点跟随屏幕滚动而相对静止](https://github.com/realgeoffrey/knowledge/blob/master/网站前端/JS方法积累/实用方法/README.md#jquery节点跟随屏幕滚动而相对静止))。 2. 若>0,则恢复dom的文档流。 2. 从文档顶部到要处理的位置用``包裹,对这个额外包裹的``进行`IntersectionObserver`处理。 1. 若消失,则处理dom。 1. 若展示,则恢复dom的文档流。 </details>
也可以给底部(或顶部)放置一个标记节点,当这个节点的顶部在容器底部以上(或这个节点的底部在容器顶部以下)时为滚动到底部(或顶部)。
DOM节点
内容滚动到底部:
dom.offsetHeight + dom.scrollTop >= dom.scrollHeight
内容滚动到顶部:
dom.scrollTop === 0
文档
视口高度、文档滚动高度、文档内容高度
1. 视口高度: `window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight` 或 `$(window).height()` 2. 文档滚动高度: `document.body.scrollTop || document.documentElement.scrollTop` 或 `$(window).scrollTop()` 3. 文档内容高度: `document.body.scrollHeight` 或 `$(document).height()`
滚动到底部:
视口高度 + 文档滚动高度 >= 文档内容高度
滚动到顶部:
文档滚动高度 === 0
支持动画过渡效果(smooth):
dom.scrollIntoView()
dom或window.scroll/scrollTo(横轴坐标, 纵轴坐标)
或 dom或window.scroll/scrollTo({ left: 横轴坐标, top: 纵轴坐标, behavior: 'smooth'或'auto' })
dom或window.scrollBy(相对横轴坐标, 相对纵轴坐标)
或dom或window.scrollBy({ left: 相对横轴坐标, top: 相对纵轴坐标, behavior: 'smooth'或'auto' })
不支持过渡效果、瞬间定位:
window.location.href = #锚点
dom.scrollTop = 纵轴坐标
dom.scrollLeft = 横轴坐标
整个文档滚动:
document.body.scrollTop = document.documentElement.scrollTop = 纵轴坐标
document.body.scrollLeft = document.documentElement.scrollLeft = 横轴坐标
scroll-behavior: smooth;
会强制上面所有滚动效果在dom上都是动画过渡效果(smooth)。DOM点击事件的定位(原生JS)
PC端MouseEvent
e.
(e
为传入事件处理程序的第一个参数)或window.event.
:
pageX/Y
(ie8-不支持)
距离文档的左边缘/上边缘。
clientX/Y
距离浏览器窗口。
screenX/Y
距离屏幕。
offsetX/Y
距离目标元素自身填充边界。
movementX/Y
(ie、Safari不支持)
(仅针对mousemove
事件)相对于上一帧鼠标移动距离。
x/y
等价于:clientX/Y
。
WAP端TouchEvent
e.touches[0].
(e
为传入事件处理程序的第一个参数):
pageX/Y
clientX/Y
screenX/Y
相对偏移(jQuery)
$dom.offset()
返回:相对于文档的距离(可以设置)。
$dom.position()
返回:相对于距离该元素最近的且被定位过的祖先元素的距离。
$dom.offsetParent()
返回:相对于距离该元素最近的且被定位过的祖先元素的jQuery对象。
$dom.scrollLeft/Top()
返回:当前滚动条的位置(可以设置)。
用HTML文本覆盖:
dom.innerHTML/outerHTML/innerText/textContent = HTML文本
用新的DOM插入或覆盖旧的DOM:
若要被添加的DOM(newDom)已存在于页面上,则会移动该DOM到目标容器元素下。
插入:
parentNode.append/prepend/appendChild(newDom)
、parentNode.insertBefore(newDom, oldDom)
、oldDom.before(newDom)
替换:
oldDom.replaceWith(newDom)
、parentNode.replaceChild(newDom, oldDom)
删除DOM:
parentNode.removeChild(oldDom)
、oldDom.remove()
创建DOM:
document.createElement(标签名)
Node
与Element
Node根据nodeType
属性区分节点类型:
1 === Node.ELEMENT_NODE
Element
Element
继承了Node
。
3 === Node.TEXT_NODE
Element
的文本
或属性
7 === Node.PROCESSING_INSTRUCTION_NODE
8 === Node.COMMENT_NODE
注释
9 === Node.DOCUMENT_NODE
document
10 === Node.DOCUMENT_TYPE_NODE
文档类型节点,如:<!DOCTYPE html>
11 === Node.DOCUMENT_FRAGMENT_NODE
DocumentFragment
attribute
与property
在HTML标签里定义标签的attributes
,一旦浏览器解析后,所有DOM节点会被创建成对象,每个DOM对象就拥有properties
,因此:
attributes
:HTML标签的属性(打开浏览器检查能够看见标签上的属性)。
.attributes
展示此DOM对象的所有attribute
.getAttribute/setAttribute/removeAttribute/hasAttribute/toggleAttribute
操作attribute
properties
:DOM对象的属性。
浏览器的 前进/后退/重新打开关闭的标签页(
刷新不行),会进行:重新下载HTML后进行常规的解析HTML-加载资源-渲染,但会尝试恢复之前页面的properties
(包括页面滚动、每个DOM对象的properties
、等)。仅针对文档对象模型(DOM构造),不包括attributes
;仅针对新加载出来页面能匹配之前页面的部分,新增的DOM没有匹配。
当一个DOM节点为某个HTML标签所创建时,其大多数properties
与对应attributes
拥有相同或相近的名字,但并非一对一的关系:
浏览器在加载页面之后会对页面中的标签进行解析,并生成与之相符的DOM对象,每个标签中都可能包含一些属性。若这些属性是标准属性,则解析生成的DOM对象中也会包含与之对应的属性。
某些property
值等于attribute
值,修改其中之一另一个也随之改变:
property
是直接反映attribute
,如:rel
、id
htmlFor
映射for
,className
映射class
,dataset
映射data-
集合很多property
所映射的attribute
是带有限制或变动
type
的值被限制在已知值(即<input>
的合法类型,如:text
、checkbox
等)。href/src
的property
值会计算出最终完整路由,它的attribute
只是HTML标签上显示的路径。部分动态属性(如:checked
、selected
、disabled
、value
、muted
等)遵循下面规则:
property
:页面展示跟随attribute
,property
值跟随attribute
。property
:页面展示跟随property
,property
与attribute
值不互通。
property
,attribute
不会随之变化。操作页面展示时,也就设置了property
。判断一个标签的动态属性(DOM对象的property
)
以
<input>
的checked
为例,类似的特性还有selected
、disabled
、value
、muted
等。
<input type="checkbox" checked="checked" id="j-input">
<script>
var $dom = $('#j-input');
var dom = $dom.get(0);
// ①
// 共通的值,存储在jQuery内部
// 获取:真实效果的值
// 设置:可以改变页面上显示效果、不会改变HTML上属性的值
dom.checked; // 设置 dom.checked = true/false
$dom.prop('checked'); // 设置 $dom.prop('checked', true/false);
// ②
// 获取:HTML上属性的值
// 设置:若没有使用①设置值,则可以控制①结果、可以改变页面上显示效果;
// 若使用①设置过,则仅设置HTML上属性的值、不会真的改变显示效果
$dom.attr('checked'); // 设置 $dom.attr('checked', true/false);
// ③
// 获取:HTML上属性的值
// 设置:若没有使用①设置值,则可以仅把关闭设置为打开、无法把打开设置为关闭(removeAttribute可以关闭);
// 若使用①设置过,则仅设置HTML上属性的值、不会真的改变显示效果
dom.getAttribute('checked'); // 设置 dom.setAttribute('checked', 值);去除 dom.removeAttribute('checked')
</script>
若要判断、获取或设置,建议仅使用:
dom.checked
,设置:dom.checked = true/false
。$dom.prop('checked')
,设置:$dom.prop('checked', true/false)
。$dom.is(':checked')
。当变量是jQuery对象时,可用$
作为开头命名,利于与普通变量区分
// e.g.
var num = 1;
var dom = $('div').get();
var $dom = $('div');
$('子','父') === $('父').find('子')
找到所有父级,若子级在此父级后面,则选择。
(效率低)$('父 子')
找到所有子级,然后向前找出有父级的,则选择(与CSS相同的查询方式)。
判断选择器选择到空内容
选择器选取的内容返回数组(空内容返回[]
),所以if($(...)) {}
永远成立。因此用以下方法:
if($(...).length > 0) {} // 若无则为0
if($(...)[0]) {} // 若无则为undefined
on
绑定效率(事件代理、事件委托)
事件代理:把原本需要绑定的事件委托给祖先元素,让祖先元素担当事件监听的职务。
- 原理:DOM元素的事件冒泡。
- 好处:提高性能,避免
批量绑定。
paste
、reset
不支持事件代理,仅能够直接绑定;事件代理不能用于SVG。
e.g.
$(eventHandler).on(event, selector, func);
确定绑定内容:
on
方法的时刻,把所有满足条件的DOM对象安装指定的应用逻辑func,成为eventHandler;绑定的eventHandler距离selector越近,效率越高。因此虽然把selector都绑定在$(document)
上能够避免增删节点对事件绑定造成的影响,但效率会下降。
每次触发事件,jQuery会比较从目标元素(
target
)到eventHandler路径中每一个元素上所有该类型的事件(冒泡)。
判断是否加载成功,不成功则加载其他文件地址
<script>
window.jQuery || document.write('<script src="其他文件地址"><\/script>');
</script>
$dom.focus()
focus
然后再添加文本,光标停留在文本结尾。$dom.data()
仅在第一次使用后(获取或设置)获取HTML上的值或设置到jQuery内部(不会改变HTML上属性的值),之后不再因为HTML改变而改变,也无法修改HTML上属性的值。
$dom.removeData()
删除绑定的数据后,再使用$dom.data()
则重新获取HTML上的值或设置到jQuery内部。
因为
<object>
、<embed>
、<applet>
不能绑定数据,所以它们不能使用.data()
。
jQuery操作CSS样式:
设置大部分CSS样式时,不写单位默认:px
;对于px
单位的样式可以设置相对值'+=数字'
或'-=数字'
:
.css()
、.width()
、.height()
、.animate()
。
jQuery.cssNumber
保存着没有默认单位的属性名,可设置为false
尝试添加默认单位。
获得样式时,都是换算成px
(若可以换算)。
.height/innerHeight/outerHeight()
返回数字
。.css('height')
返回带px
的字符串
。
$.proxy()
和原生JS的Function.prototype.bind
类似,返回一个确定了this
(和参数)的新方法
新方法的参数填补在原函数去除已设置参数的后面(与
Function.prototype.bind
一致)。
额外的,jQuery确保即使绑定的函数经过$.proxy()
处理,依然可以用原先的函数来正确地取消绑定