这一遍的重点还是在JS语言本身,后面的DOM和BOM部分看的比较快,因为现在实际上用的不多,掌握大致的原理,需要的时候再翻手册就可以了。
认为暂时没必要的知识点WebGL,
认为已经不需要、过时的知识/XML/E4X
终于在2018年的末尾把这个书又看了一遍,还是有些进步的,2019,加油(2018.12.28)
第一章 JavaScript简介
JavaScript组成部分:
- ECMAScript,有ECMA-262定义,提供核心语言功能
- DOM(文档对象模型),提供访问和操作网页的方法和接口
- BOM(浏览器对象模型),提供与浏览器教育的方法和接口
第二章 在HTML中使用JavaScript
<script>标签的解析是阻塞式的,如果放在<head>中,会导致浏览器为了解析脚本,使页面空白时间太长,解决方式是将<script>标签放在<body>内部末尾
<script>标签的defer和async属性都是为了是异步解析脚本的,不同点是:defer是将标签延迟到</html>后执行,并且按照出现的先后顺序执行(实际上虽然并不是这样),而async则不保证执行顺序
第三章 基本概念
数据类型
五种基本数据类型:string, number, boolean, undefiend, null,一种复合数据类型object
ES6中新增了一种基本类型
Symbol
typeof 操作符
typeof null; // 'object'
typeof function test(){}; // 'function'
typeof NaN; // 'number'
null
是一个空对象指针
Boolean
JS中的假值: null,undefined, 0, false, NaN, '',
Number
浮点数值计算存在误差,导致了在JS中0.1 + 0.2 = 0.30000000004
NaN
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→0undefined→NaN- 字符串:
- 省略前置的0(忽略八进制)
- 将
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
String类型
-
\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'
Object类型的属性和方法
-
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
}
负数二进制表达式
- 首先给出对应的正值的二进制表达式
- 求反码
- 求补码(+1)
// -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来释放其引用,这种做法叫做解除引用。
解除引用并不意味可以自动回收其所占内存,真正作用是让值脱离执行环境,便于垃圾收集器下次运行是将其回收。
第五章 引用类型
Object类型
两种创建方式:
new Object()- 对象字面量
Array类型
构造函数
使用构造函数创建数组时,传入单个参数和多个参数表示意义不同,传入单个参数表示数组的长度,数组成员是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个参数,分别是前一个值,当前值、当前项索引和数组对象
Date类型
使用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
比较日期时需要将日期转换为毫秒进行比较,较早的日期数值较小。
RegExp类型
匹配模式包含:
-
g:全局模式 -
i:不区分大小写 -
m:多行模式
创建正则表达式的两种方式:
let reg = /.at/i
let reg2 = new RegExp('.at', 'i')
RegExp.exec()方法
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()方法
RegExp.test()方法接受字符串作为参数,在正则表达式与该参数匹配的情况下返回true,否则返回false,常用在if语句中
RegExp构造函数属性
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类型
函数是对象,函数名是指向函数对象的指针
定义函数的三种方式:
- 函数声明
- 函数表达式
- Function构造函数(最后一个参数是函数体)(不推荐的方式)
// 函数声明
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属性
arguments是一个类数组对象,包含着传入函数中的所有参数,它还有一个名叫callee的属性,指向拥有这个arguments对象的函数
可以用这个属性来接触在递归时对函数名的耦合状态:
function test(v) {
if(v === 1) {
return v
} else{
return v* arguments.callee(v-1)
}
}
注意,在严格模式下是禁止访问arguments.callee属性的。
this属性
this引用的是函数执行时的环境对象,再调用函数之前,this的值并不确定,因此this可能在代码执行过程中引用不同的对象。
函数的caller属性
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对象就是全局对象,在浏览器环境中,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对象
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
BOM对象的核心是window,它既是浏览器的一个实例,又是ECMAScript的Gblobal对象。
全局作用域下声明的变量和函数,都是window对象的成员
setTimeout
JavaScript是单线程语言,一定时间内只能执行一段代码。setTimeout的第二个参数告诉JavaScript过多长时间将当前任务添加到队列。如果队列是空的,那么添加的代码会立即执行。如果队列不是空的,那么就要等到前面的代码执行完了以后再执行。
location
用除了replace的方法外,浏览器都会生成新纪录,可以后退,replace不可以
调用reload方法,会使用缓存重载页面,传递参数reload(true)会强制从服务器加载
navigator
通常用来检测显示网页的浏览器类型
navigator.plugins数组可以用来检测浏览器是否安装了特定的插件
history
history保存着用户的上网记录,开发者无法得知用户浏览过的URL,同意通过history.go方法来在用户的历史记录中跳转,参数可以是正负数,也可以是字符串。
history.length保留着历史记录的数量。
第十章 DOM
页面包含其他子域的框架时,由于跨域,不同子域的页面不能通过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操作。
第十一章 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用来引用文档的<head>元素
自定义属性
可以为元素添加非标准的属性,但要添加前缀data-,目的是为元素提供与渲染无关的信息,或者提供语义信息。
通过元素的dataset属性来访问自定义属性的值。
-
innerHTML返回内容 -
outerHTML返回标签加内容 -
innerText操作元素中所有文本内容
页面滚动
HTML5中使用scrollIntoView方法作为标准方法,所有元素都可以条用,调用后元素就会出现在视口中
传入true或者不传入参数,窗口滚动后会让调用元素顶部与视图顶部尽可能平齐,如果传入false,调用元素会尽可能全部出现在视口中
样式
offsetWidth = clientWidth + border
clientWidth = width + padding
第十三章 事件
可以在document对象上使用createEvent创建event对象,这个方法接受一个参数,即表示要创建的事件类型的字符串:
- UIEvents:一般化的UI时间,鼠标事件和键盘事件都继承自UI时间
- MouseEvents:一般化的鼠标事件
- MutationEvents:一般化的DOM变动事件
- HTMLEvents:一般化的HTML事件
触发事件使用的是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()方法会令文本框获得焦点
操纵剪贴板
有六个剪贴板事件:
beforeCopeycopybeforeCutcutbeforePastepaste
访问剪贴板中的数据可以使用clipboardData对象,这个对象有三个方法:
getDatasetDataclearData
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同时放到<form>当中,点击按钮或者按下回车键会触发浏览器自带的表单验证功能,主要的验证类型有:
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事件触发
<div class="inner-content" contenteditable="true" @mouseup="input"></div>
input(e) {
if(document.getSelection().toString()) {
console.log(document.getSelection().toString())
}
}
关于富文本,书里介绍的并不多,并且主要是基于iframe实现的,现在更多的是基于contenteditable实现,日后有相关需要再进一步研究吧。
第十五章
<canvas>必须先设置width和height属性
<canvas width="500" height="200" id="drawing">I can draw something here.</canvas>
绘制之前,需要先获取绘图上下文:
const drawing = document.querySelector('#drawing');
const context = drawing.getContext('2d');
通过toDataURL可以到处在<canvas>上绘制的元素
在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
WebGl是针对Canavs的3D上下文
第十六章 HTML5脚本编程
跨文档消息传递
使用postMessage可以页面与当前页面的<iframe>元素或者当前页面弹出的窗口进行通信,当收到消息时,会触发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。
媒体元素
在使用<video>和<audio>标签时,可以指定多个不同的媒体来源:
历史状态管理
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)
}
这个函数的灵活指出在于:
- 使用Image对象来发送请求,所有浏览器都支持Image对象
- 可以避免跨域限制,因为通常是一台服务器负责处理多台服务器的错误
第二十章 JSON
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和Comet
Ajax请求
面试最爱考的,先手写一个原生的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方法取消异步请求
HTTP头信息
请求头:(Request Header)
- Accept:浏览器能够处理的内容类型
- Method:请求方法
- Accept-encoding:浏览器能够处理的压缩变化
- Accept-charset:浏览器能够显示的字符集
- Accept-language:浏览器当前设置的语言
- Connection:浏览器与服务器之间的连接类型
- Host:发出请求的页面所在的域
- Referer:发出请求的页面的URI
- Cookie:当前页面设置的cookie
- user-agent:浏览器的用户代理字符串
可以调用xhr.setRequestHeader()方法重写请求头
xhr.setRequestHeader('myHeader', 'myValue');
自定义一些header属性进行跨域请求时,可能会报错,你可能需要在你的服务端设置Access-Control-Allow-Headers。
可以通过XHR对象的getAllResponseHeaders方法可以获取全部响应头信息,getResponseHeader(name)可以获取对应的响应头信息
响应头:
FormData对象
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 = '<a id="a"><b id="b">hey!</b></a>'; // 新文件的正文...
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-OrginAccess-Control-Request-MethodsAccess-Control-Request-HeadersAccess-Control-Max-Age
预检请求结束后,浏览器会按照上面的第4项将遇见请求缓存起来
带Cookie跨域
默认情况下,跨域请求不提供凭据(cookie,HTTP认证或者客户端SSL证明),通过将withCredentials设为true,可以指定某个请求应携带凭据
服务器如果接受,会返回下面的响应头字段:
Access-Control-Allow-Credentials: true
其他跨域技术
图像Ping
<img>标签不存在跨域的问题,可以访问任意其他网页的图片,并且可以通过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
JSONP,JSON with Padding实际上利用的是<srcipt>标签可以不受跨域限制的特定进行的跨域,使用JSONP时相当于动态创建了下面的标签
<script src='http://www.baidu.com/test?callback=handlerResponse'
服务器会读取callback后面的参数,并且执行handlerResponse回调函数,将数据在这个函数中传递过来
JSONP可以直接访问响应文本,支持客户端与服务器之间的双向通信
JSONP的两个问题:
- 有可能导致CSRF和XSS攻击,在其他域中在响应中夹带恶意代码
- 无法确定JSONP是否失败,
<script>标签的onerror事件未获得广泛支持
Comet
用于模拟服务器主动推送的技术,其实也就是长轮询:页面发起一个到服务器的请求,然后服务器保持连接打开,将请求挂起,知道有数据可发送时对请求进行响应,发送完数据后连接断开,浏览器会又发起一个新的请求。
这个过程在页面打开期间一直持续不断。
WebSocket
WebSocket会用一个HTTP请求简历连接,在取得服务器后连接会从HTTP协议升级为Websocket协议
Websocket使用了自定义的协议ws://和wss://,因为使用了自定义协议,就不需要再向HTTP协议那样发送请求头,传递的数据包更小,适合移动应用
Websocket建立的连接不存在跨域问题,因此可以通过建立Websocket打开到任何站点的俩进阶,至于是否可以与页面通信,完全取决于服务器(通过握手信息就可以知道请求来自何方)
SSE
SSE(Sever-Sent Events)是单向通道,只能服务器向浏览器发送,因为流信息本质上就是下载。如果浏览器向服务器发送信息,就变成了另一次HTTP请求。
相比于Websocket,SSE有以下的有优点:
- SSE使用HTTP协议,现有的服务器软件都支持。WebSocket是一个独立协议。
- SSE属于轻量级,使用简单;WebSocket 协议相对复杂。
- SSE默认支持断线重连,WebSocket需要自己实现。
- SSE一般只用来传送文本,二进制数据需要编码后传送,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"
实例也可以监听自定义事件,服务器触发参考阮一峰的网络日志。
SSE与Websocket
如何选择:
- 考虑是否有自由度建立和维护Websockets服务器(现有服务器不能用于Websocket通信)
- 到底是否需要双向通信,如果只需要读取服务数据,那么SSE比较容易容县,如果必须双向通信,那么Websocket更好
安全
为了确保通过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
cookie的构成:
namevaluedomainpathexpiresmax-agehttp-onlysecure
具体的内容前一阵子学习《图解HTTP》刚刚学过
JavaScript存入cookie时应该是用URL编码encodeURIComponent,读取的时候也应该使用decodeURIComponent解码
a=b; c=d; x=y
给document.cookie赋值时,除非名称已经存在,否则不会覆盖
Web Storage
略
第二十四章 最佳实践
可维护性
可维护性的代码需要遵循的特点:
- 可理解性
- 直观性
- 可适应性
- 可扩展性
- 可调试性
代码约定
(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就完事了
使用常量
使用常量的场景:
- 重复值
- 用户界面字符串
- URL
- 任意可能会更改的值
性能
注意作用域
- 避免全局查找
- 避免使用
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引擎会进行双重解释,导致性能差
但是,如果有特殊的需求,不用也不行啊,除此之外当然可以尽量避免。
优化DOM交互
最小化下场更新
用文档片段document.createDocumentFragment放置新创建的项目,一次性插入DOM,减少DOM操作
使用innerHTML
构建好一个字符串,一次性调用innerHTML
但是要考虑输入来源,如果是外部输入,一定要考虑XSS攻击
使用事件代理(事件委托)
部署
构建
尽量减少JS的使用,使用<script>标签会阻塞DOM的构建和渲染
其他的都不适用了,现在是webpack的天下了。
验证
现在是ESLint的天下了,JSLint不能配置,JSHint配置混乱
压缩
(1)文件压缩:
Webpack通过插件的形式接入UglifyJS实现压缩JSS,通过css-loader实现压缩CSS,通过html-webpack-plugin压缩HTML
(2)HTTP压缩
服务器利用Gzip压缩,现在比较新的压缩算法是Brotli(Br)
第二十五章 新兴的API
requestAnimationFrame
动画循环的最佳循环间隔是 1000ms / 60(刷新率)等于17ms,但是无论是setTimeout还是setInterval,执行时间都不精确,第二个参数只是指定了把动画添加到浏览器的UI线程队列中并等待执行的时间。
知道什么时候绘制下一帧是保证动画平滑的关键。
浏览器的计时器精度为4ms
所以window.requestAnimationFrame()出现了,它告诉浏览器我们希望执行动画,并请求浏览器在下一次重绘之前调用指定的函数来更新动画。
方法的返回值是一个请求ID,可以传递给window.cancelAnimationFrame()用来取消动画执行
requestAnimationFrame方法使用一个回调函数:
window.requestAnimationFrame(callback);
callback会在浏览器重绘之前调用。注意,requestAnimationFrame只会执行一次,想要实现连续的动画,必须在callback里面手动再次调用requestAnimationFrame方法
在大多数浏览器里,当运行在后台标签页或者隐藏的<iframe>里时,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);
Page Visibility API
一般情况下,开发者要知道用户正在离开页面,一般会监听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
通过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
通过使用<input type="file">,监听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
读取文件还可以使用对象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:拖动过程中,释放鼠标按键时触发
-
<div class="drag" id="drag" draggable="true"></div>
释放元素(或文件)的相关的事件是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的默认行为
使用XHR上传文件
可以使用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计时(performance)
这里说的Web计时机制指的是对页面性能的考量标准,核心是window.performance对象,对页面的所有度量信息都包含在这个对象里面。
有两个属性,一个是performance.navigation,表示在当前页面重定向、导航的此时等,另一个是属性是performance.timing属性,它是一个时间戳,包含页面各种事件(比如导航到当前页面、重定向结束等)的时间
通过这些时间值就能了解页面被加载到浏览器的过程中都经历了哪些阶段,哪些阶段可能是影响性能的瓶颈。
Web Worker
Web Worker的作用就是为JavaScript创造多线程环境,允许主线程创建Worker线程,将一些任务分配给后者运行。
后台线程和主线程分别运行,互不干扰,这样后台线程可以随时响应主线程的通信,主线程可以将一些复杂的计算任务交给后台线程,完成之后再把结果返回给主线程
Worker线程一旦创建成功就会始终运行,但是这就会比较浪费资源,所以不应该过渡使用,而且一旦使用完毕,就行该立刻关闭。
使用Web的注意点:
- 同源限制,非配给Worker运行的脚本,必须与主线程脚本同源,不能跨域
- DOM限制,Worker线程不能读取网页的DOM对象,无法使用
document/window,可以使用loction/navigator - 通信联系,Worker线程和主线程不再同一个上下文环境,不能直接通信,必须通过消息事件完成
- 脚本限制,Worker线程不能执行
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的顺序执行
更多的参考阮一峰的文章。