对象、类与面向对象编程 Object.assign()
接收一个目标对象和一个或多个元对象作为参数 然后将每个原对象中可枚举(Object.propertyIsEnumerable()
返回 true)和自有(Object.hasOwnProperty()
返回 true)属性复制到目标对象以字符串和符号为键的属性 会被复制。对每个符合条件的属性,这个方法会使用源对象上的[[Get]]
取得属性的值,然后使用目标 对象上的[[Set]]
设置属性的值。
1 2 3 4 5 6 7 8 9 10 11 12 dest = {}; src = { id : "src" }; result = Object .assign (dest, src); console .log (dest === result); console .log (dest !== src); console .log (result); console .log (dest);
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 dest = { set a (val ) { console .log (`Invoked dest setter with param ${val} ` ); } }; src = { get a () { console .log ('Invoked src getter' ); return 'foo' ; } }; Object .assign (dest, src);console .log (dest); "Invoked dest setter with param foo" {}dest = {}; src = { a : {} }; Object .assign (dest, src);console .log (dest); console .log (dest.a === src.a ); console .log (dest == src)
assign()
是浅复制 意味着只会复制对象的引用
如果赋值期间出错 操作会中止并退出 同时抛出错误 因此可能只完成部分复制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 let dest, src, result;dest = {}; src = { a : "foo" , get b () { throw new Error (); }, c : "bar" , }; try { Object .assign (dest, src); } catch (e) {} console .log (dest);
可计算属性表达式中抛出任何错误都会中断对象创建 如果计算属性的表达式有副作用就要小心 因为如果表达式抛出错误 那么之前完成的计算是不能回滚的
解构并不要求变量必须在解构表达式中说明 不过 如果是事先声明的变量 则赋值表达式必须包含在一对括号中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 let personName, personAge;let person = { name : "Matt" , age : 27 , }; ({ name : personName, age : personAge } = person); console .log (personName, personAge); const data = reactive ({ name : "" , age : 0 , }); const response = { name : "olddog" , age : 23 , }(({ name : data.name , age : data.age } = response));
嵌套解构 可以通过解构来复制对象属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 let person = { name : 'Matt' , age : 27 , job : { title : 'Software engineer' } }; let personCopy = {};({ name : personCopy.name , age : personCopy.age , job : personCopy.job } = person); person.job .title = 'Hacker' console .log (person);console .log (personCopy);-------------------------------- vue的写法 const data = reactive ({ name : '' , age : 0 , address :{ home :"" } }) const response = { name : 'olddogewqeeqwweq' , age : 23 , address :{ home : 'huilaieqw1341343' } } onMounted (() => { ({name : data.name , age : data.age , address :{home : data.address .home }} = response) response.name = "wdqdw" response.address .home = '2132' data.address .home = 'qdewqdeqsw' console .log (response); console .log (data) })
涉及到多个属性的解构赋值是一个无关输出的顺序化操作 如果一个解构表达式涉及多个赋值 如果开始的赋值成功而后面的赋值出错 则整个赋值表达式只会完成一部分
Object.setPrototypeof()可能会严重影响代码性能
hasOwnProprtty()
用于确定某个属性是实例上还是原型对象上(实例上返回true
)
原型和 in 操作符 单独使用in
只要可以访问到对象指定属性时返回true
无论是在实例上还是在原型上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function hasPrototypePrototy (object, name ) { return !object.hasOwnProprtty (name) && name in object; } function Person ( ) {}Person .prototype .name = "Nicholas" ;Person .prototype .age = 29 ;Person .prototype .job = "Software Engineer" ;Person .prototype .sayName = function ( ) { console .log (this .name ); }; let person = new Person ();console .log (hasPrototypeProperty (person, "name" )); person.name = "Greg" ; console .log (hasPrototypeProperty (person, "name" ));
for-in
只要能通过对象访问到并且可以被枚举的属性都会返回 包括实例属性和原型属性(遮蔽原型中不可枚举的实例属性也会被返回 因为默认情况下开发者定义的属性都是可枚举的)
如果要获得对象上的所有可枚举实例属性 可以使用Object.keys()
(返回包含该对象所有可枚举属性名称的字符串数组)
Object.keys()
和 Object.getOwnPropertyNames()
在适当的时候都可以用来代替for- in
循环
属性枚举顺序 for-in、Object.keys()
的枚举顺序是不确定的 Object.getOwnPropertyNames()、Object.getOwnPropertySymbols()和 Object.assign()
的枚举顺序是确定性的。先以升序枚举数值键,然后以插入顺序枚举字符串和符号键。在对象字面量中 定义的键以它们逗号分隔的顺序插入。
对象迭代 Object.values()
返回对象值的数组 Object.entries()
返回键值对的数组
这两个方法执行对象浅复制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const data = { name : "james" , address : { home : "sga" , }, }; const per1 = Object .values (data)[0 ];const per2 = Object .values (data)[1 ];console .log (per1); console .log (per2);{ home : "sga" ; } data.name = "curry" ; data.address .home = "school" ; console .log (per1); console .log (per2);
类 使用new
调用类的构造函数会执行以下操作
在内存中创建一个新对象
在这个新对象内部的[[Prototype]]
指针被复制为构造函数的prototype
属性
构造函数内部的 this 被赋值为这个新对象(即this
指向新对象)
执行构造函数内部的代码(给新对象添加属性)
如果构造函数返回非空对象 则返回该对象 否则 返回刚创建的新对象
代理和反射 在代理对象上执行的任何操作实际上都会应用到目标对象 唯一可以感知的不同就是代码中操作的是代理对象那个
1 2 3 4 5 6 7 8 9 10 11 12 13 const target = { id : 'target' } const handler = {};const proxy = new Proxy (target, handler);console .log (target.id == proxy.id )target.id = 'foo' ; console .log (target.id == proxy.id );proxy.id = 'bar' ; console .log ( target.id == proxy.id );console .log (target instanceof Proxy );'undefined' in instanceof check Proxy .prototype 是undefined console .log (target == proxy)
定义捕获器 使用代理的主要目的是可以定义捕获器(trap)
1 2 3 4 5 6 7 8 9 10 11 const target = { foo : "bar" , }; const handler = { get ( ) { return "handler override" ; }, }; const proxy = new Proxy (target, handler);
捕获器参数和反射 API 所有捕获器都可以访问相应的参数 基于这些参数可以重建被捕获方法的原始行为
1 2 3 4 5 6 7 8 9 10 11 const target = { foo : "bar" , }; const handler = { get (trapTarget, property, receiver ) { return trapTarget[property]; }, }; const proxy = new Proxy (target, handler);console .log (proxy.foo ); console .log (target.foo );
可以调用全局Reflect
对象上(封装了原始行为)的同名方法来轻松重建
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 const target = { foo : 'bar' }; const handler = { get ( ) { return Reflect .get (...arguments ); } get : Reflect .get }; const proxy = new Proxy (target, handler);console .log (proxy.foo ); console .log (target.foo ); const target = { foo : 'bar' , baz : 'qux' }; const handler = { get (trapTarget, property, receiver ) { let decoration = '' ; if (property === 'foo' ) { decoration = '!!!' ; } return Reflect .get (...arguments ) + decoration; } }; const proxy = new Proxy (target, handler);console .log (proxy.foo ); console .log (target.foo ); console .log (proxy.baz ); console .log (target.baz );
捕获器不变式 捕获器应该遵守某些规范而不是出现过于反常的行为
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 如果目标对象有一个不可配置且不可写的数据属性,那么在捕获器返回一个与该属性不同的 值时,会抛出 TypeError : const target = {};Object .defineProperty (target, 'foo' , { configurable : false , writable : false , value : 'bar' }); const handler = { get ( ) { return 'qux' ; } }; const proxy = new Proxy (target, handler);console .log (proxy.foo );
可撤销代理 使用new Proxy()
创建的普通代理的联系在代理的生命周期内会一直持续纯在 可以使用revocable()
来撤销代理对象和目标对象的联系 撤销时不可逆和幂等的(调用多少次都一样) 撤销后再调用代理就会抛出TypeError
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const target = { foo : "bar" , }; const handler = { get ( ) { return "intercepted" ; }, }; const { proxy, revoke } = Proxy .revocable (target, handler);console .log (proxy.foo ); console .log (target.foo ); revoke ();console .log (proxy.foo );
使用反射 API 优先使用反射 API 的情况
Object 上的方法适用于通用程序 而反射方法适用于细粒度的对象控制与操作
状态标记 1 2 3 4 5 6 7 8 9 10 11 12 13 14 const o = {};try { Object .defineProperty (o, "foo" , "bar" ); console .log ("success" ); } catch (e) { console .log ("failure" ); } if (Reflect .defineProperty (o, "foo" , { value : "bar" })) { console .log ("success" ); } else { console .log ("failure" ); }
以下反射方法都会提供状态标记:
Reflect.defineProperty()
Reflect.preventExtensions()
Reflect.setPrototypeOf()
Reflect.set()
Reflect.deleteProperty(
以下反射方法提供只有通过操作符才能完成的操作。
Reflect.get():
可以替代对象属性访问操作符
Reflect.set()
:可以替代=赋值操作符
Reflect.has()
:可以替代 in 操作符或 with()
Reflect.deleteProperty()
:可以替代 delete 操作符
Reflect.construct()
:可以替代 new 操作符
可以创建一个代理去代理另一个代理 这样就能在一个目标对象上构建多层拦截网
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const target = { foo : "bar" , }; const firstProxy = new Proxy (target, { get ( ) { console .log ("first proxy" ); return Reflect .get (...arguments ); }, }); const secondProxy = new Proxy (firstProxy, { get ( ) { console .log ("second proxy" ); return Reflect .get (...arguments ); }, }); console .log (secondProxy.foo );
代理的问题与不足 代理中的 this 如果目标对象依赖于对象标识 那么就可能出现问题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const wm = new WeakMap ();class User { constructor (userId ) { wm.set (this , userId); } set id (userId ) { wm.set (this , userId); } get id () { return wm.get (this ); } } const user = new User (123 );console .log (user.id ); const userInstanceProxy = new Proxy (user, {});console .log (userInstanceProxy.id ); 例。要解决这个问题,就需要重新配置代理,把代理 User 实例改为代理 User 类本身。之后再创建代 理的实例就会以代理实例作为 WeakMap 的键了: const UserClassProxy = new Proxy (User , {});const proxyUser = new UserClassProxy (456 );console .log (proxyUser.id );
代理与内部槽位 1 2 3 4 5 6 7 内部槽位[[NumberDate ]]。代理对象上不存在这个内部槽位,而且这个内部槽位的值也不能通过普通 的 get ()和 set ()操作访问到,于是代理拦截后本应转发给目标对象的方法会抛出 TypeError const target = new Date ();const proxy = new Proxy (target, {});console .log (proxy instanceof Date ); proxy.getDate ();
代理捕获器和反射方法
对于代理对象上执行的任何一种操作 只会有一个捕获程序会被调用 不存在重复捕获的情况
get()方法 get()
捕获器会在获取属性值的操作中被调用 对应的反射 API 为Reflect.get()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const myTarget = {};const proxy = new Proxy (myTarget, { get (target, property, receiver ) { console .log ('get()' ); return Reflect .get (...arguments ) } }); proxy.foo ; proxy.property proxy[property] Object .create (proxy)[property]Reflect .get (proxy, property, receiver)如果target.property 不可写且不可配置 则处理程序返回的值必须与target.property 匹配 如果target.property 不可配置且[[Get ]]特性为undefined 处理程序的返回值也必须是undefined
set() set()
捕获器会在设置属性值的操作中被调用。对应的反射 API 方法为 Reflect.set()。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const myTarget = {};const proxy = new Proxy (myTarget, { set (target, property, value, receiver ) { console .log ('set()' ); return Reflect .set (...arguments ) } }); proxy.foo = 'bar' ; proxy.property = value; proxy[peoperty] = value; Object .create (proxy)[property] = valueReflect .set (proxy, property, value, receiver)target : 目标对象property : 引用的目标对象上的字符串键属性value:要赋给属性的值 receiver:接收最初赋值的对象 如果target.property 不可写且不可配置 则不修改目标属性的值 如果target.property 不可配置且[[Set ]]特性为undefined 则不能修改目标属性的值 在严格模式下 处理程序中返回false 会抛出TypeError
has() has()
捕获器会在 in 操作符中被调用。对应的反射 API 方法为 Reflect.has()。
1 2 3 4 5 6 7 8 9 10 11 12 const myTarget = {};const proxy = new Proxy (myTarget, { has (target, property ) { console .log ("has()" ); return Reflect .has (...arguments ); }, }); "foo" in proxy;
apply() apply()捕获器会在调用函数时被调用 对应的反射 api 方法为 Reflect.apply()
1 2 3 4 5 6 7 8 9 const myTarget = ( ) => {};const proxy = new Proxy (myTarget, { apply (target, thisArg, ...argumentsList ) { console .log ("apply()" ); return Reflect .apply (...arguments ); }, }); proxy ();
代理模式 跟踪属性访问 (很重要)通过捕获get set 和has
等操作 可以知道对象属性说明时候被使用 查询的那个 把实现相应捕获器的某个对象代理放到应用中 就可以检测这个对象何时在何处被访问过
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const user = { name : 'Jack' } const proxy = new Proxy (user, { get (target, property, receiver ) { console .log ('Getting ${property}' ); return Reflect .get (...arguments ); } set (target, property, value, receiver ) { console .log ('Setting ${property} = ${value}' ); return Reflect .set (...arguments ) } }) proxy.name ; proxy.age = 27 ;
隐藏属性 代理的内部实现对外部代码是不可见的 因此可以用来隐藏目标对象上的属性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 const hiddenProperties = ["foo" , "bar" ];const targetObject = { foo : 1 , bar : 2 , baz : 3 , }; const proxy = new Proxy (targetObject, { get (target, property ) { if (hiddenProperties.includes (property)) { return undefined ; } else { return Reflect .get (...arguments ); } }, has (target, property ) { if (hiddenProperties.includes (property)) { return false ; } else { return Reflect .has (...arguments ); } }, }); console .log (proxy.foo ); console .log (proxy.bar ); console .log (proxy.baz ); console .log ("foo" in proxy); console .log ("bar" in proxy); console .log ("baz" in proxy);
属性验证 因为所有的赋值操作都会触发set()
捕获器 所以可以根据所赋的值决定是允许还是拒绝赋值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const target = { onlyNumbersGoHere : 0 , }; const proxy = new Proxy (target, { set (target, property, value ) { if (typeof value !== "number" ) { return false ; } else { return Reflect .set (...arguments ); } }, }); proxy.onlyNumbersGoHere = 1 ; console .log (proxy.onlyNumbersGoHere ); proxy.onlyNumbersGoHere = "2" ; console .log (proxy.onlyNumbersGoHere );
函数与构造函数参数验证 跟保护和验证对象属性类似,也可对函数和构造函数参数进行审查。比如,可以让函数只接收某种 类型的值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 function median (...nums ) { return nums.sort ()[Math .floor (nums.length / 2 )]; } const proxy = new Proxy (median, { apply (target, thisArg, argumentsList ) { for (const arg of argumentsList) { if (typeof arg !== 'number' ) { throw 'Non-number argument provided' ; } } return Reflect .apply (...arguments ); } }); console .log (proxy (4 , 7 , 1 )); console .log (proxy (4 , '7' , 1 ));类似地,可以要求实例化时必须给构造函数传参: class User { constructor (id ) { this .id_ = id; } } const proxy = new Proxy (User , { construct (target, argumentsList, newTarget ) { if (argumentsList[0 ] === undefined ) { throw 'User cannot be instantiated without id' ; } else { return Reflect .construct (...arguments ); } } }); new proxy (1 );new proxy ();
数据绑定和可观察对象 通过代理把运行中原本不相关的部分联系在一起 可以实现各种模式 从而让不同的代码互操作 比如 可以将代理的类绑定到一个全局实例集合 让所有创建的实例都被添加到这个集合中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const userList = [];class User { constructor (name ) { this .name_ = name; } } const proxy = new Proxy (User , { construct ( ) { const newUser = Reflect .construct (...arguments ); userList.push (nreUser); return newUser; }, }); new Proxy ("John" );new Proxy ("Jacob" );new Proxy ("Jingqdqdk" );
另外 还可以将集合绑定到一个事件分派程序 每次插入新实例都会发送消息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const userList = [];function emit (newValue ) { console .log (newValue); } const proxy = new Proxy (userList, { set (target, property, value, receiver ) { const result = Reflect .set (...arguments ); if (result) { emit (Reflect .get (target, property, receiver)); } return result; }, }); proxy.push ("John" ); proxy.push ("Jacob" );
函数
ECMAScript 中的所有参数都按值传递 不可能按引用传递 如果把对象作为参数重载 那么传递的值就是这个对象的引用
JavaScript 引擎在任何代码执行之前 会先读取函数声明 并在执行上下文中生成函数定义 而函数表达式必须等到代码执行到它的那一行 才会在执行上下文中生成函数定义。
1 2 3 4 5 6 7 8 9 10 11 console .log (sum (10 , 10 ));function sum (num1, num2 ) { return num1 + num2; } console .log (sum (10 , 10 ));var sum = function (num1, num2 ) { return num1 + num2; };
从一个函数内部返回另一个函数也是可以的 而且非常重要
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function createComparisonFunction (propertyName ) { return function (object1, object2 ) { let value1 = object1[propertyName]; let value2 = object2[propertyName]; if (value1 < value2) { return -1 ; } else if (value1 > value2) { return 1 ; } else { return 0 ; } }; } let data = [ { name : "Zachary" , age : 28 }, { name : "Nicholas" , age : 29 }, ]; data.sort (createComparisonFunction ("name" )); console .log (data[0 ].name ); data.sort (createComparisonFunction ("age" )); console .log (data[0 ].name );
箭头函数中的this
引用的是定义箭头函数的上下文
1 2 3 4 5 6 7 8 9 10 window .color = "red" ;let o = { color : "blue" , }; let sayColor = ( ) => console .log (this .color );sayColor (); o.sayColor = sayColor; o.sayColor ();
ES6 新增:new.target
如果是使用new
关键字调用的 则 new.target
将引用被调用的构造函数 否则值是undefined
可以用来规定某个函数只能通过new
来构建
1 2 3 4 5 6 7 8 function King ( ) { if (!new .target ) { throw 'King must be instantiated using "new"' ; } console .log ('King instantiated using "new"' ); } new King (); King ();
递归 可以使用命名表达式来完成递归
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function factorial(num) { if (num <= 1) { return 1; } else { return num * factorial(num - 1); } } let anotherFactorial = factorial; //将函数保存在anthorPactorial中 factorial = null; console.log(anotherFactorial(4)); // 报错 递归调用factorial 但是factoprial已经不是函数了 //可以使用命名函数来解决 const factorial = (function f(num) { if (num <= 1) { return 1; } else { return num * f(num - 1); } });
尾调用优化 ES6 新增了一想内存管理优化机制 让 js 引擎在满足条件时可以重用栈帧 非常适合尾调用(外部函数的返回值时一个内部函数的返回值)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function outerFunction ( ) { return innerFunction (); } ES6 优化之前:(1 ) 执行到 outerFunction 函数体,第一个栈帧被推到栈上。 (2 ) 执行 outerFunction 函数体,到 return 语句。计算返回值必须先计算 innerFunction。 (3 ) 执行到 innerFunction 函数体,第二个栈帧被推到栈上。 (4 ) 执行 innerFunction 函数体,计算其返回值。 (5 ) 将返回值传回 outerFunction,然后 outerFunction 再返回值。 (6 ) 将栈帧弹出栈外。 在 ES6 优化之后,执行这个例子会在内存中发生如下操作。 (1 ) 执行到 outerFunction 函数体,第一个栈帧被推到栈上。 (2 ) 执行 outerFunction 函数体,到达 return 语句。为求值返回语句,必须先求值 innerFunction。 (3 ) 引擎发现把第一个栈帧弹出栈外也没问题,因为 innerFunction 的返回值也是 outerFunction 的返回值。 (4 ) 弹出 outerFunction 的栈帧。 (5 ) 执行到 innerFunction 函数体,栈帧被推到栈上。 (6 ) 执行 innerFunction 函数体,计算其返回值。 (7 ) 将 innerFunction 的栈帧弹出栈外 很明显,第一种情况下每多调用一次嵌套函数,就会多增加一个栈帧。而第二种情况下无论调用多 少次嵌套函数,都只有一个栈帧。这就是 ES6 尾调用优化的关键:如果函数的逻辑允许基于尾调用将其 销毁,则引擎就会那么做。
1 2 3 4 5 6 7 8 9 10 11 12 function fib (n ) { return fibImpl (0 , 1 , n); } function fibImpl (a, b, n ) { if (n === 0 ) { return a; } return fibImpl (b, a + b, n - 1 ); }
闭包 闭包指的是那些引用了另一个函数作用域中变量的函数 通常是在嵌套函数中实现的
在调用一个函数的时候 会为这个函数调用创建一个执行上下文 并创建一个作用域链 然后用 arguments 和其他命名参数来初始化这个函数的所有活动对象 外部函数的活动对象是内部函数作用域链上的第二个对象 这个作用域链一直向外串起了所有包含函数的活动对象 知道全局执行上下文才终止
理解调用的函数表达式 立即调用的匿名函数又称作立即调用的函数表达式(IIFE)ES6 之后没有使用的必要了
1 2 3 4 5 6 7 8 (function ( ) { for (var i = 0 ; i < count; i++) { console .log (i); } })(); console .log (i);
使用闭包和私有变量会导致作用域链变长 作用域链越长 则找到变量所需的时间就越多
Promise 和异步函数 promise
的状态是私有的 不能直接通过 js 检测到 这是为了避免读取到的promise
的状态以同步方式处理 同时 promise
的状态也不能被外部 js 代码修改 要是为了隔离外部的同步代码
Promise.resolved()
是幂等状态
1 2 3 4 let p = new Promise (() => {});setTimeout (console .log , 0 , p); setTimeout (console .log , 0 , Promise .resolve (p)); setTimeout (console .log , 0 , p === Promise .resolve (p));
Promise.reject()
会将传给他的作为错误信息返回 包括promise.resloved()
1 2 setTimeout (console .log , 0 , Promise .reject (Promise .resolve ()));
Finally
用于给promise
添加onFinally
处理程序 可以避免 onResolved
和 onRejected
处理程序中出 现冗余代码 主要用于添加清理代码
明天再看
BOM 窗口关系 outerWidth 和 outerHeight
返回浏 览器窗口自身的大小(不管是在最外层 window 上使用,还是在窗格中使用)。innerWidth 和 innerHeight
返回浏览器窗口中页面视口的大小(不包含浏览器边框和工具栏)
document.documentElement.clientWidth 和 document.documentElement.clientHeight
返回页面视口的宽度和高度。
视口位置 度量文档相对于视口滚动距离的属性有两对,返回相等的值:window.pageXoffset/window. scrollX 和 window.pageYoffset/window.scrollY
1 2 3 4 5 6 7 8 window .scrollBy (0 , 100 );window .scrollBy (40 , 0 );window .scrollTo (0 , 0 );window .scrollTo (100 , 100 );
除了偏移值之外 还能通过behavior
属性告诉浏览器是否平滑滚动
1 2 3 4 5 6 7 window .scrollTo ({ left : 100 , top : 100 , behavior : "auto" , });
DOM Document URL
包含当前页面的完整 URL(地址栏中的 URL) domain
包含页面的域名 referrer
包含连接到当前页面的那个页面的 URL(如果当前页面无来源 则尾空字符串)只有 domain 的值是可以设置的
getElementsByName()
方法最常用于单选按钮,因为同 一字段的单选按钮必须具有相同的 name
属性才能确保把正确的值发送给服务器
scrollIntoView()
方法存在于所有 HTML 元素上,可以滚动浏览器窗口或容器元素以便包含元 素进入视口
alignToTop:boolean default:true
true:窗口滚动后元素的顶部与视口顶部对齐
false:窗口滚动后元素的底部与视口底部对齐
false:窗口滚动后元素的底部与视口底部对齐
false:窗口滚动后元素的底部与视口底部对齐
block:定义垂直方向的对齐,可取的值为”start”、”center”、”end”和”nearest”,默 认为 “start”。
inline:定义水平方向的对齐,可取的值为”start”、”center”、”end”和”nearest”,默 认为 “nearest”。
这个方法可以用来在页面上发生某个事件时引起用户关注。把焦点设置到一个元素上也会导致浏览 器将元素滚动到可见位置。
事件 DOM 事件对象 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 bubbles 布尔值 只读 表示事件是否冒泡 cancelable 布尔值 只读 表示是否可以取消事件的默认行为 currentTarget 元素 只读 当前事件处理程序所在的元素 defaultPrevented 布尔值 只读 true 表示已经调用 preventDefault()方法(DOM3 Events 中新增) detail 整数 只读 事件相关的其他信息 eventPhase 整数 只读 表示调用事件处理程序的阶段:1 代表捕获阶段,2 代表 到达目标,3 代表冒泡阶段 preventDefault() 函数 只读 用于取消事件的默认行为。只有 cancelable 为 true 才 可以调用这个方法 stopImmediatePropagation() 函数 只读 用于取消所有后续事件捕获或事件冒泡,并阻止调用任 何后续事件处理程序(DOM3 Events 中新增) stopPropagation() 函数 只读 用于取消所有后续事件捕获或事件冒泡。只有 bubbles 为 true 才可以调用这个方法 target 元素 只读 事件目标 trusted 布尔值 只读 true 表示事件是由浏览器生成的。false 表示事件是开 发者通过 JavaScript 创建的(DOM3 Events 中新增) type 字符串 只读 被触发的事件类型 View AbstractView 只读 与事件相关的抽象视图。等于事件所发生的 window 对象
在事件处理程序内部 this
对象始终等于currentTatget
的值 而target
只包含事件的实际目标
type
属性在一个对象处理多个程序的时候很有用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 let btn = document .getElementById ("myBtn" );let handler = function (event ) { switch (event.type ) { case "click" : console .log ("Clicked" ); break ; case "mouseover" : event.target .style .backgroundColor = "red" ; break ; case "mouseout" : event.target .style .backgroundColor = "" ; break ; } }; btn.onclick = handler; btn.onmouseover = handler; btn.onmouseout = handler;
preventDefault()
方法用于阻止特定事件的默认动作
stopPropagation()
方法用于立即阻止事件流在 DOM 结构中传播,取消后续的事件捕获
或冒泡
eventPhase
属性可用于确定事件流当前所处的阶段
event 对象只在事件处理程序执行期间存在 一旦执行完毕 就会被销毁
事件类型
用户界面事件(UIEvent
):涉及与 BOM 交互的通用浏览器事件
resize:在 window 或窗格上当窗口或窗格被缩放时触发(浏览器窗口在最大化或最小化的时候也会触发这个事件)
scroll:当用户滚动包含滚动条上的元素时在元素上触发
abort:在<object>
元素上当相应对象加载完成前被用户提前终止下载时触发
焦点事件(FocusEvent
):在元素获得和失去焦点时触发
blur:失去焦点时触发
focus:用户获得焦点时触发 不冒泡
focusin:当元素获得焦点时触发。这个事件是 focus 的冒泡版
focusout:当元素失去焦点时触发。这个事件是 blur 的通用版
鼠标事件(MouseEvent
):使用鼠标在页面上执行某些操作时触发
dblclick:在用户双击鼠标主键(通常是左键)时触发
mousedown:在用户按下任意鼠标键时触发。这个事件不能通过键盘触发
mouseenter:在用户把鼠标光标从元素外部移到元素内部时触发。这个事件不冒泡,也不会在 光标经过后代元素时触发
mouseleave:在用户把鼠标光标从元素内部移到元素外部时触发。这个事件不冒泡,也不会在 光标经过后代元素时触发
mousemove:在鼠标光标在元素上移动时反复触发
mouseout:在用户把鼠标光标从一个元素移到另一个元素上时触发
mouseover:在用户把鼠标光标从元素外部移到元素内部时触发
mouseup:在用户释放鼠标键时触发
除了 mouseenter 和 mouseleave,所有鼠标事件都会冒泡, 都可以被取消,而这会影响浏览器的默认行为
滚轮事件(WheelEvent
):使用鼠标滚轮(或类似设备)时触发
输入事件(InputEvent
):向文档中输入文本时触发
键盘事件(KeyboardEvent
):使用键盘在页面上执行某些操作时触发
textInput:输入事件 当 会在文本被插入到文本框之前触发(只在可编辑区域触发 只有新字符被插入时才会触发)
keydown 和 keypress 会在文本框变化前触发(按住不放则重复触发) keyUp 会在发生后触发
key 属性用于替代 keyCode,且包含字符串。在按下字符键时,key 的值等于文本字符(如 “k”或“M”);在按下非字符键时,key 的值是键名(如“Shift”或“ArrowDown”)。char 属性在按 下字符键时与 key 类似,在按下非字符键时为 null
合成事件(CompositionEvent
):在使用某种 IME(Input Method Editor,输入法编辑器)输入 字符时触发。
内存和性能 事件委托 事件委托利用事件冒泡 可以只使用一个事件处理程序来处理一种类型事件
所有使用按钮的事件(大多数鼠标事件和键盘事件)都适用于这个解决方案
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 <ul id="myLinks" > <li id ="goSomewhere" > Go somewhere</li > <li id ="doSomething" > Do something</li > <li id ="sayHi" > Say hi</li > </ul>; let item1 = document .getElementById ("goSomewhere" );let item2 = document .getElementById ("doSomething" );let item3 = document .getElementById ("sayHi" );item1.addEventListener ("click" , (event ) => { location.href = "http:// www.wrox.com" ; }); item2.addEventListener ("click" , (event ) => { document .title = "I changed the document's title" ; }); item3.addEventListener ("click" , (event ) => { console .log ("hi" ); }); let list = document .getElementById ("myLinks" );list.addEventListener ("click" , (event ) => { let target = event.target ; switch (target.id ) { case "doSomething" : document .title = "I changed the document's title" ; break ; case "goSomewhere" : location.href = "http:// www.wrox.com" ; break ; case "sayHi" : console .log ("hi" ); break ; } });
事件委托的好处
document
对象随时可用,任何时候都可以给它添加事件处理程序(不用等待 DOMContentLoaded 或 load 事件)。这意味着只要页面渲染出可点击的元素,就可以无延迟地起作用。
节省花在设置页面事件处理程序上的时间。只指定一个事件处理程序既可以节省 DOM 引用,也 可以节省时间
减少整个页面所需的内存,提升整体性能
最适合使用事件委托的事件包括:click、mousedown、mouseup、keydown 和 keypress
JavaScript API 跨上下文消息 跨文档消息(XDM)是一种在不同执行上下文(如不同工作线程或不同源的页面能力)间传递消息的能力
跨上下文消息用于窗口之间通信或工作线程之间通信
postMessage()
方法接收三个参数:消息、表示目标接收源的字符串和可选的可传输对象的数组(只与工作线程相关)第二个参数对于安全非常重要 可以限制浏览器交付数据的目标
1 2 3 let iframeWindow = document .getElementById ("myframe" ).contentWIndow ;iframeWindow.postMessage ("a message" , "http://www.wrox.com" );
接收到 XDM 消息后 window 对象上会触发message
事件(异步
触发 从发出到接收消息可能会有延迟)传给message
的event
对象包含以下三方面重要信息
data
:作为第一个参数传递给postMessage()
的字符串数据
origin
:发送消息的文档源
source
:发送消息的文档中window
对象的代理 这个代理对象主要用于发送上一条消息的窗口中执行postMessage()
方法 如果发送窗口有相同的源 那么对象就是 window 对象
onmessage
事件处理程序中检查发送窗口的源可以保证数据来自正确的 地方
1 2 3 4 5 6 7 8 9 window .addEventListener ("message" , (event ) => { if (event.origin == "http://www.wrox.com" ) { processMessage (event.data ); event.source .postMessage ("Received!" , "http://p2p.wrox.com" ); } });
大多数情况下,event.source
是某个 window
对象的代理,而非实际的 window 对象。因此不能 通过它访问所有窗口下的信息。最好只使用 postMessage()
,这个方法永远存在而且可以调用
tips: 最好就是只 通 过 postMessage()
发送字符串。如果需要传递结构化数据,那么最好先对该数据调用 JSON.stringify(),
通过 postMessage()传过去之后,再在 onmessage
事件处理程序中调用 JSON.parse()
在通过内嵌窗格加载不同域时,使用 XDM 是非常方便的。这种方法在混搭(mashup)和社交应用
中非常常用。通过使用 XDM 与内嵌窗格中的网页通信,可以保证包含页面的安全。XDM 也可以用于 同源页面
之间通信
Encoding API 用于实现字符串和定型数组之间的转换 TextEncoder、TextEncoderStream、TextDecoder 和 TextDecoderStream。
批量编码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const textEncoder = new TextEncoder ();const decodedText = 'foo' ;const encodedTRext = textEncoder.encode (docodedText);console .log (encodedText); 字典,该字典包含 read 和 written 属性,分别表示成功从源字符串读取了多少字符和向目标数组写 入了多少字符。如果定型数组的空间不够,编码就会提前终止, const textEncoder = new TextEncoder ();const fooArr = new Uint8Array (3 );const barArr = new Uint8Array (2 );const fooResult = textEncoder.encodeInto ('foo' , fooArr);const barResult = textEncoder.encodeInto ('bar' , barArr);console .log (fooArr); console .log (fooResult); console .log (barArr); console .log (barResult);
文本编码会始终使用 UTF-8 格式,而且必须写入 Unit8Array 实例。使用其他类 型数组会导致 encodeInto()抛出错误
File API 与 Blob API File 类型 每个 File 对象都有一些只读属性
name:本地系统中的文件名
size:以字节计的文件大小
type:包含文件 MIME 类型的字符串
lastModifiedDate:表示文件最后修改的时间的字符串 只有 chome 实现了
1 2 3 4 5 6 7 8 9 10 11 12 13 通过监听 change 事件然后遍历 files 集合可以取得每个选中文件的信息 let filesList = document .getElementById ("files-list" );filesList.addEventListener ("change" , (event ) => { let files = event.target .files , i = 0 , len = files.length ; while (i < len) { const f = files[i]; console .log (`${f.name} (${f.type} , ${f.size} bytes)` ); i++; } });
FileReader 类型 FileReader
类型表示一种异步
文件读取机制
readAsText(file, encoding)
:从文件中读取纯文本内容并保存在 result 属性中。第二个 参数表示编码,是可选的
readAsDataURL(file)
:读取文件并将内容的数据 URI 保存在 result 属性中
readAsBinaryString(file)
:读取文件并将每个字符的二进制数据保存在 result 属性中
readAsArrayBuffer(file)
:读取文件并将文件内容以 ArrayBuffer
形式保存在 result 属性
因为这些读取方法是异步的,所以每个 FileReader 会发布几个事件,其中 3 个最有用的事件是 progress、error 和 load
,分别表示还有更多数据、发生了错误和读取完成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 let filesList = document .getElementById ("files-list" );filesList.addEventListener ("change" , (event ) => { let info = "" , output = document .getElementById ("output" ); (progress = document .getElementById ("progress" )), (files = event.target .files ), (type = "default" ), (reader = new FileReader ()); if (/image/ .test (files[0 ].type )) { reader.readAsDataURL (files[0 ]); type = "image" ; } else { reader.readAsText (files[0 ]); type = "text" ; } reader.onerror = function ( ) { output.innerHTML = "Could not read file, error code is " + reader.error .code ; }; reader.onprogress = function (event ) { if (event.lengthComputable ) { progress.innerHTML = `${event.loaded} /${event.total} ` ; } }; reader.onload = function ( ) { let html = "" ; switch (type) { case "image" : html = `<img src="${reader.result} ">` ; break ; case "text" : html = reader.result ; break ; } output.innerHTML = html; }; });
FileReaderSync 类型 FileReader
的同步版本 只有整个文件都加载到内存之后才能继续进行 只在工作线程中可用 因为如果读取整个文件的耗时过长则会影响全局
1 2 3 4 5 6 7 8 9 10 11 12 13 self.omessage = (messageEvent ) => { const syncReader = new FileReaderSync (); console .log (syncReader); const result = syncReader.readAsDataUrl (messageEvent.data ); console .log (result); self.postMessage (result); };
Blob 与部分读取 File
对象提供了一个名为slice()
的方法 (接收两个参数 其实字节和读取的字节数 返回一个 Blob 实例 )Blob 实际上是 File 的超类
Blob 对象有一个 size
属性和一个 type
属性,还有一个 slice()
方法用于进一步切分数据。另 外也可以使用 FileReader
从 Blob 中读取数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 let filesList = document .getElementById ('files-list' );filesList.addEventListener ("change" , (event ) => { let info = "" , output = document .getElementById ('output' ); progress = document .getElementById ('progress' ), files = event.target .files , reader = new FileReader (), blob = blobSlice (files[0 ], 0 , 32 ); if (blob) [ reader.readAsText (blob); reader.onerror = function ( ) { output.innerHTML = "Could not read file, error code is" + reader.error .code ; }; reader.onload = function ( ) { output.innerHTML = reader.result ; } ]else { console .log ('error' ) } })
只读取部分文件可以节约时间 特别是在只需要特定部分比如文件头的时候
对象 URL 与 Blob 引用储存在File
或Blob
中数据的URL
**优点:**不用把文件内容读取到 JavaScript 也可以使用文件 只要在适当的位置提供对象 URL 即可
使用 window.URL.createObjectURL()
方法并传入 File
或 Blob
对象 返回一个指向内存中地址 的字符串 在DOM
中可以直接使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 let filesList = document .getElementById ("files-list" );filesList.addEventListener ('change' , (event ) => { let info = '' , output = document .getElementById ("output" ), progress = document .getElementById ("progress" ), files = event.target .files , reader = new FileReader (), url = window .URL .createObjectURL (files[0 ]); if (url) { if (/image/ .test (files[0 ].type )) { output.innerHTML = `<img src="${url} ">` ; }esle { output.innerHTML = "Not an image." ; } }else { output.innerHTML = "Your browser doesn't support object URLs." ; } })
使用完数据之后,最好能释放与之关联的内存。只要对象 URL 在使用中,就不能释放内存。如果 想表明不再使用某个对象 URL,则可以把它传给 window.URL.revokeObjectURL()。页面卸载时, 所有对象 URL 占用的内存都会被释放。不过,最好在不使用时就立即释放内存,以便尽可能保持页面 占用最少资源
读取拖放文件 拖放文件可以像拖放图片或连接一样触发drop
事件 被放置的文件可以通过事件的event.dataTransfer.files
属性读到 这个属性保存着一组 File 对象 就像文本输入字段一样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 let droptarget = document .getElementById ("droptarget" );function handleEvent (event ) { let info = "" , output = document .getElementById ("output" ), files, i, len; event.preventDefault (); if (event.type == "drop" ) { files = event.dataTransfer .files ; i = 0 ; len = files.length ; while (i < len) { info += `${files[i].name} (${files[i].type} , ${files[i].size} bytes)<br>` ; i++; } output.innerHTML = info; } } droptarget.addEventListener ("dragenter" , handleEvent); droptarget.addEventListener ("dragover" , handleEvent); droptarget.addEventListener ("drop" , handleEvent);
媒体元素
检测编解码器 有一个canPlayType()
的方法 接收一个格式/编解码器字符串 返回一个”probably”、”maybe”或””
1 2 3 4 if (audio.canPlayType ("audio/mpeg" )) { } "probably" 和"maybe" 都是真值,在 if 语句的上下文中可以转型为 true
Notifications API 用于向用户显示通知
通知只能运行再安全上下文的代码中被触发
通知必须按照每个源的原则明确得到用户允许
浏览器会记住用户的选择 如果被拒绝则无法重来
通知授权 页面可以使用全局对象Notification
向用户请求通知权限 这个对象会有一个requestPermission()
返回一个promise
对象 用户在授权对话框上执行操作后这个promise
会被解决
1 2 3 Notification .requestPermission ().then ((permission ) => { console .log ("User responded to permission request:" , permission); });
"granted"
值意味着用户明确授权了显示通知的权限。除此之外的其他值意味着显示通知会静默失 败。如果用户拒绝授权,这个值就是"denied"
。一旦拒绝,就无法通过编程方式挽回,因为不可能再 触发授权提示。
显示和隐藏通知 Notification 构造函数用于创建和显示通知
1 2 3 4 5 6 7 new Notification ("Title text!" , { body : "Body text!" , image : "path/to/image.png" , vibrate : true , }); const n = new Notification ("I will close in 1000ms" );setTimeout (() => n.close (), 1000 );
通知生命周期
onshow
在通知显示时触发
onshow
在通知显示时触发
onclose
在通知消失或通过 close()关闭时触发
onclose
在通知消失或通过 close()关闭时触发
Page Visibility API 为开发者提供页面对用户是否可见的信息
document.visibilityState
值,表示下面 4 种状态之一
页面在后台标签页或浏览器中最小化了。
页面在前台标签页中
实际页面隐藏了,但对页面的预览是可见的(例如在 Windows 7 上,用户鼠标移到任务栏图标 上会显示网页预览)。
页面在屏外预渲染
visibilitychange
事件,该事件会在文档从隐藏变可见(或反之)时触发
document.hidden
布尔值,表示页面是否隐藏。这可能意味着页面在后台标签页或浏览器中被最小 化了。这个值是为了向后兼容才继续被浏览器支持的,应该优先使用 document.visibilityState
检测页面可见性
计时 API High Resolution Time API Date.now()
不精确
performance.now()
:相对度量 计时器在执行上下文创建时从 0 开始
performance.timeOrigin
属性返回计时器初始化时全局系统时钟的值
performance.getEntries()
:性能时间线(performance timeline)。每个 PerformanceEntry 对象 都有 name、entryType、startTime 和 duration 属性
错误处理和调试 为了保证浏览器兼容 最好只使用message
只要代码中包含了finally
子块 try
块或 catch
块中的 return
语句就会被忽 略
错误类型
Error
InternalError :底层 JavaScript 引擎发出异常时由浏览器抛出 如递归过多导致栈移除 发生这种错误一般是出了问题
EvalError
:使用eval()
函数发生异常时抛出
RangeError
:数值越界
ReferenceError
:找不到对象时发生或者变量
SyntaxErro
:r 经常在给 eval()
传入的字符串包含 JavaScript 语法错误时发生
TypeError
主要发生在变量不是预期类型,或者访问不存在的方法时
``URIError使用
encodeURI()或
decodeURI()`但传入了格式错误的 URI 时发
可以在 try/vatch 中使用 instance of 操作符确定错误的类型
抛出错误 使用 throw
操作符时,代码立即停止执行,除非 try/catch
语句捕获了抛出的值。
可以通过继承 ERROR 来创建自定义的类型错误 创建自定义类型错误需要提供name
以及message
属性
1 2 3 4 5 6 7 8 class CustomError extends Error { constructor (message ) { super (message); this .name = "CustomError" ; this .message = message; } } throw new CustomError ("My message" );
使用自定义错误有助于捕获错误时更准确地区分错误
error 事件 任何没有被try/catch
处理的错误都会在window
对象上触发error
事件
错误处理策略 最好使用(===)来避免比较时发生类型转换
在流控制语句中使用非布尔值作为条件时很常见的错误来源 为避免这种错误 需要始终坚持使用布尔值作为条件
1 2 3 4 if (str3) { if (typeof str3 === "string" ) {
如果知道预期的确切类型 最好使用instanceof
来确定值的正确类型
1 2 3 4 if (values) { // 不要 if (values) { // 不要 if (typeof values.sort === "function") { // 不要 if (values instanceof Array) { // 修复
一般来说 原始类型的值应该使用 typeof 检测 而对象值应该使用 instanceof 检测
抛出错误 在大型应用程序中,自定义错误通常使用 assert()
函数抛出错误。这个函数接收一个应该为 true
的条件,并在条件为 false 时抛出错误
1 2 3 4 5 6 7 8 9 10 11 12 13 function assert (condition, message ) { if (!condition) { throw new Error (message); } } function divide (num1, num2 ) { assert ( typeof num1 == "number" && typeof num2 == "number" , "divide(): Both arguments must be numbers." ); return num1 / num2; }
网络请求与远程资源 跨源资源共享 CORS:
定义了浏览器和服务器如何实现跨源通信(背后的基本思路就是使用自定义的 HTTP 头部允许浏览器服务器相互了解 以确定请求或响应应该成功还是失败)
对于简单的请求(get/post
)没有自定义头部 而且请求体是text/plain
类型 这样的请求在发送时会有一个额外的头部教Origin
Origin
头部包含发送请求的页面的源(协议、域名和端口)以便服务器确定是否为其提供响应
1 2 3 4 5 Origin: http://www.nczonline.net 如果服务器决定响应请求,那么应该发送 Access-Control-Allow-Origin 头部,包含相同的源; 或者如果资源是公开的,那么就包含"*"。 如果没有这个头部,或者有但源不匹配,则表明不会响应浏览器请求。否则,服务器就会处理这个 请求。注意,无论请求还是响应都不会包含 cookie 信息
因为无论同域还是跨域请求都使用同一个接口 所以最好在访问本地资源时使用相对 URL 在访问远程资源时使用绝对 URL 这样可以更明确区分使用场景 同时避免出现访问本地资源时出现头部或 cookie 信息访问受限的问题
预检请求 CORS
通过一种教预检请求的服务器验证机制 允许自定义头部、除GET和POST
之外的方法 以及不同请求体内容类型 预检请求使用OPTIONS
发送包含以下头部
origin
:与简单请求相同
Access-Control-Request-Method
:请求希望使用的方法。
Access-Control-Request-Headers
:(可选)要使用的逗号分隔的自定义头部列表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 下面假设一个post请求 包含自定义的NCZ头部: Origin: http://www.nczonline.net Access-Control-Request-Method: POST Access-Control-Request-Headers: NCZ 在这个请求发送后 服务器可以确定是否允许这种类型的请求 服务器会通过响应中发送以下头部与浏览器沟通这些信息 Access-Control-Allow-Origin:与简单请求相同 Access-Control-Allow-Methods:允许的方法(逗号分隔的列表 Access-Control-Allow-Headers:服务器允许的头部(逗号分隔的列表) Access-Control-Max-Age:缓存预检请求的秒数 例如: Access-Control-Allow-Origin: http://www.nczonline.net Access-Control-Allow-Methods: POST, GET Access-Control-Allow-Headers: NCZ Access-Control-Max-Age: 1728000
预检请求返回后 结果会按响应指定的时间缓存一段时间 换句话说 只有第一次发送这种类型的请求时才会多发送一次额外的 http 请求
凭据请求 默认情况下 跨源请求不提供凭据(cookie、HTTP 认证和客户端 SSL 证书) 可以通过将withCredentials
属性设置为 true 来表明请求会发送凭据 如果服务器允许携带凭据的话 可以包含Access-Control-Allow-Credentials: true
Headers 对象是所有外发请求和入站响应头部的容器
客户端存储 cookie:服务器在响应 http 请求时 通过发送 Set-CookieHTTP 头部会话信息 cookie 是与特定域绑定的 设置 cookie 后 他会与请求一起发送到创建它的域 这个限制能保证 cookie 中存储的信息只对被认可的接收者开放 不被其他域访问
cookie 的构成
名称:唯一表示 cookie 的名称 不区分大小写 必须经过 URL 编码
值:存储在 cookie 中的字符串 这个值必须经过 URL 编码
域:cookie 有效的域 发送到这个域的所有请求都会包含到对应的 cookie
路径:请求 URL 中包含这个路径才会把 cookie 发送到服务器
过期时间:合适删除 cookie 的时间戳(什么时间之后就不发送到服务器了)
安全标志:设置之后 只有在使用 SSL 安全连接的情况下才会把 cookie 发送到服务器
JavaScript 中的 cookie document.cookie
返回包含页面中所有有效 cookie 的字符串(根据域、路径、过期时间和安全设置)
根据域、路径、过期时间和安全设置
所有名和值都是 URL 编码 因此必须通过decodeURIComponent()
解码
1 2 console.log(decodeURIComponent(document.cookie)); //_octo=GH1.1.1592692317.1629286532; tz=Asia/Shanghai
在设置值是 可以通过document.cookie
设置新的 cookie 字符串 这个字符串在被解析之后会添加到原有的cookie
中 设置document.cookie
不会覆盖之前任何的cookie
除非设置了已有的 cookie
1 document.cookie = "name=Nicholas";
CookieUtil.get()
方法用于取得给定名称的 cookie 值
CookieUtil.set()
方法用于设置页面上的 cookie
子 cookie:为了绕过浏览器对每个域 cookie 数的限制 在单个 cookie 存储的小块数据 本质上是使用 cookie 的值在单个 cookie 中存储多个名/值对 如
1 name=name1=value1&name2=value2&name3=value3&name4=value4&name5=value5
子 cookie 的格式类似于查询字符串 这些值可以储存单个 cookie 而不用单独存储为自己的名/值对 结果就是网站或者 Web 应用程序能够在单域 cookie 数限制下存储更多的结构化数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 //要取得某个子cookie 必须先取得cookie 然后在解码值之前需要先像下面这样找到子cookie class SubCookieUtil { static get(name, subName) { let subCookies = SubCookieUtil.getAll(name); //getAll用于取得所有子cookie的值 get用于取得一个子cookie return subCookies ? subCookies[subName] : null; } static getAll(name) { let cookieName = encodeURIComponent(name) + "=", cookieStart = document.cookie.indexOf(cookieName), cookieValue = null, cookieEnd, subCookies, parts, result = {}; if (cookieStart > -1) { cookieEnd = document.cookie.indexOf(";", cookieStart); if (cookieEnd == -1) { cookieEnd = document.cookie.length; } cookieValue = document.cookie.substring(cookieStart + cookieName.length, cookieEnd); if (cookieValue.length > 0) { subCookies = cookieValue.split("&"); for (let i = 0, len = subCookies.length; i < len; i++) { parts = subCookies[i].split("="); result[decodeURIComponent(parts[0])] = decodeURIComponent(parts[1]); } return result; } } return null; } // 省略其他代码 };
使用 cookie 的注意事项 HTTP-only
可在浏览器设置 也可以在服务器设置 但是只能在服务器上读取 这是因为 JavaScript 无法取得这种 cookie 的值
**http-only
的作用**
1 2 如果cookie中设置了http-only属性 那么通过js脚本无法读取cookie信息 这样能有效防止XSS攻击 窃取cookie内容 这样增加了cookie1的安全性 XSS攻击:跨站脚本攻击 属于被动式且客户端的攻击方式 原理是攻击者向有XSS漏洞的网站中输入恶意HTML代码 当其他用户浏览该网站时 这段HTML代码会自动执行 从而达到攻击的目的 通常用来盗取用户Cookie 破坏页面结构 重定向到其他网站等
因为所有 cookie 都会作为请求头部由浏览器发送给服务器,所以在 cookie 中保存大量信息可能会影 响特定域浏览器请求的性能。保存的 cookie 越大,请求完成的时间就越长。即使浏览器对 cookie 大小有 限制,最好还是尽可能只通过 cookie 保存必要信息,以避免性能问题。 对 cookie 的限制及其特性决定了 cookie 并不是存储大量数据的理想方式。因此,其他客户端存储技 术出现
不要在 cookie 中存储重要或敏感的信息。cookie 数据不是保存在安全的环境中,因 此任何人都可能获得。应该避免把信用卡号或个人地址等信息保存在 cookie 中
Web Storage 解决通过客户端储存不需要频繁发送回服务器的数据时使用 cookie 的问题
提供在 cookie 之外的储存会话数据的途径
提供跨会话持久化储存大量数据的机制
localStorage
是 永久存储机制,sessionStorage
是跨会话的存储机制
Storage 类型 用于保存名/值对数据
clear()
:删除所有值
getItem(name)
:取得给定 name 的值
key(index):
取得给定数值位置名称
removeItem(name):
删除给定的 name 的名/值对
setItem(name, value)
:设置给定 name 的值
sessionStorage 对象 sessionStorage
对象只存储会话数据,这意味着数据只会存储到浏览器关闭
因为 sessionStorag
e 对象与服务器会话紧密相关,所以在运行本地文件时不能使用。存储在 sessionStorage
对象中的数据只能由最初存储数据的页面使用,在多页应用程序中的用处有限
浏览器在实现存储的时候使用同步阻塞方式 因此数据会立即被提交到储存
通过 webStorage 写入的任何数据都能被立即读取
可以使用 for-in 循环迭代sessionStorage
的值
1 2 3 4 5 for (let key in sessionStorage) { let value = sessionStorage.getItem (key); alert (`${key} =${value} ` ); }
可以使用delete
或removeItem
删除sessionStorage
的数据
localStorage 对象 要想访问同一个 localStorage 对象 页面必须来自同一个域(子域不可以 ) 在相同的端口上使用相同的协议
两种存储方法的区别在于,存储在 localStorage 中的数据会保留到通过 JavaScript 删除或者用户 清除浏览器缓存。localStorage 数据不受页面刷新影响,也不会因关闭窗口、标签页或重新启动浏览 器而丢失。
储存事件 每当 Storage 对象发生变化时 都会在文档上触发 storage 事件
domain
:存储变化对应的域。
key:
被设置或删除的键。。
newValue
:键被设置的新值,若键被删除则为 null。
oldValue
:键变化之前的值。
对于 sessionStorage
和 localStorage
上的任何更改都会触发 storage 事件,但 storage 事 件不会区分这两者
indexedDB indexedDB
的请求几乎都是异步 的 添加onerror
和onsuccess
事件处理程序来确定输出
数据库 使用对象储存
1 2 3 4 5 6 7 8 9 10 11 let db, request, version = 1 ; request = indexedDB.open ("admin" , version); request.onerror = ( event ) => alert (`Failed to open: ${event.target.errorCode} ` );request.onsuccess = (event ) => { db = event.target .result ; };
简历好数据库连接后就是使用对象储存了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 let user = { username : "007" , firstName : "James" , lastName : "Bond" , password : "foo" , }; request.onupgradeneeded = (event ) => { const db = event.target .result ; if (db.objectStoreNames .contains ("users" )) { db.deleteObjectStore ("users" ); } db.createObjectStore ("users" , { keyPath : "username" }); };
事务 事务通过调用数据库对象的transaction
方法创建
1 2 3 let transaction = db.transaction ; let transaction = db.transaction ("users" , "readwrite" );
有了事务的引用,就可以使用 objectStore()
方法并传入对象存储的名称以访问特定的对象存储。 然后,可以使用 add()
和 put()
方法添加和更新对象,使用 get()
取得对象,使用 delete(
)删除对象, 使用 clear()
删除所有对象
1 2 3 4 5 6 7 8 9 10 11 const transaction = db.transaction ("users" ), store = transaction.objectStore ("users" ), request = store.get ("007" ); request.onerror = (event ) => alert ("Did not get the object!" ); request.onsuccess = (event ) => alert (event.target .result .firstName ); transaction.onerror = (event ) => { }; transaction.oncomplete = (event ) => { };
不能通过oncomplete
事件处理程序的event
对象访问get()
请求返回的任何数据 因此 仍然需要通过请求的onsuccess
事件处理程序来获取数据
插入对象 使用add()
或put()
写入数据 add()
是插入新值 而put
是更新值
1 2 3 4 5 6 7 8 9 10 11 12 13 let request, requests = []; for (let user of users) { request = store.add (user); request.onerror = () => { }; request.onsuccess = () => { }; requests.push (request); }
通过游标查询 如果想要取得多条数据 则需要在事务中创建一个游标(指向结果集的指针)使用openCursor()
创建
1 2 3 4 5 6 7 8 9 10 11 onst transaction = db.transaction ("users" ), store = transaction.objectStore ("users" ), request = store.openCursor (); request.onsuccess = (event ) => { }; request.onerror = (event ) => { };
游标可用于更新个别记录 update()
方法指定使用的对象更新当前游标对应的值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 request.onsuccess = (event ) => { const cursor = event.target .result ; let value, updateRequest; if (cursor) { if (cursor.key == "foo" ) { value = cursor.value ; value.password = "magic!" ; updateRequest = cursor.update (value); updateRequest.onsuccess = () => { }; updateRequest.onerror = () => { }; } }
如果事务没有修改对象储存的权限 uodate()和delete()
都会抛出错误
默认情况下 每个游标只会创建一个请求 如果想要创建另一个请求 必须调用以下方法
continue(key)
:移动到结果集中的下一条记录 如果没有指定 key 则移动到下一条 否则移动到指定的
advance(count)
:向前移动指定 count 条记录
这两个方法都会让游标重用相同的请求 因此也会重用onsuccess和onerror
处理程序 知道不需要
1 2 3 4 5 6 7 8 9 10 11 request.onsuccess = (event ) => { const cursor = event.target .result ; if (cursor) { console .log (`Key: ${cursor.key} , Value: ${JSON .stringify(cursor.value)} ` ); cursor.continue (); } else { console .log ("Done!" ); } };
键范围 使用键范围可以让游标更容易管理 键范围对应 IDBKeyRange 的实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 const onlyRange = IDBKeyRange .only ("007" );这个范围保证只获取键为"007" 的值。使用这个范围创建的游标类似于直接访问对象存储并调用 get ("007" )。第二种键范围可以定义结果集的下限。下限表示游标开始的位置。例如,下面的键范围保证游标从 "007" 这个键开始,直到最后:const lowerRange = IDBKeyRange .lowerBound ("007" ); 第二种键范围可以定义结果集的下限。下限表示游标开始的位置。例如,下面的键范围保证游标从 "007" 这个键开始,直到最后:const lowerRange = IDBKeyRange .lowerBound ("007" );要同时指定下限和上限,可以使用 bound ()方法。这个方法接收四个参数:下限的键、上限的键、 可选的布尔值表示是否跳过下限和可选的布尔值表示是否跳过上限 要同时指定下限和上限,可以使用 bound ()方法。这个方法接收四个参数:下限的键、上限的键、 可选的布尔值表示是否跳过下限和可选的布尔值表示是否跳过上限 定义了范围之后,把它传给 openCursor ()方法,就可以得到位于该范围内的游标: const store = db.transaction ("users" ).objectStore ("users" ), range = IDBKeyRange .bound ("007" , "ace" ); request = store.openCursor (range); request.onsuccess = function (event ){ const cursor = event.target .result ; if (cursor) { console .log (`Key: ${cursor.key} , Value: ${JSON .stringify(cursor.value)} ` ); cursor.continue (); } else { console .log ("Done!" ); } };
并发问题 如果两个不同的浏览器标签同时打开了同一个网页 可能出现一个网页尝试升级数据库而另一个网页尚未就绪的情形
应该在每次成功打开数据库后都指定 onversionchange 事件处理程序。记住,onversionchange 有可能会被其他标签页触发。
模块 无论一个模块在 require()中被引用多少次 模块永远时单例(无论请求多少次 module 只会被加载一次)
模块第一次被加载后会被缓存 后续加载会取得缓存的模块 模块加载顺序由依赖图决定
在 CommonJS 中 模块加载是模块系统的同步操作
1 2 3 4 console .log ("moduleA" );if (loadCondition) { require ("./moduleA" ); }
异步模块定义(AMD) COmmonJS
以服务器为目标环境 能够一次性将所有的模块都加载到内存中 而异步模块定义(AMD
)的模块定义系统则以浏览器为目标执行环境 这需要考虑网络延迟的问题。
AMD
的一般策略是让模块声明 自己的依赖 而运行在浏览器中的模块系统会按需获取依赖 并在依赖加载完成后 立即执行依赖他们的模块
AMD
模块实现的核心是用函数包装 模块定义 这样可以防止声明全局变量 并允许加载器控制何时加载模块
AMD
模块可以使用字符串标识符指定自己的依赖 而AMD加载器
会在所有依赖模块加载完毕后立即调用模块工厂函数
1 2 3 4 5 6 7 define ('moduleA' , ['moduleB' ], function (moduleB ) { return { stuff : moduleB.doStuff (); }; });
AMD 也支持require和exports
对象 通过他们可以在 AMD 模块工厂函数内部定义 CommonJS 风格的模块
1 2 3 4 5 6 7 8 9 10 define ('moduleA' , ['require' , 'exports' ], function (require , exports ) { var moduleB = require ('moduleB' ); exports .stuff = moduleB.doStuff (); }); 动态依赖也是通过这种方式支持的: define ('moduleA' , ['require' ], function (require ) { if (condition) { var moduleB = require ('moduleB' ); } });
UMD UMD
用于创建两个系统都可以使用的模块代码 本质上 UMD 定义的模块会在启动时检测到要使用那个模块系统 然后进行适当配置 并把所有逻辑包装在一个立即调用的函数表达式中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 (function (root, factory ) { if (typeof define === "function" && define.amd ) { define (["moduleB" ], factory); } else if (typeof module === "object" && module .exports ) { module .exports = factory (require (" moduleB " )); } else { root.returnExports = factory (root.moduleB ); } })(this , function (moduleB ) { return {}; });
工作者线程 使用工作者线程 浏览器可以在原始页面环境之外再分配一个完全独立的二级子环境 这个子环境不能与依赖单线程交互的 API 互操作 但可以与父环境并行执行代码
工作者线程和线程
工作者线程是以实际线程实现的
工作者线程并行执行 虽然页面和工作者线程都是单线程 JavaScript 环境 每个环境中的指令可以并行执行
工作者线程可以共享某些内存 使用SharedArrayBuffer
在多个环境间共享 内容 JavaScript 使用 Atomics
接口实现并发控制
工作者线程不共享全部内存
工作者线程不一定再同一个进程里
创建工作者线程的开销更大 工作者线程有自己独立的事件循环 全局对象 事件处理程序 和其他 js环境 必须的特性
工作者线程相对比较重 不建议大量使用 工作者线程应该是长期运行的 启动成本比较高 每个实例占用的内存耶比较大
工作者线程的类型 专用工作者线程、共享工作者线程和服务工作者线程
专用工作者线程 可以让脚本单独创建一个 js 线程 只能被创建它的页面使用
共享工作者线程 可以被多个不同的上下文使用 任何与创建共享工作者线程的脚本同源的脚本,都可以向共享工作者线程发送 消息或从中接收消息
服务工作者线程 主要用于拦截、重定向和修改页面发出的请求 充当网络请求的仲裁者
ES2018 与 ES2019 剩余运算符再对象间执行前复制 因此只会复制对象的引用而不会克隆整个对象
1 2 3 4 5 6 7 8 9 10 const person = { name : "Matt" , age : 27 , job : { title : "Engineer" , level : 10 } };const { ...remainingData } = person;console .log (person === remainingData); console .log (person.job === remainingData.job ); remainingData.name = "olddog" ; person.name = "wqdq" ; console .log (remainingData.name ); person.name ; person.job .title = "123" ; remainingData.job , title;
剩余运算符会复制所有自有可枚举属性 包括括号
1 2 3 4 const s = Symbol ();const foo = { a : 1 , [s]: 2 , b : 3 };const { a, ...remainingData } = foo;console .log (remainingData);
扩展运算符 扩展运算符会像拼接数组一样合并两个对象 应用到内部对象的扩展运算符会对所有自有可枚举属性执行浅复制到外部对象 包括符号
对象跟踪插入顺序 从扩展对象复制的属性按照他们在对象字面量中列出的顺序插入
对象会覆盖重名属性 出现重名属性时会使用后续出现的值
与剩余操作符一样 所有复制都是浅复制
Promise.prototype.finally() 有了Promise.prototype.finally
可以统一共享的处理程序 finally()
不传递任何参数 也不知道自己处理的promise
是什么状态
每个finally()
都会创建一个新的promise
实例 而这个新promise
会被添加到浏览器的微任务队列 只有前面的处理程序执行完成才会解决
数组打平方法
flat()
和 flatMap()
只能用于打平嵌套数组。嵌套的可迭代对象如 Map 和 Set 不能打平
Array.prototype.flatten() 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 function flatten (sourceArray, flattenedArray = [] ) { for (const element of sourceArray) { if (Array .isArray (element)) { flatten (element, flattenedArray); } else { flattenedArray.push (element); } } return flattenedArray; } const arr = [[0 ], 1 , 2 , [3 , [4 , 5 ]], 6 ];console .log (flatten (arr));function flatten (sourceArray, depth, flattenedArray = [] ) { for (const element of sourceArray) { if (Array .isArray (element) && depth > 0 ) { flatten (element, depth - 1 , flattenedArray); } else { flattenedArray.push (element); } } return flattenedArray; } const arr = [[0 ], 1 , 2 , [3 , [4 , 5 ]], 6 ];console .log (flatten (arr, 1 ));
Array.prototype.flat()
接收一个 depth 参数 默认为 1 返回一个打平 Array 实例的浅复制版本
1 2 3 4 5 6 const arr = [[0 ], 1 , 2 , [3 , [4 , 5 ]], 6 ];const arr1 = arr.flat (2 );console .log (arr1); arr1.push (10 ); console .log (arr1); console .log (arr);
Array.prototype.flatMap() Array.prototype.flatMap()
方法会在打平数组之前执行一次映射操作。在功能上,arr.flatMap(f)
与 arr.map(f).flat()
等价;但 arr.flatMap()
更高效,因为浏览器只需要执行一次遍历
1 2 3 4 5 const arr = [[1 ], [3 ], [5 ]];console .log (arr.map (([x] ) => [x, x + 1 ]));console .log (arr.flatMap (([x] ) => [x, x + 1 ]));
flatMap()
在非数组对象的方法返回数组时特别有用,例如字符串的 split()
方法
1 2 3 4 5 const arr = ['Lorem ipsum dolor sit amet,' , 'consectetur adipiscing elit.' ];console .log (arr.flatMap ((x ) => x.split (/[\W+]/ )));"elit" , "" ]
用于通过键/值对数组的 集合构建对象。这个方法执行与 Object.entries()
方法相反的操作
1 2 3 4 5 6 7 8 9 const obj = { foo : "bar" , baz : "qux" , }; const objEntries = Object .entries (obj);console .log (objEntries);console .log (Object .fromEntries (objEntries));
可以快速地将Map
实例转换为Object
实例 因为 Map 迭代器返回的结果与formEntries()
的参数恰好匹配
1 2 const map = new Map ().set ("foo" , "bar" );console .log (Object .formEntries (map));
字符串修理方法 trimStart():
删除字符串开头的空格
trimEnd()
;删除末尾的空格
在只有一个空格的情况下 这两个方法相当于执行与padStart()和padEnd()
相反的操作
1 2 3 let s = " foo " ;console .log (s.trimStart ()); console .log (s.trimEnd ());
Symbol.prototype.description 用于取得可选的符号描述 只读的 如果没有描述 默认为undefined
1 2 const s = Symbol ("foo" );console .log (s.description );
可选的 catch 绑定 在try/catch
中 可以忽略catch
的错误对象不做任何操作
1 2 3 4 5 try { throw "foo" ; } catch { }