这一遍的重点还是在JS语言本身,后面的DOM和BOM部分看的比较快,因为现在实际上用的不多,掌握大致的原理,需要的时候再翻手册就可以了。
认为暂时没必要的知识点WebGL,
认为已经不需要、过时的知识/XML/E4X
终于在2018年的末尾把这个书又看了一遍,还是有些进步的,2019,加油(2018.12.28)
JavaScript组成部分:
标签的解析是阻塞式的,如果放在
中,会导致浏览器为了解析脚本,使页面空白时间太长,解决方式是将
标签放在
内部末尾
标签的
defer
和async
属性都是为了是异步解析脚本的,不同点是:defer
是将标签延迟到后执行,并且按照出现的先后顺序执行(实际上虽然并不是这样),而
async
则不保证执行顺序
五种基本数据类型:string
, number
, boolean
, undefiend
, null
,一种复合数据类型object
ES6中新增了一种基本类型
Symbol
typeof null; // 'object'
typeof function test(){}; // 'function'
typeof NaN; // 'number'
是一个空对象指针
JS中的假值: null
,undefined
, 0
, false
, NaN
, ''
,
浮点数值计算存在误差,导致了在JS中0.1 + 0.2 = 0.30000000004
NaN
与任何值都不相等
isNaN()
方法判断是否是NaN
,会将非数值转换为数值;Number.isNaN()
不会强制将参数转换成数字,只有在参数是真正的数字类型,且值为NaN
的时候才会返回true
。
isNaN('fff'); //true
Number.isNaN('fff'); // false
isNaN(undefined); // true
Number.isNaN(undefined); // false
Number.isNaN({}); // false
isNaN({}); // true
isNaN(true); //false
Number.isNaN(true); //false
// 最后两个例子,前者是`false`,因为`true`可以转换为数字1,而后者是因为`ture`就不是数值
Number()
方法使用Number()
方法与使用+
操作符效果相同
Number()
的转换规则如下:
ture
→ 1
, false
→ 0
,null
→ 0
undefined
→ NaN
0x
解析为十进制(不忽略十六进制)0
NaN
(Number('123blue')
→ NaN
)valueOf()
方法按照上述规则解析,如果解析结果是NaN,调用toString()
方法再来一遍Number('123Hello'); // NaN
Number(''); // 0
Number('0011'); // 11
Number('0x11'); // 17
Number(null); // 0
Number(undefined); // NaN
Number(true); // 1
Number([1]); // 1
Number([1, 2]); // NaN
Number({}); // NaN
parseInt()
方法用于解析字符串和数字,一切非字符串和数字, 更加常用
第二个参数是解析使用的基数(即多少进制),建议无论何时都显式指定第二个参数。
parseInt(11, 10); // 11
parseInt(11, 8); // 8
parseInt(11, 2); //3
与Number()
最明显的不同是:
Boolean
,Object
)都给出NaN
Number()
返回0
, parseInt()
返回NaN
Number('123blue'); // NaN
parseInt('123blue'); // 123
Number(''); // 0
parseInt(''); // NaN
\r
:linux换行\n
:window换行除了null
和undefined
,数值、对象、字符串、布尔值都有toString()
方法,对于数字的toString()
方法,参数指定输出数值的基数:
let a = 100;
a.toString(10); // "100"
a.toString(2); // "1100100"
a.toString(8); // "144"
对于Object
的toString()
方法,返回值是"[object Object]"
类型,第二个表达式是调用者的类型(可以用来判断变量的类型):
let a = 100;
Object.prototype.toString.call(a); // "[object Number]"
let b = {};
b.toString(); // "[object Object]"
let c = 'a';
Object.prototype.toString.call(c); // "[object String]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call(null); // "[object Null]"
let e = {a: 1} + 'f'; // "[object Object]f"
String()
方法与+''
效果相同,转换规则:
toStirng()
方法,调用null
,返回'null'
undefined
,返回'undefined'
constructor
: 创建当前对象的构造函数hasOwnProperty(propertName)
: 检查给定的属性是否在对象实例中(而非对下属的那个的原型中)isPrototypeOf(Object)
: 用于检查传入的对象是否是当前对象的原型propertyIsEnumberable(propertyName)
: 用于检查属性是否可枚举toLocalString()
: 返回对象的字符串表示toString()
: 返回对象的字符串表示valueOf()
: 默认情况返回对象本身~
操作符~
是按位非操作符,返回值是数值的反码:
~1 === -2; //true
~0 === -1; // true
用于简化indexOf
表达:
if (~[1,2,3].indexOf(1)) {
// do something
}
// 等同于
if ([1,2,3].indexOf(1) !== -1) {
// do something
}
// -5的二进制表达式
// 5的二进制
101
// 补全
0000 0101
// 求反码
1111 1010
// 求补码
1111 1011
&&
操作符和||
操作符&&
找第一个为false
的项,找不到取最后一个||
找第一个为true
的项,找不到取最后一个&&
优先级高于||
可以利用&&
简化if
表达式:
if(a) {
b()
}
// 简化为
a && b()
==
基本规则:
true
→ 1
, false
→ 0
valueOf()
方法,在按照上面规则进行比较null
== undefined
null
和undefined
不转换为其他值NaN
不等于任何值,包括自己null == undefined;
// true
null == 0;
// false
undefined == 0;
// false
'4f' == 4
// false
[] == [];
// false
// 原因是指向不同的内存地址
{} == {};
// false
一个问题: [] == ![]
[] == ![]
// 首先执行!操作,so
[] == false
// 然后将布尔值转为数字,so
[] == 0
// 然后调用数组的valueOf()方法,so
[] == 0
// 然后调用数组的toString()方法,so
'' == 0
// 然后将字符串转换为数字,so
0 == 0
// so,结果为true
不要试图通过转换数据类型来解释null == undefined
,因为:
Number(null); // 0
Number(undefined); // NaN
// 在比较相等性之前,null 没有被转换为其他类型
null == 0 ; //false
要牢记:在比较相等性之前,null
和undefined
没有被转换为其他类型
for...in
输出顺序不可预测
可以用label
语句为循环语句添加标签,用于在break
或者continue
时指定目标:
outer: for(let i=0; i<10; i++) {
inner: for(let j=0; j<10; j++) {
if(j === 5) {
break outer;
}
}
}
switch
语句中使用的是全等操作符,意味着:①传入的值和条件case之间是判断是否相等,而非判断case是否为真;②不会发生类型转换
函数参数可以通过类数组对象arguments
访问参数,严格模式下(也不应该)重写arguments
值会报错
基本类型指的是简单的数据段,而引用类型值指那些可能由多个值构成的对象。
函数传参时,基本类型和引用类型都是按值传递(而不是按引用传递)
let a = {
value: 1
}
function setValue(obj) {
obj.value = 55
obj = {}
obj.value = 100
}
setValue(a);
console.log(a.value); // 55
上面的例子中,在函数内部重写obj
时,变量引用的是另一个局部对象了,它会在函数执行完毕后立即被销毁。
可以认为函数的参数是一个局部变量。
使用typeof
检测类型的时候对于引用类型用处不大,返回值都是object
,可以使用instanceof
// 检测数组
arr instanceof Array
对于引用类型,如果变量是给定引用类型的实例,instanceof
返回true
,但是注意:
Object
的实例"foo" instanceof String //=> false
"foo" instanceof Object //=> false
let a = new String('foo')
a instanceof String //=> true
a instanceof Object //=> true
所以这也解释了为什么严格意义上,不能使用typeof
来检测字符串类型
let a = new String('foo')
typeof a; // 'object'
严格的方式是使用对象的toString
方法:
function isString(string) {
return Object.prototype.toString.call(string) === '[object String]'
}
// 或者
function isString(string) {
return typeof string === 'string' || string instanceof String
}
ES6之前是没有块级作用域的,只有全局作用域和函数作用域
在函数作用域中如果没有使用var
关键字,定义的变量是添加到全局作用域的。不应该这样做,并且严格模式下会报错。
一旦数据不再有用,最好通过将其值设为null
来释放其引用,这种做法叫做解除引用。
解除引用并不意味可以自动回收其所占内存,真正作用是让值脱离执行环境,便于垃圾收集器下次运行是将其回收。
两种创建方式:
new Object()
使用构造函数创建数组时,传入单个参数和多个参数表示意义不同,传入单个参数表示数组的长度,数组成员是undefined
,传入多个参数的时候表示数组成员:
let arr = new Array(2);
console.log(arr); // [undefined, undefined]
let arr2 = new Array(2, 2);
console.log(arr2); // [2, 2]
为了统一,ES6新增了Array.of方法,无论传入多少参数,都代表数组成员:
let arr = Array.of(2);
console.log(arr); // [2]
let arr2 = Array.of(2, 2);
console.log(arr2); // [2, 2]
可以使用instanceof
方法,也可以使用Array.isArray()
方法
b.
数组的toString
会返回有将数组每个值的字符串形式用逗号凭借而成的字符串
数组的valueOf
方法返回原数组
alert()
方法需要接收字符串作为参数,如果传入数组会自动调用数组的toString()
方法
push
和unshift
,返回值是数组长度pop
和shift
,返回值是移除项sort
方法sort
方法比较的是字符串,按升序排列数组
可以接受一个函数来确定排序规则
concat
方法会创建一个新数组, 以调用者作为基准,传入的参数如果是单个成员,直接添加在后面,如果是数组,将数组的每一项添加在后面
slice(a, b)
会截取a-b
间的数组(包含a
,不包含b
),返回截取部分,不会影响原数组。如果b>a
,返回空数组。
splice(a, b, c, d...)
方法,a
表示从第几项开始(包含),b
表示删除几项,c
/d
及后续参数表示参数的成员,返回值是删除的项目,会更改原数组
reduce(a, b)
接受两个参数,第一个是在每一项上调用的函数,第二个是(可选的)作为归并基础的初始值。每项调用的函数接受4个参数,分别是前一个值,当前值、当前项索引和数组对象
使用new Date()
创建日期对象,不传递参数的情况下,新创建的对象自动获得当前日期和时间
传递的参数可以使毫秒数,也可以是特定的日期格式,这个日期格式会自动调用Date.parse
来解析,支持的日期格式:
月/日/年
: 02/08/2018
YYYY-MM-DDTHH:mm:ss
: 2018-08-05T00:00:00
Date.now()
返回调用这个方法时的毫秒数,也可以使用+
操作符实现同样的操作:
let a = Date.now(); // 1534062821069
let b = +new Date(); // 1534062821069
Date类型重写了toLoaclStirng()
、toString()
和valueOf()
方法
toLoaclStirng()
、toString()
都会根据浏览器设置的地区返回日期和时间,却别是前者不会包含市区信息,后者会
new Date().toLocaleString()
// "2018/8/16 下午9:28:14"
new Date().toString()
// "Thu Aug 16 2018 21:28:24 GMT+0800 (中国标准时间)"
valueOf()
返回日期的毫秒表示:
new Date().valueOf() === new Date().getTime()
// true
new Date().valueOf() === Date.now()
// true
比较日期时需要将日期转换为毫秒进行比较,较早的日期数值较小。
匹配模式包含:
g
:全局模式i
:不区分大小写m
:多行模式创建正则表达式的两种方式:
let reg = /.at/i
let reg2 = new RegExp('.at', 'i')
RegExp.exec()
是RegExp实例的方法,接受的参数是字符串,返回值一个数组,这个数组的第一项是与整个正则表达式匹配的字符串,其他项是与正则表达式中捕获组匹配的字符串。数组的index
属性是匹配的字符串出现在字符串的位置,数组的input
属性是字符串参数
let text = 'These are mom and dad and baby';
let pattern = /mom (and dad (and baby))/;
let res = pattern.exec(text);
console.log(res.index);
// 10
console.log(res.input)
// "These are mom and dad and baby"
console.log(res[0])
// "mom and dad and baby"
console.log(res[1])
// "and dad and baby"
console.log(res[2])
// "and baby"
console.log(res[3])
undefined
对于exec方法而言,RegExp如果没有设置全局标识g
,执行多次的返回值都是相同的,如果设置了g
,每次执行也都返回一个匹配项,但是后续调用都会在现有的基础上继续查找,直到搜索到字符串末尾为之。
在没有设置全局标识g
的情况下:
let text = 'These are mom and dad and baby'
let pattern = /\sand/
pattern.exec(text).index
// 13
pattern.exec(text).index
// 13
pattern.exec(text).index
// 13
在设置全局标识g
的情况下:
let text = 'These are mom and dad and baby'
let pattern = /\sand/g
pattern.exec(text).index
// 13
pattern.exec(text).index
// 21
pattern.exec(text).index
// Uncaught TypeError: Cannot read property 'index' of null
RegExp.test()
方法接受字符串作为参数,在正则表达式与该参数匹配的情况下返回true
,否则返回false
,常用在if
语句中
RegExp构造函数有一些属性,适用于作用域中所有的正则表达式,并且基于所执行的最近一次正则表达式操作而变化。
常用的是$1
/$2
/$3
/$4
…$9
用来存储第一、第二……第九个捕获组,在调用exec
或者test
方法时这些属性会被自动填充
let string = '123-123-1234'
let reg = /(\d)-(\d+)/
reg.exec(string)
// ["3-123", "3", "123", index: 2, input: "123-123-1234", groups: undefined]
RegExp.$1
// "3"
RegExp.$2
// "123"
RegExp.$3
// ""
RegExp.$4
// ""
函数是对象,函数名是指向函数对象的指针
定义函数的三种方式:
// 函数声明
function test(num1, num2) {
return num1 + num2
}
// 函数表达式
const test = function(num1, num2) {
return num1 + nume2
}
// Function构造函数
const test = new Function('num1', 'num2', "return num1 + num2")
函数声明会进行函数声明提升,在代码开始执行之前就将函数声明添加到执行环境中。函数表达式没有这一过程。
将函数作为返回值或者参数传递给另外一个参数。
有这样一个例子,将对象数组按照其中一个属性进行排序:
写出一个通用的函数,传入的参数是属性名,返回值是另外一个函数,这个函数作为排序方法sort()
的参数
let arr = [
{name: 1, value: 3},
{name: 2, value: 100},
{name: 3, value: 99}
]
const createComparsionFunc = key => (
(prevItem, nexItem) => prevItem(key) - nextItem(key)
)
arr.sort(createComparsionFunc('value'))
arguments
是一个类数组对象,包含着传入函数中的所有参数,它还有一个名叫callee
的属性,指向拥有这个arguments
对象的函数
可以用这个属性来接触在递归时对函数名的耦合状态:
function test(v) {
if(v === 1) {
return v
} else{
return v* arguments.callee(v-1)
}
}
注意,在严格模式下是禁止访问arguments.callee
属性的。
this
引用的是函数执行时的环境对象,再调用函数之前,this
的值并不确定,因此this
可能在代码执行过程中引用不同的对象。
caller
中保存着对象函数的引用,在全局作用域下调用函数,函数的caller
是null
函数的length
属性表示函数希望接受的命名参数的个数
每个函数都包含两个非继承而来的方法apply()
和call()
,用途都是在特定的作用域中调用函数,实际上等于设置函数体内this
对象的值。
二者的第一个参数都是在其中运行函数的作用域(也就是this
的指向),apply
的第二个参数是以数组形式传递给函数的参数,而call
则需要将传递给函数的参数逐个列举出来。
使用call
或apply
的好处是,方法不再需要与对象有任何耦合关系。
ES5的bind
方法会创建一个函数的实例,其this
值会绑定到传给bind
的对象
完全可以用apply
来实现bind
Function.prototype.bind2 = function(obj) {
const self = this;
const param = [].slice.call(arguments, 1);
return function() {
self.apply(obj, param.concat([].slice.call(arguments)))
}
}
bind
方法,要传给函数的真参数是可以写在bind
方法中,也可以写在调用时:
function test(a) {
console.log(this.name, a)
}
test.bind2({name: 999})(123)
test.bind2({name: 999}, 123)()
上面两种都是可行的,所以在bind2
中传入的参数是param.concat([].slice.call(arguments))
注意外层函数不能用箭头函数,因为箭头函数没有自己的this
,是在绑定时向外层寻找的,如果使用箭头函数,this
就是window
,而无法按预期指向函数实例,此外箭头函数是没有arguments
属性的
函数的toString
方法会返回函数代码的字符串形式,valueOf
直接返回函数代码
利用Number
、String
和Boolean
构造行数创建的对象就是基本包装类型
基本类型(Number
、String
和Boolean
)不是对象,本不应该有方法,但是他们却有方法:
let a = 'test'
let b = a.slice(2)
// "st"
这是因为在调用slice
方法时后台自动完成了一系列的处理:
String
类型的一个实例let _a = new String('test');
let b = _a.slice(2);
_a = null
经过这番处理,基本的字符串值就变的和对象呢一样了,上面的步骤也适用于Number
和Boolean
要注意的是,自动创建的基本包装类型的对象,只存在于一行代码的执行瞬间,然后立即被销毁,这样就意味着我们不能再运行时为基本类型值添加属性和方法
let a = 'test';
a.color = 'red';
console.log(a.color); // undefined
可以显示的调用Number
、String
和Boolean
构造函数来创建基本包装类型的对象,但是应当谨慎使用,因为对其使用typeof
会返回object
,让人分不清实在处理基本类型还是引用类型
使用new Object()
可以根据传入值的类型返回相应基本包装类型的实例:
const obj = new Object('fff');
console.log(typeof obj); // 'object'
console.log(obj instanceof String); // true
要注意:使用new
关键字调用构造函数,与直接调用同名的转型函数是不同的:
let a = '123'
typeof new Number(a); // "object"
typeof Number(a); // "number"
使用new
关键字返回的是Number
的实例,而直接使用Number返回的是基本类型的数字,基本类型当然不是Number
的实例
关于Boolean
基本包装类型容易混淆的地方:
!! new Boolean(false); // true
此外:要注意,基本包装类型是对对象
typeof new Boolean(false); // 'object'
typeof false; // 'boolean'
所以最稳妥的鉴别类型的方式就是Object.prototype.toString.call()
Number类型的toString方法返回数字的字符串形式,toLocalString()方法返回这个数字在特定语言环境下的表示字符串,可以用来用逗号分割数字(可以传入不同的参数返回不同格式的数字,默认按照英文习惯)
let num = 10000
num.toLocaleString(); // "10,000"
Number.toFixed()方法返回指定小数数位的四舍五入的数值的字符串表示,所以将一个数字保留2为小数可以用两种方法:
let a = 1.23334
a.toFixed(2); // "1.23"
Math.round(a * 100) / 100; // 1.23
注意,toFixed
的四舍五入方法和数学中的规则不同,使用的是银行家舍入规则:
四舍六入五取偶:四舍六入五考虑,五后非零就进一,五后为零看奇偶,五前为偶应舍去,五前为奇要进一。
Number的toPrecision
方法可以根据传入的参数决定数值的表
示形式:
let a = 99
a.toPrecision(1)
// "1e+2"
a.toPrecision(2)
// "99"
a.toPrecision(3)
// "99.0"
String
的slice
方法,接受两个参数slice(a, b)
,最终截取的部分大于等于a并且小于b的部分,如果传入的参数是负数,则会将负值与字符串长度相加,再进行截取
注意,这个方法不会将参数位置对调:
let c = '1234567'
c.slice(0, -4)
// 相当于c.slice(0, 3)
// "123"
c.slice(-4, 0)
// 相当于c.slice(3, 0)
""
string.indexOf()
方法的第二个参数表示从字符串那个位置开始搜索。string.lastIndexOf()
方法是从后向前找,但返回值也是字符串的位置(并不是倒数的位置)
string.trim()
方法创建文字的副本,删除前置及后置的空格
关于match
方法exec
方法单独写了总结,比较绕。
string.replace(a, b)
用来替换简化替换字符串的操作,第一个参数a
可以是字符串也可以是正则表达式,第二个参数是字符串。如果第一个参数是字符串,则值替换第一个字符串,想替换所有字符串,唯一的办法就是提供正则表达式并且指定全局标志(g)
可以使用$
获取正则表达式匹配的值,比如$1
表示匹配第一个捕获组的子字符串
Global对象就是全局对象,在浏览器环境中,Global对象是window对象,在NodeJS中就不同了,可以通过下面的取得当前的Global对象:
function getGlobal() {
return this;
}
encodeURI()
和encodeURIComponent()
方法可以对URI进行编码,前者不会对属于URI的特殊字符进行编码,比如冒号、正斜杠等,而后者会对所有发现的特殊字符进行编码
let str = 'http://www.baidu.com/我'
encodeURI(str)
// "http://www.baidu.com/%E6%88%91"
encodeURIComponent(str)
// "http%3A%2F%2Fwww.baidu.com%2F%E6%88%91"
所以一般来说,使用encodeURIComponent()
的场景是对URI中某一段(一般是查询参数)进行处理,使用相对更加频繁
eval()
方法将传入的字符串参数作为实际的JS代码来进行解析并且执行,可以被用来代码注入。
Math
对象提供了min()
和max()
方法,可以接受任意数量的参数,要找到数组中的最大值或者最小值,有两种方法:
const arr = [1,2,3,4]
Math.max.apply(Math, arr)
Math.max(...arr)
通过Math.random()
获得随机值,编写一个函数,返回二者之间的随机数:
原理:
值 = Math.floor(Math.random() * 可能值的个数 + 最小值)
函数:
function selectFrom(max, min) {
if (max < min) {
[max, min] = [min, max]
}
const choices = max - min + 1;
return Math.floor(Math.random() * choices + min)
}
对象的定义:无序属性的集合,其属性可以包含基本值、对象或者函数
内部属性(属性的特性)在双方括号中[[Enumerable]]
有两种属性:数据属性和访问器属性。
数据属性就是平时用字面量定义的属性,有四个内部特性:
[[Configurable]]
:能否被delete
删除,能否修改属性特性,默认值为true
[[Enumberable]]
:是否可枚举(通过for...in
循环返回),默认值为true
[[Writable]]
:是否可以修改属性的值,默认值为true
[[Value]]
:包含这个属性的数据值,默认值是undefined
修改这些特性使用Object.defineProperty()
或者Object.defineProperties()
方法(多数情况下没有必要使用这个方法提供的高级功能)
var obj = {};
Object.defineProperties(obj, {
'property1': {
value: true,
writable: true
},
'property2': {
value: 'Hello',
writable: false
}
// etc. etc.
});
注意:一旦将configurable
置为false
, 就不能再将它变回可配置的,再调用Object.defineProperties()
方法修改除了writable
之外的任何特性,都会导致错误
访问器属性不包含数据值,包含getter函数和setter函数,包含四个属性
[[Configurable]]
:能否被delete
删除,能否修改属性特性,默认值为true
[[Enurmable]]
:是否可枚举(通过for...in
循环返回),默认值为true
[[Get]]
:读取属性时调用的函数,默认值为undefined
[[Set]]
:写入属性时调用的函数,默认值为undefined
使用访问器属性的常见方式是,设置一个属性值的时候,返回值根据要求变化,并且导致其他属性发生相应变化
使用Object.definePropertiers()
一次性定义多个属性,使用Object.getOwnPropertyDescriptors()
方法获取所有属性的描述符
创建对象的几种方式:
使用构造函数模式创建对象:
关键:实例的__proto__
属性指向构造函数的prototype
原型对象,这个连接存在于实例与原型之间,而非实例与构造函数之间
所以:
function Person(){
}
Person.age = 18;
const p = new Person();
console.log(p.age); // undefined
上面的age
属性不是定义在Person
的原型上的(相当于静态属性),所以p
是无法继承age
属性的。
可以通过isPrototypeoOf()
方法确定对象之间是否存在上面的关系,也可以通过Object.getPrototypeOf()
方法获取实例的原型
jay.__proto__ === Person.prototype
代码读取某个对象的某个属性时,会执行搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始。如果在实例找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象(也就是在其自身的__proto__
属性上寻找),在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这个属性,则返回该属性的值。
原型的constructor
属性也是可以被继承的,所以,实质上实例是没有constructor属性的,其constructor属性是继承自原型的:
console.log(jay.constructor === jay.__proto__.constructor === Person.prototype.constructor)
可以通过对象实例访问保存在原型中的值,但是不能通过对象实例重写原型中的值。在实例中添加与原型中同名的属性,不会覆盖原型中的属性,而是在实例中创建该属性,该属性将屏蔽原型中的那个属性:
function Person(name) {
}
Person.prototype.type = 'father;
const jay = new Person('jay');
jay.type = 'child';
console.log(jay.type) // child
可以通过delete
操作符删除实例属性,从而再次访问原型属性
通过hasOwnProperty()
方法检测属性是存在于原型中还是存在于实例中,in
操作符只能检测对象中是否可以访问该属性,而无法判断属性存在于原型还是实例。结合使用就可以判断对象是否存在以及存在的位置
function hasPrototypeProperty(object, key) {
return !object.hasOwnProperty(key) && (key in object)
}
对构造函数的原型进行赋值的时候,可以通过对象字面量的形式,一次性赋值。但是这会带来一个副作用,那就是原型对象的constructor
属性不再指向原来的构造函数了。这是因为每创建一个函数,就会同时创建其prototype
对象,这个对象会自动获得construcotr
属性。通过对象字面量形式对原型对象的赋值,本质上完全重写了默认的prototype
对象,因此其construcotr
属性就变成了新对象的construcotr
属性(指向Object
构造函数)
function Person(name) {
}
Person.prototyp = {
type: 'father'
};
console.log(Person.prototyp.constructor === Person); // false
console.log(Person.prototyp.constructor === Object); // true
可以特意将它重置:(注意,这样会导致constructor
属性变为可枚举的)
Person.prototyp.constructor = Person
可以随时为原型添加属性方法,并且修改能够立即在所有对象实例中反映出来。但是如果在已经创建了实例的情况下,重写了整个原型对象,那么情况就不同了。将原型修改为另外一个对象就等于切断了构造函数与最初原型之间的关系
function Person(name) {
}
const jay = new Person('jay');
Person.prototype = {
type: 'father'
};
console.log(jay.type); // undefined
原生对象的原型上定义了很多方法,也可以往上面添加新的自定义方法。但是不推荐在产品化的程序中修改原生对象的原型。原因是可能会导致命名冲突、并且可读性、可维护性都比较差。
混合模式就是组合使用构造函数模式和原型模式,利用构造函数创建实例属性,利用原型模式创建方法和共享属性。
原型链是实现继承的主要方法,最简单的继承就是让子类的原型对象等于父类的一个实例,实现的本质是重写子类的原型对象,代之以一个新类型的实例:
child.prototype = new Father()
原来存在于Father
实例中的所有属性和方法,现在也存在于child.prototype
中了
Child
的原型替换为Fahter
的实例后,Child.prototype
不仅拥有作为Father
实例的全部属性和方法,其内部还有一个指针,指向了Father.prototype
console.log(Child.prototype.__proto__ === Father.prototype); // true
要注意,所有函数的默认原型都是Object
的实例 ,因此默认原型都会包含一个内部指针,指向Object.prototype
console.log(Father.prototype.__proto__ === Object.prototype); // true
完整的原型链:
给子类的元吸顶添加方法,一定要放在替换原型后面,并且,不能使用对象字面量创建原型方法,因为这样会重写原型链,导致继承失效
存在的问题
在父类中构造函数中定义的引用类型(数组),会便成为子类的原型的属性,这样子类的实例都会共享这个属性,导致不同的子类实例都会修改这个属性。
为了解决这个问题,可以借用构造函数来实现继承,即在子类的构造函数内部通过call
或者apply
方法调用父类的构造函数,这样继承的是父类的实例属性,而非原型属性
function Father(name) {
this.colors = [1, 2, 3];
this.name = name
}
function Child(name) {
Father.call(this, name)
}
const child1 = new Child('jay');
const child2 = new Child('chow');
child1.colors.push(100);
console.log(child1.colors); // [1, 2, 3, 100]
console.log(child2.colors); // [1, 2, 3]
console.log(child1.name); // 'jay'
console.log(child2.name); // 'chow'
最常用的继承模式是组合继承,将原型链和借用构造函数的技术组合到一起:使用原型链实现对原型属性的继承,通过借用构造函数实现对实例属性的继承。
可以通过Object.create()
方法实现原型式继承,它接受两个参数,第一个是用作新对象原型的对象,第二个是为新对象对应额外数学归纳的对象(可选):
const a = {};
const b = Object.create(a);
console.log(b.__proto__ === a)
其实现是:
function create(object){
function F(){};
F.prorotype = object;
return new F()
}
也可以使用这个方法实现对象的拷贝:
const a = {a: 1};
const b = Object.create(Object.getPrototypeOf(a), Object.getOwnPropertyDescriptors(a));
b.a = 100;
console.log(a)
这种方式不需要显式的创建构造函数,只想让一个对象与另一个对象保持类似的情况下,原型式继承是可以的。但是包含引用类型值的属性都会共享相应的值。
创建函数有两种方式,一种是函数声明:
function test(){}
另外一种是函数表达式:
const test = function() {}
函数的name
属性是函数的名字,对于函数表达式来说,如果创建的匿名函数,name
会返回变量名,如果创建的不是匿名函数,name
返回函数名称
函数声明和函数表达式的最大区别是:函数声明存在着函数声明提升,函数表达式不会。
一个经典的递归阶乘函数:
function factorial(num) {
if (num <= 1) {
return 1;
}
return num * factorial(num - 1)
}
这里面的问题是在进行递归调用的时候使用了factorial
这个函数的函数名,如果函数指针发生变化,会导致在递归调用中发生错误。
解决的方法就是通过arguments.callee
来指向再执行的函数:
function factorial(num) {
if (num <= 1) {
return 1;
}
return num * arguments.callee(num - 1)
}
注意,在严格模式下是禁止访问arguments.callee
属性的。
创建闭包的常见方式就是在一个函数内部创建另一个函数。
this
对象this
对象是运行时基于函数的执行环境绑定的:在全局函数中,this
等于window
,而当函数作为某个对象的方法调用时,this
等于那个对象
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function() {
return function() {
return this.name;
};
}
};
alert(object.getNameFunc()()); //结果 The Window
由于this
始终表示对调用者的引用,object.getNameFunc()
的返回值是object
对象内部的匿名函数,这个匿名函数的调用者是window
,所以this
的指向就是window
,最后一句相当于打印的就是window.name
,结果是'The Window'
那么为什么返回的匿名函数没有取得其包含作用域的this
对象呢?
每个函数在被调用时都会自动取得两个特殊变量,this
和arguments
。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数的这两个变量。不过把外部作用域的this
对象保存在一个闭包能搞访问的变量里,就可以让闭包访问该对象了。
arguments
对象也有这样的问题。如果想反问作用域中的arguments
对象,必须将该对象的引用保存到另一个闭包能够访问的变量中。
用作模拟块级作用域(私有作用域)的匿名函数的语法:
(function({
// 这里是块级作用域
}))()
将函数声明包含在一对圆括号中,表示它实际上是一个函数表达式。
这种技术经常在全局作用域中被用在函数外部,从而限制想全局作用域中添加过多的变量和函数。一般来说,我们都应该尽量少想全局作用域中添加变量和函数。
函数的参数、局部变量和在函数内部定义的其他函数,都是函数的私有变量。
在函数内部创建一个闭包,闭包可以通过自己的作用域链访问这些变量。利用这一点,就可以创建用于访问变量的公有方法。
function Person(){
// 私有变量和私有方法
var name = 'name',
function say(){
alert('hello')
}
this.publicMethod = function() {
name = 'world';
return say()
}
}
创建了Person
的实例后,除了publicMethod
方法,没有任何办法可以直接访问私有变量。
私有变量在Person
的每一个实例中都不相同,因为每次调用构造函数都会重新创建公有方法。但是,必须使用构造函数模式来达到这个目的。
在私有作用域中定义私有变量或函数,也可以创建特权方法,基本模式如下:
(function(){
// 私有变量和私有函数
var privateVariable = 10;
function privateFunction(){
return false;
}
// 构造函数
Person = function(){}
Person.prototype.publicMethod = function(){
privateVariable++;
return privateFunction()
}
})()
这个模式创建了私有作用域,并且在其中封装了一个构造函数和相应的方法。构造函数是函数表达式,并且没有使用var
,所以构造函数在私有作用域外是可以访问的(函数声明只能创建局部函数)。对应的特权方法是定义在原型上的,所以每个实例都可以访问这个方法。
这种方式创建的静态私有变量,是被所有实例共享的,在一个实例上调用特权方法或者新建一个实例,都会影响私有变量。
模块模式是为单利创建私有变量和私有方法。单例值得就是只有一个实例的对象。
语法如下:
var singleton = function(){
// 私有变量和私有函数
var privateVariable = 10;
function privateFunction(){
return false;
}
return {
publicProperty: true,
publicMethod: function() {
privateVariable++;
return privateFunction()
}
}
}
这个模块模式使用了一个返回对象的匿名函数,返回的对象字面量中只包含可以公开的属性和方法。
BOM对象的核心是window
,它既是浏览器的一个实例,又是ECMAScript的Gblobal
对象。
全局作用域下声明的变量和函数,都是window
对象的成员
JavaScript是单线程语言,一定时间内只能执行一段代码。setTimeout
的第二个参数告诉JavaScript过多长时间将当前任务添加到队列。如果队列是空的,那么添加的代码会立即执行。如果队列不是空的,那么就要等到前面的代码执行完了以后再执行。
用除了replace
的方法外,浏览器都会生成新纪录,可以后退,replace
不可以
调用reload
方法,会使用缓存重载页面,传递参数reload(true)
会强制从服务器加载
通常用来检测显示网页的浏览器类型
navigator.plugins
数组可以用来检测浏览器是否安装了特定的插件
history保存着用户的上网记录,开发者无法得知用户浏览过的URL,同意通过history.go
方法来在用户的历史记录中跳转,参数可以是正负数,也可以是字符串。
history.length
保留着历史记录的数量。
页面包含其他子域的框架时,由于跨域,不同子域的页面不能通过JS通信,但是通过将每个页面的document.domain
设置为相同的值,这些页面就可以相互访问对方包含的JS对象了。
document.write
在页面呈现过程中输入的内容会追加到文档中,如果在文档加载结束后被调用,那么输出的内容将重写整个页面。
具体可以参考自己的这篇笔记
DocumentFragment是轻量级的文档,可以当做仓库使用,保存未来可能添加到文档中的节点。使用document.createDocumentFragment
方法创建文档片段。可以将多次反复的DOM操作现在文档片段中完成,然后一次性的将文档片段(即其中的节点)添加到文档中。
通过getElementById
类似的接口获取文档中的Node节点与DOM结构是一种动态的关系,返回的是NodeListe的动态引用,浏览器每次访问时都会从文档中读取集合的最新值:
let divs = document.getElementsByTagName('div');
for(let i=0; i<5; i++) {
document.body.appendChild(document.createElement('div'));
console.log(divs.length);// 1 2 3 4 5
}
但是通过document.querySelectorAll
获取的则是一个当前镜像值,返回的是NodeList的实例:
let divs = document.querySelectorAll('div');
for(let i=0; i<5; i++) {
document.body.appendChild(document.createElement('div'));
console.log(divs.length);// 0 0 0 0 0
}
DOM操作往往是JavaScript程序中开销最大的部分,最好的办法是尽量减少DOM操作。
querySelector
和querySelectorAll
可以通过document调用,也可以通过Element类型节点调用。可以避免对文档进行搜索的动态查询,进而避免性能问题。
访问返回的NodeList的成员,可以直接使用数组的中括号语法,也可以使用item()
方法
所有元素都有classList属性,可以直接操作对象的class,具体方法包括add
、containes
、remove
、toggle
document.activeElement
属性会引用DOM中获得了焦点的元素,document.hasFocus
确定文档是否获得了焦点。
想要了解文档是否加载完成,除了使用onload
事件外,也可以使用document.readyState
属性,loading
表示正在加载,complete
表示加载完成
document.head
用来引用文档的元素
可以为元素添加非标准的属性,但要添加前缀data-
,目的是为元素提供与渲染无关的信息,或者提供语义信息。
通过元素的dataset
属性来访问自定义属性的值。
innerHTML
返回内容outerHTML
返回标签加内容innerText
操作元素中所有文本内容HTML5中使用scrollIntoView
方法作为标准方法,所有元素都可以条用,调用后元素就会出现在视口中
传入true
或者不传入参数,窗口滚动后会让调用元素顶部与视图顶部尽可能平齐,如果传入false
,调用元素会尽可能全部出现在视口中
offsetWidth
= clientWidth
+ border
clientWidth
= width
+ padding
可以在document
对象上使用createEvent
创建event
对象,这个方法接受一个参数,即表示要创建的事件类型的字符串:
实际上
createEvent
方法已被废弃,应该使用MouseEvent
来代替
触发事件使用的是dispatchEvent
方法。
模拟按钮的单击事件:
const btn = document.querySelector('.logo');
const event = new MouseEvent('click', {
bubbles:true,
cancelable:true,
view:window
});
btn.dispatchEvent(event)
具体的参数参考这里。
同样的,模拟键盘事件应该使用KeyboardEvent
构造函数
event = new KeyboardEvent(typeArg, KeyboardEventInit);
模拟键盘事件的例子:
const event = new KeyboardEvent('keydown', {
altKey: true,
bubbles: true,
cancelable: true,
code: 'KeyK',
composed: true,
ctrlKey: true,
key: 'k',
metaKey: true,
repeat: true,
shiftKey: true,
view: window
})
document.addEventListener('keydown', (e) =>{
console.log(e.key)
})
document.dispatchEvent(event);
// k
利用select()
方法可以选择文本框中的全部文本
利用selectionStart
和selectionEnd
可以获得选取的起始位置
使用setSelectionRange(start, end)
可以选择start
至end之
间的文本,要看到选择的文本,必须在调用之前使用focus()
方法会令文本框获得焦点
有六个剪贴板事件:
beforeCopey
copy
beforeCut
cut
beforePaste
paste
访问剪贴板中的数据可以使用clipboardData
对象,这个对象有三个方法:
getData
setData
clearData
clipboardData
对象是一个DataTransfer对象,上面几个方法都需要指明类型,最常用的就是text
类型,更多的类型参考这里。
这里面的方法在实际使用时有一些处于安全、隐私方面的限制,并不是所有浏览器在所有情况下都可以正常工作的。
无非使用循环调用focus,在Vue中可能需要利用到自定义指令directives
,这样可以避免直接操作node
directives: {
focus: {
inserted(el, val) {
if (el && val.value) {
el.focus()
}
},
update(el, val) {
if (el && val.value) {
el.focus()
}
}
}
},
将表单项目和提交按钮type=submit
同时放到当中,点击按钮或者按下回车键会触发浏览器自带的表单验证功能,主要的验证类型有:
required
input
的类型,比如type=email
等min
/max
/step
等input
中增加pattern
字段,使用正则表达式来匹配输入的全部内容富文本编辑器可以通过iframe
设置designMode
属性实现,更常见的是通过contenteditable
实现
与富文本编辑器交互的主要方式是使用document.execCommand(aCommandName, aShowDefaultUI, aValueArgument)
,接受三个参数:
aCommandName
:一个DOMString ,命令的名称。可用命令列表请参阅命令 。aShowDefaultUI:
一个Boolean, 是否展示用户界面,一般为false
。aValueArgument
:一些命令(例如insertImage
)需要额外的参数(insertImage
需要提供插入image的url),默认为null
。获取富文本选区使用document.getSelection().toString()
方法,可以通过mouseup
事件触发
input(e) {
if(document.getSelection().toString()) {
console.log(document.getSelection().toString())
}
}
关于富文本,书里介绍的并不多,并且主要是基于iframe
实现的,现在更多的是基于contenteditable
实现,日后有相关需要再进一步研究吧。
必须先设置
width
和height
属性
绘制之前,需要先获取绘图上下文:
const drawing = document.querySelector('#drawing');
const context = drawing.getContext('2d');
通过toDataURL
可以到处在上绘制的元素
在2d上下文环境下,可以绘制矩形,原型等,详细的参考网站,感觉比书上的讲解更加清晰
Canvas是基于状态的绘制,如果在代码中更改颜色,然后一次性绘制stroke
,那么只会保留最后一次颜色,需要多次stroke
,但是为了防止覆盖,需要在每段绘制之前加上beginPath
,代表下次绘制的起始之处为beginPath()
之后的代码,让绘制方法不重复绘制
/**
* Canvas是基于状态的绘制
* 如果在代码中更改颜色,然后一次性绘制`stroke`,那么只会保留最后一次颜色需要多次`stroke`
* 但是为了防止覆盖,需要在每段绘制之前加上`beginPath`,代表下次绘制的起始之处为`beginPath()`之后的代码,让绘制方法不重复绘制
*/
// 表盘外围
ctx.beginPath();
const outerCircle = {
x: 250,
y: 100,
r: 80,
sAngel: 0,
endAngel: Math.PI * 2,
};
ctx.strokeStyle = 'red';
ctx.arc(outerCircle.x, outerCircle.y, outerCircle.r, outerCircle.sAngel, outerCircle.endAngel, false);
ctx.closePath();
ctx.stroke();
// 表盘内层
ctx.beginPath();
const innerCircle = {
x: 250,
y: 100,
r: 72,
sAngel: 0,
endAngel: Math.PI * 2,
};
ctx.strokeStyle = 'blue';
ctx.moveTo(outerCircle.x + innerCircle.r, innerCircle.y);
ctx.arc(innerCircle.x, innerCircle.y, innerCircle.r, innerCircle.sAngel, innerCircle.endAngel, false);
ctx.closePath();
ctx.stroke();
// 不移动原点
// // 时针
// const hourLine = {
// startX: 250,
// startY: 100,
// endX: 220,
// endY: 70,
// };
// ctx.moveTo(hourLine.startX,hourLine.startY);
// ctx.lineTo(hourLine.endX, hourLine.endY);
//
// // 分针
// const minuteLine = {
// startX: 250,
// startY: 100,
// endX: 300,
// endY: 70,
// };
// ctx.moveTo(minuteLine.startX,minuteLine.startY);
// ctx.lineTo(minuteLine.endX, minuteLine.endY);
// 移动原点
ctx.translate(innerCircle.x, innerCircle.y);
// 时针
ctx.beginPath();
ctx.strokeStyle = 'pink';
const hourLine = {
startX: 0,
startY: 0,
endX: -30,
endY: -30,
};
ctx.moveTo(hourLine.startX, hourLine.startY);
ctx.lineTo(hourLine.endX, hourLine.endY);
ctx.closePath();
ctx.stroke();
// 分针
ctx.beginPath();
ctx.strokeStyle = 'green';
const minuteLine = {
startX: 0,
startY: 0,
endX: 40,
endY: -40,
};
ctx.moveTo(minuteLine.startX, minuteLine.startY);
ctx.lineTo(minuteLine.endX, minuteLine.endY);
ctx.closePath();
ctx.stroke();
WebGl是针对Canavs的3D上下文
使用postMessage
可以页面与当前页面的元素或者当前页面弹出的窗口进行通信,当收到消息时,会触发
window
对象的message
事件
h1.addEventListener('dragstart', (e) => {
console.log('dragstart');
e.dataTransfer.setData('Text', e.target.id);
});
h1.addEventListener('dragend', () => {
console.log('dragend')
});
h1.addEventListener('drag', () => {
console.log('drag')
});
dragZone.addEventListener('dragenter', (e) => {
console.log('dragenter');
});
dragZone.addEventListener('dragleave', (e) => {
console.log('dragleave');
});
dragZone.addEventListener('dragover', (e) => {
console.log('dragover');
});
dragZone.addEventListener('drop', (e) => {
e.preventDefault();
const data = e.dataTransfer.getData('Text');
e.target.appendChild(document.getElementById(data));
})
在拖放过程中,事件对象的dataTransfer
属性,用于从被拖动元素向放置目标传递字符串格式的数据。
有两个方法,getData()
取得由setData()
保存的值,setData方法的的哥参数是表示保存数据类型的字符串,取值为text
或URL
用draggable
属性表示元素是否可以拖动,图像和链接的这个属性默认为true
。
在使用和
标签时,可以指定多个不同的媒体来源:
history.pushState(stateObj, title', url)
在历史状态栈中加入一个新的状态信息,地址栏会发生变化,但是当前页面状态不会重置,而history.replaceState(stateObj, title)
则会重写当前页面状态,但不会在历史状态栈中创建新状态
二者都不会导致页面的刷新
try-catch
语句是JS中处理异常的一种标准方式:
try {
// 可能导致错误
} catch (err) {
// 在错误发生如何处理
}
err
对象的message
属性把偶才能这错误信息
只要包含finally
语句,无论是try
还是catch
语句块中的return
都会被忽略
try-catch
语句适合处理我们无法控制的错误,如果明明白白知道自己的代码会发生错误时,就不应该使用了。
throw
操作符用于随时抛出自定义错误,后面必须接着一个值
throw
后面的代码会立即停止执行,仅有try-catch
语句捕获到抛出的值时,代码才会继续执行
如果是编写一个JS库,或者是可能在程序内部多个地方使用的辅助函数,应该在可能发生错误时抛出一个错误,给出详尽的信息,而不是静默的失败。
onerror
事件只能使用DOM0级技术,没有遵循DOM2级事件的标准格式
windowl.onerror = function (message, url, line) {
// do something
}
可以使用下面这个函数来讲数据写入服务器:
function logError(sev, msg) {
const img = new Image();
img.src = 'log.php?sev=' + encodeURIComponent(sev) + '&msg=' + encodeURIComponent(msg)
}
这个函数的灵活指出在于:
JSON对象有两个方法,stringify
用来把JavaScript对象序列化为JSON字符串,parse
用来吧JSON字符串解析为原生JavaScript值
在使用stringify
序列化JavaScript对象时,所有函数和原型成员都会被忽略,并且值为undefined
的属性也会被跳过
JSON.stringify()
的第二个参数是过滤器,可以是数组,也可以是函数,如果是数组,那么序列化的结果将只包含数组中列出的属性
const a = {a:123, b: function(){alert(123)}, c: [1,2,3]}
JSON.stringify(a, ['a'])
// "{"a":123}"
如果是函数,该函数接受两个参数,键名和属性值,根据处理结果返回对应值
JSON.stringify()
的第三个参数是控制结果中的缩进和空白符,可以传入数字,也可以传入字符串,作为制表符
const a = {a:123, b: function(){alert(123)}, c: [1,2,3]}
JSON.stringify(a, ['a'], 2)
"{
"a": 123
}"
JSON.stringify(a, ['a'], '++')
"{
++"a": 123
}"
JSON.stringify()
也可以接受第二个参数,也是一个过滤函数,效果和上面相同
面试最爱考的,先手写一个原生的Ajax请求
const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://yesno.wtf/api');
xhr.onreadystatechange = () = > {
if (xhr.status === 200 && xhr.readyState === 4) {
console.log(xhr.response)
}
};
xhr.send(null)
open
方法没有真正发送请求,而是启动一个请求以备发送。接受三个参数,第一个是HTTP请求方法,第二个是URL,第三个是否异步发送请求(默认是true
,以异步的方式发送请求)
send
方法是实际上发送请求的方法,接受一个参数,是作为请求主体发送的数据,如果不需要通过请求主体发送数据必须传入null
发送请求后,服务器的响应数据会作为XMLHttpRequest对象的属性返回来,status
是成功的标志,readyState
属性表示请求/响应过程的当前活动阶段:
0
,未初始化,未调用open
方法1
,启动,调用open
方法,未调用send
方法2
,发送,调用了send
方法,但未收到响应3
,接受,接收到部分响应数据4
, 完成,收到全部响应数据每次readyState
属性值发生变化,都会触发readystatechange
事件,与其他事件处理程序不同,这里没有向readystatechange
事件处理程序传递event
对象,必须通过XHR对象本身确定下一步该怎么做(也可以使用this
对象,但是出于兼容性、可靠性的考虑,使用XHR对象更加稳妥)
在接收到响应之前,可以通过调用abort
方法取消异步请求
请求头:(Request Header)
可以调用xhr.setRequestHeader()
方法重写请求头
xhr.setRequestHeader('myHeader', 'myValue');
自定义一些header属性进行跨域请求时,可能会报错,你可能需要在你的服务端设置Access-Control-Allow-Headers
。
可以通过XHR对象的getAllResponseHeaders
方法可以获取全部响应头信息,getResponseHeader(name)
可以获取对应的响应头信息
响应头:
XMLHttpRequest Level2新添加的接口,利用FormData
对象,可以通过一些键值对来模拟一系列表单空间,可以用XMLHttpRequest的send
方法来提交表单。
const formData = new FormData();
formData.append("username", "Groucho");
formData.append("accountnum", 123456); //数字123456会被立即转换成字符串 "123456"
// HTML 文件类型input,由用户选择
formData.append("userfile", fileInputElement.files[0]);
// Blob对象
const content = 'hey!'; // 新文件的正文...
const blob = new Blob([content], { type: "text/xml"});
formData.append("webmasterfile", blob);
const request = new XMLHttpRequest();
request.open("POST", "http://foo.com/submitform.php");
request.send(formData);
XMLHttpRequest Level2中,XHR对象新增加了一个timeout
属性,如果在规定的时间内浏览器没有收到响应,那么就会触发timeout事件,进而调用ontimeout
事件处理程序
xhr.timeout = 500;
xhr.ontimeout = function () {
alert('超时了')
};
有六个进度事件:
loadstart
:收到相应数据的第一个字节时触发progress
:在接受响应期间不断触发error
:在请求发生错误时触发abort
:在因调用abort
方法而终止连接时触发load
:在接收到完整的相应数据时触发loadend
:在通信完成或者触发error
、abort
或load
事件后触发每个XHR请求都从loadstart
事件开始,接下来是一个或者多个progress
事件,然后触发load
/error
/abort
中的一个,最后以触发loadend
事件结束
可以使用load
事件简化原生XHR请求,无需再判断readyState
和status
的状态了
xhr.onload = function(e) {
console.log(xhr.responseText);
}
在进度事件中,函数会接收到一个event
对象,其target
属性就指向XHR实例,因为也可以直接用e.responseText
来代替
progress
事件onprogress
事件会接收到一个event
对象,其target
属性是XHR对象,包含三个额外的属性:
lengthComputable
:表示进度信息是否可用,loaded
:已经接受的字节数(原书中的position
已被废弃)total
:表示预期总字节数(原书中的totalSize
已被废弃)const xmlhttp = new XMLHttpRequest(),
method = 'GET',
url = 'https://developer.mozilla.org/';
xmlhttp.open(method, url, true);
xmlhttp.onprogress = function (event) {
//do something
const progressRatio = event.loaded / event.total
};
xmlhttp.send();
跨域的原因:浏览器的同源策略,协议
+主域
+二级域名
+端
口,任一不同都算跨域
CORS,Cross-Orgin Resource Sharing,用来定义必须访问跨域资源时,浏览器和服务器如何进行沟通,实质上是通过HTTP的头信息完成的这个沟通过程
在HTTP请求的头信息中添加Origin: http://www.baidu.com
(除IE外,其余浏览器会自动添加这个头部字段),服务器如果认为这个请求是可以接受的的,就会在响应头中添加Access-Control-Allow-Origin: http://www.baidul.com
请求和响应都不包含cookie信息
建议访问本地资源时,最好使用相对URL,访问远程资源再使用绝对URL,可以消除歧义
OPTIONS
对于值了GET/POST之外的方法进行跨域(称为非简单请求)需要浏览器使用OPTIONS
方法向服务发送预检请求
预检请求发送一下几个头部:
Origin
,与简单请求相同Access-Control-Request-Method
,请求使用的方法Access-Control-Request-Headers
,自定义的头部信息服务器的响应头会包含:
Access-Control-Allow-Orgin
Access-Control-Request-Methods
Access-Control-Request-Headers
Access-Control-Max-Age
预检请求结束后,浏览器会按照上面的第4项将遇见请求缓存起来
默认情况下,跨域请求不提供凭据(cookie,HTTP认证或者客户端SSL证明),通过将withCredentials
设为true
,可以指定某个请求应携带凭据
服务器如果接受,会返回下面的响应头字段:
Access-Control-Allow-Credentials: true
标签不存在跨域的问题,可以访问任意其他网页的图片,并且可以通过onload
和onerror
事件了解到响应是何时完成的
let img = new Image();
img.onload = img.onerror = function () {
alert('done')
}
img.src = 'http://www.baidu.com/test?name=123'
当img
的src
被设置那一刻,请求就发出了,服务器的响应一般是像素图或204 No-Content
一般用来跟踪用户点击页面或动态广告曝光字数,是一种单向的跨域方式,只能是GET请求,并且无法访问服务器的相应文本。
JSONP,JSON with Padding实际上利用的是
标签可以不受跨域限制的特定进行的跨域,使用JSONP时相当于动态创建了下面的标签
服务器会读取callback
后面的参数,并且执行handlerResponse
回调函数,将数据在这个函数中传递过来
JSONP可以直接访问响应文本,支持客户端与服务器之间的双向通信
JSONP的两个问题:
标签的onerror
事件未获得广泛支持用于模拟服务器主动推送的技术,其实也就是长轮询:页面发起一个到服务器的请求,然后服务器保持连接打开,将请求挂起,知道有数据可发送时对请求进行响应,发送完数据后连接断开,浏览器会又发起一个新的请求。
这个过程在页面打开期间一直持续不断。
WebSocket会用一个HTTP请求简历连接,在取得服务器后连接会从HTTP协议升级为Websocket协议
Websocket使用了自定义的协议ws://
和wss://
,因为使用了自定义协议,就不需要再向HTTP协议那样发送请求头,传递的数据包更小,适合移动应用
Websocket建立的连接不存在跨域问题,因此可以通过建立Websocket打开到任何站点的俩进阶,至于是否可以与页面通信,完全取决于服务器(通过握手信息就可以知道请求来自何方)
SSE(Sever-Sent Events)是单向通道,只能服务器向浏览器发送,因为流信息本质上就是下载。如果浏览器向服务器发送信息,就变成了另一次HTTP请求。
相比于Websocket,SSE有以下的有优点:
基本实现:
if(window.EventSource) {
// 参数是一个URL,可以使与当前网址同域,也可以跨域
// 打开withCredentials属性,表示是否一起发送Cookie。
const source = new EventSource('myEvent.com', { withCredentials: true});
// EventSource实例的readyState属性,表明连接的当前状态
if(source.readyState === 0) {
// 0 相当于常量EventSource.CONNECTING,表示连接还未建立,或者断线正在重连。
} else if (source.readyState === 1) {
// 1 相当于常量EventSource.OPEN,表示连接已经建立,可以接受数据。
} else if (source.readyState === 2) {
// 2 相当于常量EventSource.CLOSED,表示连接已断,且不会重连。
}
// 连接一旦建立,就会触发open事件,可以在onopen属性定义回调函数。
source.addEventListener('open', function (event) {
// ...
}, false);
// 客户端收到服务器发来的数据,就会触发message事件,可以在onmessage属性的回调函数。
source.addEventListener('message', function (event) {
var data = event.data;
// handle message
}, false);
// 如果发生通信错误(比如连接中断),就会触发error事件,可以在onerror属性定义回调函数。
source.addEventListener('error', function (event) {
// handle error event
}, false);
// close方法用于关闭 SSE 连接。
source.close();
服务端传输的数据类型为:"Content-Type":"text/event-stream"
实例也可以监听自定义事件,服务器触发参考阮一峰的网络日志。
如何选择:
为了确保通过XHR访问的URL的安全,通行的做法就是验证发送请求者是否有权限访问相应的资源。
之前面试的时候就遇到这个问题,我知道几种检测类型的方法,都存在各自的问题:
typeof
会返回很多不准确的结果instanceof
操作符对存在多个全局作用域的情况(例如一个页面包含多个frame),判断也会出错,并且instanceof
只对引用类型有效,对于基本类型字面量是无效的constructor
方法是一个可以被改写的属性所以解决方法就是调用Object对象的toString
方法,返回值是一个[object NativeContructorNmae]
格式的字符串:
Object.prototype.toString.call([]); // "[object Array]"
注意,Object.prototype.toString
也能被修改,所以要慎重啊!
实际上前些天的面试也遇到过这个问题,一个构造函数:
function Person(name) {
this.name = name
}
当不用new
操作符调用它时,会导致this
意外的绑定到window
对象上,导致出现问题
所以关键点就是要在函数内部,判断函数是直接调用还是用作构造函数,这两者的区别就是在于this
,是不是正确类型的实例
function Person(name) {
if (this instanceof Person) {
this.name = name
} else {
return new Person(name)
}
}
但是这回导致通过call
实现的实例属性的继承出现问题:
function Man(age) {
Person.call(this, 'jay');
this.age = age
}
const p1 = new Man(18);
p1.name // undefined
这是因为,在Man的内部调用Person.call
时,这个this
是Man
的实例,但是它并不是Person
的实例,所以Person
会返回一个新的Person
实例对象,所以Person
的name
属性加到了Person
的实例对象上,而Man
的实例对象p1
的this
并没有添加这个属性
这种情况下综合使用原型链继承就能够解决这个问题
function Man(age) {
Person.call(this, 'jay');
this.age = age
}
Man.prototype = new Person()
// 或者Man.prototype = Person.prototype
Man.constructor.name = Man
const p1 = new Man(18);
p1.name
一个函数中,如果有多个if
语句,每次执行都会判断,但是有些时候这些判断是一开始就会确定的,每次执行都进行判断,会导致性能的浪费,比如:
let func = () => {
if (window.addEventListener) {
console.log(1)
} else {
console.log(2)
}
}
惰性载入表示函数执行的分支仅会发生一次。有两种实现惰性载入的方式,第一个种就是在函数被调用时处理函数,在函数第一次调用时,函数背覆盖为另外一个按合适方式执行的函数(在第一次执行时改写函数)
let func = () => {
if (window.addEventListener) {
func = () => {
console.log(1)
}
} else {
func = () => {
console.log(2)
}
}
}
第二种实现惰性载入的方式是在声明函数时就指定适当的函数(在函数声明时改写函数),利用了一个自执行函数
let func = (() => {
if (window.addEventListener) {
return () => {
console.log(1)
}
} else {
return () => {
console.log(2)
}
}
})()
感觉在判断兼容性环境时用处比较大。
function curry(fn, context){
const args = [].slice.call(arguments, 2);
return function() {
const innerArgs = [].slice.call(arguments);
const finalArgs = args.concat(innerArgs);
return fn.apply(context, finalArgs)
}
}
可以参考这道题目。
注意,bind
方法是实现了函数柯里化的方法,可以在使用bind
时传入参数,也可以在新的函数调用时传入剩余的参数。
可以通过更改对象属性的描述属性来实现属性的冻结
也可以使用Object.freeze
实现对象的冻结
对于编写一个库时对象冻结是很用的。
定时器的工作方式是,当特定的时间过去后将代码插入,插入并不意味着立刻执行,只能代表它在队列中没有其他任务时执行。
避免在复杂的场景使用setInterval
,因为定时器代码可能在代码再次被添加到队列之前还没有完成执行,结果导致定时器代码连续运行几次,而之间没有停顿
用如下模式使用链式setTimeout
调用代替setInterval
:
setTimeout(function() {
// 处理中
setTimeout(arguments.callee, interval);
}, interval)
函数节流是的思想是,某些代码不可以在没有间断的情况下连续重复执行。第一次调用函数会创建一个定时器,在指定的时间间隔后运行代码。第二次调用该函数时,它会清除前一次的定时器,并设置另一个。
function throttle(fn, context) {
clearTimeout(fn.timer);
fn.timer = setTimeout(() => {
fn.call(context)
}, 100)
}
可以返回一个函数:
function throttle(fn, context) {
let timer = null;
return function () {
if (timer) {
clearTimeout(timer);
}
timer = null;
timer = setTimeout(() => {
fn.call(context)
}, 1000)
}
}
这里介绍的是观察者模式的实现
原来这里就有,第一遍看的时候这些内容全都略过去了,根本没看,估计看也是看不懂的。
但是因为这些,面试的时候吃了好大亏,现在回过头看,已经都明白怎么回事了,继续加油吧
原来实现过一个,这次改成了组件的形式,参考《JS模块14 可拖动DIV》
使用navigator.online
属性来检测设备是否可以访问网络
此外,HTML5还定义了两个事件offline
和online
,当网络从离线变为在线或者相反的过程中,会分别触发这两个事件
window.addEventListener('offline', () => {
console.log('offline')
})
'offline'
window.addEventListener('online', () => {
console.log('online')
})
'offline'
所以实际操作中,应该首先通过navigator.online
获得初始状态,然后通过监听offline
和online
这两个事件来确定网络连接的状态的变化
略
cookie是绑定在特定的域名下的,当设定了一个cookie后,再给创建它的域名发送请求时,都会包含这个cookie
每个域名下的cookie数目是有限制的,超过限制后浏览器会清除以前设置的cookie
cookie的构成:
name
value
domain
path
expires
max-age
http-only
secure
具体的内容前一阵子学习《图解HTTP》刚刚学过
JavaScript存入cookie时应该是用URL编码encodeURIComponent
,读取的时候也应该使用decodeURIComponent
解码
a=b; c=d; x=y
给document.cookie
赋值时,除非名称已经存在,否则不会覆盖
略
indexedDB可以参考阮一峰的教程和这篇文章。
可维护性的代码需要遵循的特点:
(1)可读性,缩进用空格代替制表符,并且统一,在一些地方(函数方法、大段代码、复杂的算法、Hack)需要写注释
(2)变量和函数命名,变量名为名词,函数以动词开始,返回布尔值的以is
开始,不必担心长度
(3)变量类型透明,定义了一个变量后应该被初始化为一个值,暗示它将来应如何使用
var found = false; // 布尔型
var count = -1; // 数字
var name = ''; // 字符串
var person = null; // 对象
书里介绍的其他两种方法(匈牙利标记法、类型注释法)的下搜过都不是很好,真要是实现还不如直接使用Flow.js
(1)解耦HTML/JS
这一点目前来看由于框架的使用,好像不是那么适用,无论是React还是Vue,都是用JS编写模板
(2)解耦CSS/JS
这一点有些情况也不太适应,比如CSS IN JS,用JS写CSS,紧密耦合了
不过还是有一些准则要遵守的,尽量修改元素的CSS类,来代替直接修改元素样式
(3)解耦应用逻辑/事件处理程序
绑定事件后,指定单独的事件处理程序
最佳的方法是永远不修改不是由你所有的对象。
增加命名空间(全局变量的一个对象),其余的属性和方法都定义在这个对象内
null
比较一步到位,使用Object.prototype.toString.call
就完事了
使用常量的场景:
with
(1)查找属性时,需要考虑算法复杂度,避免重复查找
对属性的直接访问和对数组元素的访问的复杂度都是O(1)
对对象的属性的访问的复杂度是O(n)
,所以,对多重属性的多次查找,应该将它缓存在变量中
for (let i = 0; i < 100; i++) {
console.log(window.location.href)
}
这样会导致多次访问属性,所以应该进行缓存:
const href = window.locaiton.href;
for (let i = 0; i < 100; i++) {
console.log(href)
}
(2)优化循环
有实践意义的也就是简化循环体,保证没必要的语句移出循环
(3)展开循环
用直接调用方法来代替循环,我认为现在这种优化基本上没有什么意义了
(3)避免双重解释
就是避免使用eval
、new Function
、setTimeout
这三者都是可以将字符串参数变成代码直接执行的方法,JavaScript引擎会进行双重解释,导致性能差
但是,如果有特殊的需求,不用也不行啊,除此之外当然可以尽量避免。
用文档片段document.createDocumentFragment
放置新创建的项目,一次性插入DOM,减少DOM操作
innerHTML
构建好一个字符串,一次性调用innerHTML
但是要考虑输入来源,如果是外部输入,一定要考虑XSS攻击
尽量减少JS的使用,使用标签会阻塞DOM的构建和渲染
其他的都不适用了,现在是webpack的天下了。
现在是ESLint的天下了,JSLint不能配置,JSHint配置混乱
(1)文件压缩:
Webpack通过插件的形式接入UglifyJS实现压缩JSS,通过css-loader实现压缩CSS,通过html-webpack-plugin压缩HTML
(2)HTTP压缩
服务器利用Gzip压缩,现在比较新的压缩算法是Brotli(Br)
requestAnimationFrame
动画循环的最佳循环间隔是 1000ms / 60(刷新率)
等于17ms,但是无论是setTimeout
还是setInterval
,执行时间都不精确,第二个参数只是指定了把动画添加到浏览器的UI线程队列中并等待执行的时间。
知道什么时候绘制下一帧是保证动画平滑的关键。
浏览器的计时器精度为4ms
所以window.requestAnimationFrame()
出现了,它告诉浏览器我们希望执行动画,并请求浏览器在下一次重绘之前调用指定的函数来更新动画。
方法的返回值是一个请求ID,可以传递给window.cancelAnimationFrame()
用来取消动画执行
requestAnimationFrame
方法使用一个回调函数:
window.requestAnimationFrame(callback);
callback
会在浏览器重绘之前调用。注意,requestAnimationFrame
只会执行一次,想要实现连续的动画,必须在callback
里面手动再次调用requestAnimationFrame
方法
在大多数浏览器里,当运行在后台标签页或者隐藏的里时,
requestAnimationFrame()
会暂停调用以提升性能和电池寿命。
callback
会接受一个参数,它是一个时间戳,表示下一次重绘发生的时间,两次传入时间戳的差值,就是在屏幕上重绘下一组变化前要经过多长时间
我理解,传入这个参数的目的并不是让我们知道并且指定下一次重绘的时机,它是由浏览器确定的,在下一次重绘前执行,而是让我们有能力知道,动画执行了多久。
一个使用的例子:
var start = null;
var element = document.getElementById('SomeElementYouWantToAnimate');
element.style.position = 'absolute';
function step(timestamp) {
// 这里的start是初始值
if (!start) {
start = timestamp;
}
var progress = timestamp - start;
// 移动到200px结束
element.style.left = Math.min(progress / 10, 200) + 'px';
// 动画执行2s,2s后停止动画
if (progress < 2000) {
window.requestAnimationFrame(step);
}
}
window.requestAnimationFrame(step);
一般情况下,开发者要知道用户正在离开页面,一般会监听onbeforeunload
事件:
window.onbeforeunload = function (e) {
return false
};
事件返回的字符串不为null
或undefined
时,会弹出对话框让用户自行选择是否关闭当前页面。
但是这些事件在手机上可能不会触发,因为手机系统可能会将一个进程直接转入后台然后杀死,开发者是无法得知页面被切换或者进程被清除的,也就是说,开发者不能指定任何一种页面卸载情况下都会执行的代码
Page Visibility API就是为了监听页面可见性而出现的。
新增了一个dcoument.visibility
属性,返回一个字符串,表示当前页面的可见性,有三个取值visibile
/hidden
/prerender
页面卸载之前,这个属性一定会变成hidden
只要document.visibility
属性发生变化,就会触发document.visibilitychange
事件,可以通过监听这个事件跟踪页面的可见性变化
window.addEventListener('visibilitychange', (e) => {
if (document.visibilityState === 'visible') {
document.title = '回来了嘻嘻'
} else {
document.title = '不可见了!!!!!!!!!!!!!'
}
})
可以用这个事件来代替其他的事件监听页面可见性状态的改变,而onbeforeunload
事件只适用于在用户填写了表单未保存而要关闭页面时。
通过Geolocation API,JavaScript可以获得用户当前的位置信息(需要得到用户明确许可)
这个API在浏览器的实现是==navigator.geolocation
对==象,包含三个方法:
(1)getCurrentPosition()
方法,会触发请求用户共享地理定位信息的对话框,接受三个参数,分别是成功回调函数,可选的失败回调函数和可选的选项对象
成功回调函数会接受一个Position对象参数,改对象有两个属性coords
和timestamp
,coords
包含latitude
(十进制维度)和longitude
(十进制经度)
navigator.geolocation.getCurrentPosition((position) => {
console.log(position.coords.latitude);
console.log(position.coords.longitude);
})
失败回调函数的参数包含的对象有两个属性,message
和code
,用来说明错误的原因
第三个参数是一个选项对象,用于设定信息类型,可以设置获取信息的监督、等待时长等。
(2)watchPosition()
方法用来跟踪用户位置,接受参数与getCurrentPosition()
方法相同。实际上,watchPosition()
方法与定时调用getCurrentPosition()
方法效果相同。它的返回值是一个ID,可以传给clearWatch()
方法来取消跟踪监控
88888 #### File API
通过使用,监听
change
事件,获取事件的files
属性,可以知道选择的文件信息
document.querySelector('#input').addEventListener('change', e => {
console.log(e.target.files)
})
files
对象是一个类数组对象,每个成员都有下列属性name
/size
/type
/lastModifiedDate
然后可以通过FileReader
对象实现对上面的file
对象的处理,它提供了以下方法:
readAsText(file, encoding)
,以纯文本方式读取文件,将读取的文本保存到result
属性中,一般用来读取文本reaAsDataURL(file)
,读取文件并且以数据URI的形式保存在result
属性中, 一般用来读取图片readAsBinaryString(file)
,读取文件并将字符串保存在reulst
属性中(已被废弃)readAsArrayBuffer(file)
,读取文件并且将一个包含文件内容的ArrayBuffer保存在reulst
属性中,一般用来读取二进制数据(Blob对象)FileReader
提供的方法都是异步读取的,所以需要通过load
/error
/progress
事件的回调函数来获取读取状态和结果
document.querySelector('#input').addEventListener('change', e => {
const file = e.target.files[0];
const {type} = file;
const reader = new FileReader();
if (/image/.test(type)) {
// 图片类型
reader.readAsDataURL(e.target.files[0]);
reader.onload = () => {
const image = new Image();
image.src = reader.result;
document.body.appendChild(image);
}
} else if (/text/.test(type)) {
// 文本类型
reader.readAsText(e.target.files[0]);
reader.onload = () => {
const p = document.createElement('p');
p.innerText = reader.result;
document.body.appendChild(p);
}
}
reader.onerror = () => {
alert('something wrong')
};
reader.onprogress = (e) => {
console.log(e.loaded / e.total)
};
})
此外,还可以调用abort
方法中断读取过程。
读取过程中,可以通过slice
方法,读取一部分内容。
读取文件还可以使用对象URL,也成为blob URL,指的是引用保存在File或者Blob中数据的URL,它的好处是可以不必把文件内容读取到JavaScript中直接使用文件内容。
只要在需要文件的地方提供对象URL即可,它是一个字符串,指向一块内存的地址,在DOM中可以直接使用
blob:http://localhost:63342/b3afe3d9-a5c4-4c0a-8cae-3f65b66b6781
使用window.URL.createObjectURL()
方法创建对象URL,页面卸载时会自动释放对象URL占用的内存,如果不再需要响应数据,最好使用window.URL.revokeObjectURL()
手动释放它占用的内容,
draggable
属性)
ondragstart
:开始拖动时触发ondrag
:拖动时触发ondragend
:拖动完成时触发ondragenter
:进入容器范围时触发ondragover
:拖动时触发(触发间隔350毫秒)ondragleave
:离开容器范围时触发ondrop
:拖动过程中,释放鼠标按键时触发
释放元素(或文件)的相关的事件是drop
事件,拖动的文件可以在e.target.files
中获取到
const drag = document.querySelector('#drag')
drag.addEventListener('dragenter', (e) => {
e.preventDefault()
});
drag.addEventListener('dragover', (e) => {
e.preventDefault()
});
drag.addEventListener("dragleave", function(e) { //拖离
e.preventDefault();
});
drag.addEventListener('drop', (e) => {
e.preventDefault();
e.stopPropagation();
const url = window.URL.createObjectURL(e.dataTransfer.files[0]);
let image = new Image();
image.src = url
drag.appendChild(image)
})
实现拖动必须取消dragenter
/dragover
/dragleave
/drop
的默认行为
可以使用formData
来上传文件,然后将新建的包含上海窜的文件信息的formData
放到xhr.send
方法中,使用POST
发送
let formData = new FormData();
formData.append('file', e.dataTransfer.files[0]);
let xhr = new XMLHttpRequest();
xhr.open('post', 'example.php');
xhr.onreadystatechange = () => {
if(xhr.readyState === 4 && xhr.status === 200) {
console.log(xhr.responseText)
}
};
xhr.send(formData)
发送文件时,如果不需要预览则不需要通过fileReader
读取
这里说的Web计时机制指的是对页面性能的考量标准,核心是window.performance
对象,对页面的所有度量信息都包含在这个对象里面。
有两个属性,一个是performance.navigation
,表示在当前页面重定向、导航的此时等,另一个是属性是performance.timing
属性,它是一个时间戳,包含页面各种事件(比如导航到当前页面、重定向结束等)的时间
通过这些时间值就能了解页面被加载到浏览器的过程中都经历了哪些阶段,哪些阶段可能是影响性能的瓶颈。
Web Worker的作用就是为JavaScript创造多线程环境,允许主线程创建Worker线程,将一些任务分配给后者运行。
后台线程和主线程分别运行,互不干扰,这样后台线程可以随时响应主线程的通信,主线程可以将一些复杂的计算任务交给后台线程,完成之后再把结果返回给主线程
Worker线程一旦创建成功就会始终运行,但是这就会比较浪费资源,所以不应该过渡使用,而且一旦使用完毕,就行该立刻关闭。
使用Web的注意点:
document
/window
,可以使用loction
/navigator
alert
和confirm
,但可以发出AJAX请求新建Worker线程:
let worker = new Worker('work.js');
给worker传递消息是通过postMessage
事件完成的,这种通信是拷贝关系,即是传值而不是传址,Worker对通信内容的修改,不会影响到主线程。事实上,浏览器内部的运行机制是,先将通信内容串行化,然后把串行化后的字符串发给Worker,后者再将它还原。
worker.postMessage('hello world');
对二进制的传递,为了避免大量数据复制造成的性能问题,JavaScript允许主线程将二进制数据直接转移给子线程,同时也会将所有权传递给子线程(避免两个进程同时修改一份数据)
这就是postMessage的最后一个参数
transfer
的目的
worker通过监听message事件,接受主线程传递的数据,子线程的self
和this
引用都是worker自身作为自己的全局对象
self.addEventListener('message', e => {
self.postMessage('\nyou said ' + e.data)
})
Worker内部的代码在执行过程中遇到错误,就会触发error
事件,事件对象包含三个属性:filename
/lineno
/message
建议在使用Web Worker时,始终要使用onerror事件
,避免察觉不到失败
调用terminate
方法会停止Worker的工作,其中代码会立刻停止执行,后续过程不再发生(包括message
事件和error
事件)
一般会把大量数据处理(比如排序)、图像处理(比如彩色图像转换为灰阶图像)和加解密等耗费时间的任务交给Worker处理
Worker内部引用其他的脚本,需要使用importScripts
方法:
importScripts('a.js', 'b.js')
下载完成后会按照a、b的顺序执行
更多的参考阮一峰的文章。