迷惑的知识点:
作用域
函数、闭包
面向对象
ES的新特性
其他知识
从图中可知 js、css文件并不是跟index.html文件一起下载下来的,而是需要时才会下载
浏览器内核来解析下载下来的文件
| 内存 | 使用 |
|---|---|
| Gecko | 早期被Netscape和Mozilla Firefox浏览器使用 |
| Trident | 微软开发,被IE4~IE11浏览器使用,但Edge转向使用Blink |
| Webkit | 苹果给予KHTML开发、开源的,用于Sfari,Google Chrome之前也在用 |
| Blink | Webkit的一个分支,Google开发,目前应用于Google Chrome、Edge、Opera等 |
| 。。。 | 。。。 |
浏览器内核常指浏览器的排版引擎
排版引擎(layout engine),也成为浏览器引擎(browser engine)、页面渲染引擎(rendering engine)或样板引擎
HTML Parser把HTML文件解析成DOM TreeDom进行操作从而修改Domt Tree,这执行Javascript代码就是js引擎CSS Parse解析成Style Rulescss规则Style Rules和Dom Tree结合(Attachment)到一起,生成渲染树(Render Tree)Painting)到界面上显示(Display)出来JavaScript代码通过JavaScript引擎来执行
| 引擎 | 使用 |
|---|---|
| SpiderMonkey | 第一款JavaScript引擎,有Brendan Eich开发 |
| Chakra | 微软开发,用于IE浏览器 |
| JavascriptCore | WebKit中的JavaScript引擎,由Apple公司开发 |
| V8 | Google开发的强大JavaScript引擎,也帮助Chrome从众多浏览器中脱颖而出 |
| 。。。 | 。。。 |
ECMAScript和WebAssembly,并在Windows7或更高版本,MacOS 10.12+和使用x64、IA-32、ARM或MIPS处理器的linux系统上运行
Parse解析Javascript源代码(包括词法分析和语法分析)成抽象语法树(AST)
AST可以通过V8引擎中的Ignition库转换成字节码(bytecode),不直接转换成机器码是为了根据运行环境做代码优化和环境适配等
V8引擎本身的源码非常复杂,大概有超过100w行C++代码,通过了解它的架构,我们可以知道他是如何对Javascript执行的
Parse模块会将Javascriptdiamagnetic转换成AST(抽象语法树),这是因为解释器并不直接认识JavaScript代码
Ignition是一个解释器,会将AST转换成ByteCode字节码
TurboFan优化所需要的信息(比如函数参数的类型信息,有了类型才能真实运算)Ignition会执行解释执行ByteCodeIgnition的V8官方文档https://v8.dev/blog/ignition-interpreterTurboFan是一个编译器,可以将字节码编译为CPU可以直接执行的机器码
Blink是Chrome内核,在解析到JavaScript时将JS代码通过流(Stream)的方式传到V8引擎Scanner(扫描器,做词法分析)Scanner将代码转换成许多Tokens,再通过Parse解析转化成AST
Parser就是直接将Tokens转成AST树结构PreParser称之为与解析
Outer()内部定义了Inner()函数,那么Inner()函数就会进行预解析在Parse阶段时,V8引擎会自动创建GlobalObject对象,比如Stirng、Date、Number、SetTimeout()、window...等都是GlobalObject的成员属性,所以在JS代码中可以字节调用这些对象
// 测试代码
var name = "why";
console.log(num1);
var num1 = 10;
var num2 = 20;
var result = num1 + num2;
foo(1)
function foo(num){
var m = 10;
var n = 20;
console.log('foo');
}
GlobalObject对象var GlobalObject = {
String : "类",
Date : "类型",
setTimeOut : "函数",
name : undefined,
num1 : undefined,
num2 : undefined,
result : undefined
foo : 0xa00
}
String、Date、setTimeOut是
GlobalObject自带的对象
name、num1、num2、result是解析出来的变量并添加到GlobalObject中
因为此时只是解析阶段,所以name、num1、num2、result都是undefined的
foo存储的函数地址
这些上下文中存在一个VO(variable object)(变量对象),其中VO分为GO和AO两种,全局上下文中VO = GO,函数执行上下文中VO = AO
代码从上下往下依次执行,从VO中取出目标对象并为其赋值
foo存储函数空间中存在一个父级作用域(parent scope),可以获得父级作用域中的数据,当foo作用域中的使用的数据在foo作用域中没找到就往父级作用域(parent scope)中查找,如果一直没有最终会找到全局作用域
var message = "Hello Global";
function foo(){
console.log(message);
}
function bar(){
var message = "Hello Bar";
foo();
}
bar(); // 输出 Hello Global
GlobalObject对象foo的父级作用域(parent scope)就是GlobalObject
bar的父级作用域(parent scope)也是GlobalObject
赋值。。。
function foo(){
m = 100;
}
foo();
console.log(m);
function foo1(){
var a = b = 10;
/*
等价于
var a = 10;
b = 10;
*/
}
foo1();
console.log(a);
console.log(b);
foo函数来说,如果没有用var或者let定义变量,则m会被直接定义到全局变量(GO)中,所以对于console.log(m)会输出100而不是报错foo1函数来说,var a = b = 10;会被理解为var a = 10; b = 10;,根据1的解释,b会被定义到全局变量中,而a还在foo1的AO中,所以最后console.log(a)会报错,而console.log(b)会输出10每一个执行上下文会关联到一个环境变量(Variable Environment)中,在执行代码中变量和函数的声明会作为环境记录(Environment Record)添加到变量环境中
对于函数来说,参数也会被作为环境记录添加到变量环境中
所以对于上面的解释来说,将不再是VO(变量环境),而是环境记录(VariableEnvironment),也就是说不一定是O(object),只要是记录都行(map或者其他可以记录的类型)
function foo() {
console.log(n) // 输出 undefined
var n = 200
console.log(n) // 输出 200
}
var n = 100;
console.log(n)
针对上面的测试用例,第二个输出为 200 应该很好理解,第一个输出为什么是 undefined 呢?
因为函数 foo 本身也有有一个函数作用域 FEC,与全局作用域变量初始化一样,先给定义每个变量,再根据执行顺序给各个变量赋值,由于函数 foo 内定义的变量 n,所依此时函数作用域内会定义变量 n 并赋值为 undefined (这个 n 与全局变量中的 n 不是同一个)
那么当第一个 console.log(n) 执行的时候 n 并未赋值,所以会输出 undefined
var a = 100
function foo() {
console.log(a) // 输出 undefined
return
var a = 100
}
foo()
这个输出 undefined 与上面的案例同理,虽然这次 foo 函数中的变量 a 定义在 return 之后,是永远不会被执行的,但是代码在解析阶段是不会执行的,也就是不知道会被 return 掉,所以在 foo 的作用域内依旧初始化了变量 a 并且赋初值为 undefined
不管什么语言,在代码执行的过程中都需要分配内存,不同的是某些语言需要手动管理内存,某些编程语言可以帮助管理内存
一般而言,内存管理存在如下的生命周期
JavaScript会在定义变量时为我们分配内存
因为内存是有限的,所以当内存不再需要的时候,我们需要对其进行释放,以便腾出更多的内存空间
再手动管理内存的语言中,我们需要通过一些方式来释放不再需要的内存,比如free函数:
1. 手动管理的方式会**影响编写逻辑的代码的效率**
2. 对开发者**要求较高**,不小心就会产生**内存泄漏**
JS引用使用标记清除算法,V8引擎为了更好的优化,它在算法的实现细节上也会结合一些其他的算法
JS中函数是一等公民,函数可以作为参数传递、作为返回值
function foo(func){
func();
}
function bar(){
console.log("aaa");
}
function run(){
function rush(){
console.log("bbb");
}
return rush;
}
foo(bar);
var fn = run();
fn();
高阶函数:一个函数如果接受另外一个函数作为参数,或者该会将另外一个函数作为返回值,就被称为高阶函数
比如上述
run()就是高阶函数
当函数属于某个对象时,称该函数为对象的方法
闭包:词法闭包或函数闭包
闭包解释2
function run(){
var name = "run";
function rush(){
console.log(name);
}
return rush;
}
var fn = run();
fn();
在常见的编程语言中,this 通常只出现在类的方法中,但是在 javascript 更加灵活
函数第一公民 可以作为形式参数和返回值
副作用:在执行一个函数时,除了返回函数值以外,对调函数产生了附加的影响,比如修改了全局变量、修改参数或者改变外部的存储
副作用往往是产生BUG的温床
var names = ["avc", "cba", "eax", "fas"];
// 纯函数,确定输入确定输出,没有副作用(没有修改外部变量等,原来的数组name没有被修改)
var name2 = names.slice(0, 3);
// 非纯函数,调用之后原来的数组name被改变了
function foo1(num1, num2){ // 纯函数
return num1 + num2;
}
var name = "log";
function foo2(num1, num2) { // 非纯函数 修改了外界的值
name = "log1";
return num1 + num2;
}
function foo(m, n, x, y){
return m + n * 2 + x * 3 + y * y;
}
foo(1, 2, 3, 4);
function bar(m){
return function(n){
n = n * 2;
return function(x){
x = x * 3;
return function(y){
y = y * y;
return m + n + x + y;
}
}
}
}
bar(1)(2)(3)(4);
// 简易柯里化写法
var bar2 = m => n => x => y => m + n * 2 + x * 3 + y * y;
var bar2 = m => n => x => y => {
return m + n * 2 + x * 3 + y * y;
}
从
function foo()变function bar()的过程 称为柯里化
// 柯里化的代码复用
function MakeAddr(num){
return function Addr(count){
return count + num;
}
}
var addr = MakeAddr(5);
addr(1); // 5 + 1
addr(2); // 5 + 2
addr(3); // 5 + 3
// 普通写法
function Add(m , n){
return m + n;
}
Add(5, 1); // 5 + 1
Add(5, 2); // 5 + 2
Add(5, 3); // 5 + 3
如果需要频繁对一个数进行加减处理,使用柯里化的代码比普通写法的字母数更少(不用每次都写"5,")
// 函数的形参个数
function adddd(x, y, z){
}
console.log(adddd.length); // 输出3,即表示该函数有三个形参
// 自动柯里化函数
function hyCurring(fn){
function curried(...args){
if(args.length >= fn.length){
return fn.apply(this, args);
// return fn.call(this, ...args);
// return fn(...args);
}
else {
function curried2(...args){
return curried.apply(this, args.concat(args2));
}
}
}
return curried;
}
function double(num){
return num * 2;
}
function square(num){
return num ** 2;
}
var count = 10;
var result = square(double(count));
var count1 = 10;
var result1 = square(double(count));
var count2 = 10;
var result2 = square(double(count));
function composeFn(m, n){
return function(count){
n(m(count));
}
}
var newFn = composeFn(double, square);
count3 = 10;
result3 = newFn(count3); // 组合了 double和square的函数
function hyCompose(...fns){
val length = fns.length;
for(let i = 0; i < length; i++){
if(typeof fn[i] !== 'function'){
throw new TypeError("");
}
}
function compose(...args){
var index = 0;
var result = length ? fn[index].apply(this, args) : args;
while(index < length){
result = fns[index].call(this, result);
}
}
return compose;
}
var message = "VO : GO";
var obj = {name : "Y", message = "Obj message"};
function foo() {
function bar() {
with(obj){
console.log(message);
}
}
bar();
}
foo(); // 输出 Obj message
with() {}语句用于定义对象查找作用域
不建议使用with语句,存在兼容性问题
var jsString = 'var message = "hello world"; console.log(message);'
eval(jsString;)
通过
eval来将字符串翻译成js语句并执行
Google Chrome报错,不推荐在开发中使用eval可读性差
运行中可能被篡改
不能被js引擎优化,因为是eval去执行的不经过引擎
面向对象是现实的抽象方式
对象是JavaScript中一个非常重要的概念,因为对象可以将多个相关联的数据封装到一起,更好的描述一个事物
// 创建对象 方式1 使用Object类和new关键字来创建对象
var obj2 = new Object();
obj.name = "y";
obj.age = 15;
obj.height = 180;
// 创建对象 方式2 通过 字面量 的方式
var obj = {
name : "y",
age : 15,
height : 180,
eat : function() {
console.log("在吃饭");
}
};
{}是字面量,可以立即求值,而new Object()本质上是方法(只不过这个方法是内置的)调用,既然是方法调用,就涉及到在proto链中遍历该方法,当找到该方法后,又会生产方法调用必须的堆栈信息,方法调用结束后,还要释放该堆栈
var obj = {
name : "y",
age : 16
};
console.log(obj.name); // 获取属性
obj.name = "j"; // 修改属性
delete obj.name; // 删除属性
for (var key in obj){ // 遍历属性
console.log(key);
}
上述对象的属性都是直接定义在对象内部,或者直接添加到对象内部的,这样做不能对这个属性进行一些限制:比如是否可以delete/被遍历等
为了对属性进行比较精准的操作控制,我们可以使用属性描述符,通过属性描述符可以精准的添加或修改对象的属性,属性描述符需要使用Object.defineProperty来对属性进行添加或修改
// obj : 对象、prop : 属性、descriptor : 属性描述符
Object.defineProperty(obj, prop, descriptor);
Object.defineProperty()会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
该方法会返回被修改的对象,原本的对象也会被修改,所以可以不用接收函数返回值,并且可以发现该函数并非纯函数
| configurable | enumerable | value | writable | get | set | |
|---|---|---|---|---|---|---|
| 数据描述符 | 可以 | 可以 | 可以 | 可以 | 不可以 | 不可以 |
| 存取描述符 | 可以 | 可以 | 不可以 | 不可以 | 可以 | 可以 |
configurable 属性是否可以通过delete删除属性,是否可以修改其特性或者修改为存取描述符
enumerable 属性是否可以通过for-in或者Object.keys()返回该属性
enumerable为trueenumerable为falsewritable 是否可以修改属性的值
writable为trueObject.defineProperty定义一个属性时,writable为falsevalue 的具体值,读取属性时返回该值,修改属性时修改该值
value是undefined的get/get,为获得和设置使用的函数
存取属性描述符:只能设置configurable,enumerable,get,set
数据属性描述符,只能设置configurable,enumerable,writable,value
个人理解:
对于不需要进行额外处理的数据可以使用数据属性描述符,比如PI = 3.1415926
对于需要额外处理的数据,比如年龄只能是10~20就需要使用存取属性描述符
var obj = {
name : "y",
age : 16
};
// obj对象不存在height属性,则先添加height属性,再设置其属性描述
Object.defineProperty(obj, 'height', {
value : 180
});
console.log(obj);
这里obj并不会输出height属性,因为
height是通过属性描述符添加的所以enumerable默认为false
// configurable展示
// obj就是前面的obj
Object.defineProperty(obj, 'height', {
value : 180,
configurable : false,
});
delete obj.name
console.log(obj.name); // undefined
delete obj.height
console.log(obj.height); // 180 没有被删除
Object.defineProperty(obj, 'height', {
value : 200,
configurable : true,
});
console.log(obj.height); // 180 没有被修改成200,因为configurable最开始是false
// enumerable展示
Object.defineProperty(obj, 'height', {
value : 180,
configurable : false,
enumerable : true
});
console.log(obj); // 此时可以正常打印出height属性
var obj = {
name : "y",
age : 16 ,
_address : "private" // 个人习惯:前面有个下划线表示私有变量
};
Object.defineProperty(obj, 'address', {
configurable : true,
enumerable : true,
get : function () {
return this._address;
},
set : function(value){
this._address = value;
}
});
这里使用
address属性作为_address属性的代理
因为_address作为私有变量不希望外界随意读取,所以使用address代理的方法
var obj = {
_age : 0,
}
Object.defineProperties(obj, {
name : {
configurable : true,
enumerable : true,
writable : true,
value : "y"
},
age : {
configurable : true,
enumerable : true,
get : function (){
return this._age;
},
set : function (value) {
this._age = value;
}
}
});
console.log(obj);
var obj = {
_age : 10,
set age(value){
this._age = value;
},
get age() {
return this._age;
}
}
obj.age = 20;
console.log(obj);
var obj = {
_age: 0,
}
console.log(Object.getOwnPropertyDescriptor(obj, "_age"));
var obj = {
name: "y",
age: 16,
_address: "private"
};
console.log(Object.getOwnPropertyDescriptors(obj));
Object.preventExtensions(obj);
Object.seal(obj);
Object.freeze(obj);
如果所有对象都使用前面所说的字面量方式来创建,那么会出现特别多的重复代码
function createPerson(name, age){
var person{};
person.name = name;
person.age = age;
return person;
}
var p1 = createPerson("name1", 10);
var p2 = createPerson("name2", 11);
var p3 = createPerson("name3", 12);
function foo(){
console.log("hello world");
}
var f = new foo();
var f2 = new foo; // 如果不需要传递参数,可以不要小括号
console.log(f2); // 返回了一个foo类型的对象
当使用
new关键字之后,foo就从普通函数变成了构造函数
new操作符调用,那么会执行如下操作
[[prototpe]]属性会被赋值为该构造函数的prototype属性function Person(name, age, height){
this.name = name;
this.age = age;
this.height = height;
this.eating = function (){
console.log(this.name + "正在吃饭");
}
}
var p1 = new Person("张三", 18, 180);
var p2 = new Person("李四", 10, 180);
console.log(p1.eating === p2.eating); // false
对比方法1的工厂方法,该方法可以明确知道p1这个变量是什么类型
p1和p2的函数并不是相同,可见每个函数都开辟了一个内存空间,存在浪费的问题
JavaScript当中每个对象都有一个特殊的内置属性[[prototype]],这个特殊的对象可以指向另一个对象,一般把[[prototype]]称为隐式原型(一般看不到、不会改、用不到)
prototype是原型的意思,在浏览器中可以使用
obj.__proto__来查看[[prototype]](部分浏览器支持)
var obj = {};
obj.__proto__.age = 10;
console.log(obj.age);
[[get]]操作[[prototype]](可以用来实现继承等操作)函数是一个对象,所以也有隐式原型[[prototype]]
函数存在一个显示原型prototype
[[prototype]]和prototype不是一个东西,前者是理论名称,后者是实际属性
fun.__proto__中的__proto__并不是标准支持的,而是部分浏览器为了方便程序员debug而增加的,__proto__的作用是为了显式的显示对象的[[prototype]]这种理论上的隐式原型对象
function Person() {
}
// 函数也是一个对象,所以也有隐式原型[[prototype]]
console.log(Person.__proto__); // {}
console.log(Person.prototype); // {}
console.log(Object.getOwnPropertyDescriptors(Person.prototype)); // 输出一个constructor属性,指向构造函数本身
var p1 = new Person();
var p2 = new Person();
console.log(p1.__proto__ === Person.prototype); // true
Person.prototype.name = "y";
console.log(p1.name, " ", p2.name);
上面有对
new的调用操作进行解释,其中第二点对象内部的[[prototpe]]属性会被赋值为该构造函数的prototype属性的意思就是将返回对象的__proto__赋值等于函数的prototype,所以console.log(p1.__proto__ === foo.prototype)返回值为true
foo.prototype = {
name : "y",
age : 19,
}
Object.defineProperty(foo.prototype, "constructor", {
enumerable : false,
configurable : true,
writable : true,
value : foo
});
直接修改
prototype对象,但是prototype中必须存在一个constructor属性指向本身
function Person(name, age){
this.name = name;
this.age = age;
}
Person.prototype.eating = function() {
console.log(this.name + "吃东西");
}
var p1 = new Person("x", 10);
var p2 = new Person("y", 10);
p1.eating();
p2.eating();
function Person(name, age){
this.name = name;
this.age = age;
}
var p1 = new Person("x", 10);
var p2 = new Person("y", 10);
上述的Person应该称之为构造函数,但是对其他语言来说更像是一个类
var obj = {
name : "y"
}
console.log(obj.address);
obj对象并没有address属性,所以回去obj.__proto__原型上查找,如果也没有就会在obj.__proto__.__proto__上去查找直到找到或者顶层原型为止,这种类似链表的查找方式就是原型链
顶层
__proto__就是Object.__proto__
var obj = {};
console.log(obj.__proto__ === Object.prototype); // true
function Person(name, age){
this.name = name;
this.age = age;
}
Person.prototype.running = function() {
console.log(this.name + " is running");
}
function Student(sno){
this.sno = sno;
this.friends = [];
}
var p = new Person("1", "2");
Student.prototype = p;
Student.prototype.studying = function(){
console.log(this.name + " is studying");
}
function Teacher(title){
this.title = title;
}
Teacher.prototype.teaching = function() {
console.log(this.name + " is teaching");
}
var s1 = new Student(1);
var s2 = new Student(2);
s1.running();
s1.studying();
上述代码的内存解释
代码中能明显发现问题:s1、s2是两个对象公用同一个Person对象的引用会互相影响
为了解决原型链继承中存在的问题,开发人员提供了一种新的技术:constructor stealing(借用构造函数、经典继承、伪造对象)
apply()或call()方法也可以在新创建的对象上执行构造函数function Person(name, age, friends){
this.name = name;
this.age = age;
this.friends = friends;
}
Person.prototype.eating = function() {
console.log(this.name + " eating");
}
var p = new Person();
function Student(name, age, friends, sno) {
Person.call(this, name, age, friends);
this.sno = sno;
}
Student.prototype = p;
var s1 = new Student("y", 10, ["1", "2"], 1);
本质上是
Student借用执行Person的构造函数的执行过程,本质其实是给Student赋值
Person函数执行了两次)一种继承方法,不是通过构造函数实现的方法
var obj = {
name : "y",
age : 16
};
function createObject(protoObj){
var newObj = {};
Object.setPrototypeOf(newObj, protoObj); // 设置newObj的原型为protoObj
return newObj;
}
// 不适用Object函数库实现设置原型的方法
function createObject2(protoObj){
function Fn() {}
Fn.prototype = protoObj;
var new Obj = new Fn();
return newObj;
// newObj.__proto__ = protoObj; // 不可这么写,因为__proto__不是所有js引擎都支持
}
// 创建info对象的原型指向obj对象
var info = {};
console.log(info);
console.log(info.__proto__);
info = Object.create(obj); // 功能等价于 createObject 和 createObject2
Object.setPrototypeOf(newObj, protoObj);设置newObj的原型为protoObj
寄生式继承的思路是结合原型类继承和工厂模式的一种方式
即创建一个封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再将这个对象返回
var personObj = {
running = function() {
console.log("running");
}
}
function createStudent(person, name){ // 工厂函数
var stu = Object.create(person); // 原型式继承
stu.name = name;
stu.studying = function() {
console.log("studying");
}
}
var stu1 = createStudent(person, "x");
var stu2 = createStudent(person, "y");
每个对象的
studying()方法都是新建的
stu1和stu2没有明确的类型(consolo.log一下就知道)
function CreateObject(o){
function Fn (){}
Fn.prototype = o;
return new Fn;
}
function Person(name, age, friends){
this.name = name;
this.age = age;
this.friends = friends;
}
Person.prototype.running = function() {
console.log(this.name + " running");
}
function Student(name, age, friends, sno, score) {
Person.call(this, name, age, friends);
this.sno = sno;
this.score = score;
}
Student.prototype = CreateObject(Person.prototype);
Student.prototype.studying = function() {
console.log(this.name + " studying " + this.score);
}
var stu = new Student("x", 10, [], 1, 100);
console.log(stu); //Person { name: 'x', age: 10, friends: [], sno: 1, score: 100 }
stu.running(); // x running
stu.studying(); // x studying 100
输出
stu是Person类的,因为输出的是constructor的name属性,而这里的constructor使用的是Person的所以最后输出的名字是Person
function inheritPrototype(SubType, SuperType){
SubType.prototype = CreateObject(SuperType.prototype);
Object.defineProperty(SubType.prototype, 'constructor', {
enumerable : false,
configurable : true,
writable : true,
value : SubType
});
}
inheritPrototype(Student, Person);
手动设置
Student的constructor是Student自己就行了
var obj = {
name : "w",
age : 19
};
var info = Object.create(obj, {
address : {
value : "BJ",
enumerable : true
}
});
console.log(info);
console.log(info.hasOwnProperty('address')); // true
console.log(info.hasOwnProperty('name')); // false
console.log("address" in info); // true
console.log("name" in info); // true
创建对象时,为新对象添加属性描述符
info.hasOwnProperty('address')判断属性是否是自己的属性
"name" in info判断对象是否存在name属性
instanceof用于检测构造函数的prototype是否出现在某个实例对象的原型链上
function CreateObject(o){
function Fn (){}
Fn.prototype = o;
return new Fn;
}
function inheritPrototype(SubType, SuperType){
SubType.prototype = CreateObject(SuperType.prototype);
Object.defineProperty(SubType.prototype, 'constructor', {
enumerable : false,
configurable : true,
writable : true,
value : SubType
});
}
function Person(){
}
function Student() {
}
inheritPrototype(Student, Person);
var stu = new Student()
console.log(stu instanceof Student); // true
console.log(stu instanceof Person); // true
console.log(stu instanceof Object); // true
instanceof后面的必须是构造函数
JavaScript当中每个对象都有一个特殊的内置属性[[prototype]],这个特殊的对象可以指向另一个对象,一般把[[prototype]]称为隐式原型(一般看不到、不会改、用不到)
var obj = {};
console.log(obj.__proto__);
函数是一个对象,所以也有隐式原型[[prototype]]
函数存在一个显示原型prototype
当创建一个函数后,JS引擎会自动给函数对象添加属性Foo.prototype = { constructor : Foo }
定义Foo()函数时,相当于new Funtion()创建函数对象,这时编译器执行Foo.__proto__ = Function.prototype,而Function.prototype = { constructor : Function }
Function是极为特殊的对象,它的prototype和__proto__相等
function Foo(){
}
console.log(Foo.__proto__);
console.log(Foo.prototype);
console.log(Foo.prototype === Foo.__proto__); // false
console.log(Foo.prototype.constructor); // Function : Foo
console.log(Foo.__proto__.constructor); // Function : Function
console.log(Function.prototype === Function.__proto__); // true
理论上
class的底层实现方式还是 上述的旧版创建代码
使用babel可以将代码装成旧版本代码
constructorconstructor方法constructor方法,如果有多个会抛出异常// 类声明
class Person{
constructor(name, age){
this.name = name;
this.age = age;
this._address = "";
}
eating() {
console.log(this.name + " eating");
}
running() {
console.log(this.name + " running");
}
// 访问器
get address(){
return this._address;
}
set address(value){
this._address = value;
}
// 静态方法
static createPerson(){
return new Person("", 1);
}
};
console.log(Person.prototype);
console.log(Person.prototype.constructor); // 指向当前Person
console.log(typeof Person); // function
// 类的表达式 用的比较少
var Animal = class {
};
var p1 = new Person("x", 1);
var p2 = new Person("y", 2);
super关键字,一般用在三个地方:子类的构造函数、实例方法、静态方法
在子类的构造函数中使用this或者返回默认对象之前,必须先通过super调用父类的构造函数
class Person{
constructor(name, age){
this.name = name;
this.age = age;
}
eating() {
console.log(this.name + " eating");
}
};
class Student extends Person {
constructor(name, age, sno){
super(name, age); // 调用父类构造方法
// super.eating(); // 调用父类的方法
this.sno = sno;
}
// 方法的重写
eating() {
console.log("Student " + this.name + " eating");
}
};
babel转换ES6为ES5class MyArray extends Array{
firstItem(){
return this[0];
}
lastItem(){
return this[this.length-1];
}
}
var arr = new MyArray(1, 2, 3);
console.log(arr.firstItem());
console.log(arr.lastItem());
扩展数组功能
Javascirpt的类只支持单继承,也就是说它只能有一个父类
class Person{
}
class Runner {
running(){
}
}
class Eater {
eating() {
}
}
function mixinRunner(BaseClass){
class NewClass extends BaseClass {
running() {
console.log("running");
}
}
return NewClass;
}
class Student extends Person{
}
var NewStudent = mixinRunner(Student);
var ns = new NewStudent();
ns.running();
通过
mixinRunner扩展类的功能
[name+123]来设置key的名称(下面的[name+123]例子)var name = "w";
var age = 10;
var obj = {
name,
age,
foo : function() {
console.log(this);
},
bar() {
console.log(this);
},
baz : () => {
console.log(this);
},
[name + 123] : "hgggg"
}
console.log(obj);
obj.foo();
obj.bar();
obj.baz();
var names = ["1", "2", "3"];
// 解构数组
var [item1, item2, item3] = names;
console.log(item1, item2, item3); // 1 2 3
// 解构后面的元素
var [, itema, itemb] = names;
console.log(itema, itemb); // 2 3
// 解构一个元素,后面的放到新数组
var [itemc, ...newNames] = names;
console.log(itemc); // 1
console.log(newNames); // 2 3
// 结构默认值 如果没有解构出来的值
var [item11, item22, item33, item44 = "aaa"] = names;
console.log(item44);
var obj = {
name : "w",
age : 15,
height : 190
};
var {name, age, height} = obj;
console.log(name, age, height);
var {height, age, name} = obj; // 读取顺序无关
console.log(name, age, height);
var {name : NewName} = obj; // 给换个新的变量名
console.log(NewName); // w
var {address : newAddress = "BJ"} = obj; // 设置默认值
console.log(newAddress);
一般而言,我们声明的变量和环境记录是被添加到环境变量中的,但是ECMA标准并没有规定这个对象是window对象还是其他对象,不同的引擎会有不同的实现。V8引擎是通过VariableMap一个HashMap来实现存储的。而window对象早期是GO对象,在新的实现中其实是浏览器添加的全局对象,并且一直保持了Windows和var之间值的相等性
console.log(foo); // undefined 不报错
var foo = "foo";
console.log(bar); // Error
let bar = "bar";
bar实际上是被创建了出来,但是不能访问
const name = "a";
name = "B"; // error
const obj = {};
obj.name = "B"; // success
ES5之前只有两个东西会形成作用域:
作用域可访问外部对象,外部对象不可访问作用域内部数据
ES6开始出现块级作用域,对var声明的对象无效,对let、const、class、function声明的类型有效
{
let x = "x";
var y = "y";
function demo(){
console.log(x);
}
}
console.log(y); // y
console.log(x); // Error
demo();
demo()方法可能可以执行,部分浏览器为了兼容旧版本让function不管块级作用域,如果是只支持ES6的浏览器会访问不到demo()
if(true){
// 块级作用域
}
switch(color){
case "red":
// 块级作用域
break;
}
for(let i=0; i<10; i++){
// 块级作用域
}
console.log(i); // Error
暂时性死区:在ES6中使用let、const声明的变量,在声明和初始化之前,变量都是不可以访问的(temporal dead zon)
var foo = 'foo';
if(true){
console.log(foo); // ERROR
let foo = 'abc';
}
理论上块级作用于外存在
foo对象,但是console.log(foo)报错
因为该块级作用域定义了let foo = 'abc',搜易console.log(foo)是要访问等于abc的foo,但是代码还没跑到foo = 'abc'这一行,所以foo不可访问(暂时性死区)
强烈建议不要使用var定义变量
let和const更适合开发中使用
const name = "x";
const age = 15;
const height = 190;
console.log("my name is " + name + ", age is " + age + ", height is " + height);
const message = `my name is ${name}, age is ${age}, height is ${height}`;
console.log(message);
const message1 = `my name is ${name}, age is ${age * 2}, height is ${height / 100.0}`;
console.log(message1);
${}function foo(m, n){
console.log(m, n);
}
// 正常调用
foo(10, 20);
// 标签模板字符串调用
foo``
foo`hello`
const name = "xyz";
const age = 19;
foo`hello${name}wo${age}rld` // ['hello','wo','rld'] xyz
部分框架使用了这个技术,比如react的stypled-components的三方库
有默认值的形参最好放到最后(在C++中,有默认值的形参必须放在最后一个)
function ES5Foo(m, n){
m = m || "aaa"; // 如果是undefined 就设置默认值
n = n || "bbb"; // 如果传入是0,也会被赋值,是BUG
}
function foo(m = "aaa", n = "bbb"){
console.log(m, n);
}
foo();
foo(1);
foo(1, 2);
var obj = {
name : "xtz",
age : 10,
height : 180
};
var obj2 = {
name : "qwer",
height : 190
}
function printInfo({name, age} = {name:"", age:19}){
console.log(name, age);
}
function printInfo1({name = "", age = 0} = {}){
console.log(name, age);
}
printInfo(); // "" 10
printInfo(obj); // xtz 10
printInfo(obj2); // qwer undefined
printInfo1(); // "" 10
printInfo1(obj); // xtz 10
printInfo1(obj2); // qwer undefined
function baz(x, y, z){
}
function baz1(x, y, z = 1){
}
function baz2(x = 2, y = 3, z = 1){
}
console.log(baz.length); // 3
console.log(baz1.length); // 2
console.log(baz2.length); // 0
存在默认值的参数,不计算如length中
如果函数的最后一个参数是...为前缀的,那么它会将剩余的参数放到该参数的中,并作为一个数组
剩余参数必须放在函数形参的最后
function foo(m, n, ...args){
console.log(m, n);
console.log(args);
console.log(arguments);
}
foo(20, 30, 40, 50, 60);
// 20 30
// [40, 50, 60]
// [20, 30, 40, 50, 60]
arguments的区别
arguments对象包含传给函数的所有实参arguments对象不是一个真正的数组(不包含一些数组的操作),而剩余参数是一个真正的数组可以进行数组的所有操作arguments对象是早期JS为了方便所有参数提供的一个数据解构,而剩余参数是为了替代arguments而设置的箭头函数没有显式原型,所以不能作为构造函数,使用new来创建对象
var bar = () => {
console.log(this);
console.log(arguments);
}
const b = new bar(); // Error
箭头函数的
this和arguments是查找使用父级作用域的
const names = ["x", "y", "z"];
const name = "123";
function foo(x, y, z){
console.log(x, y, z);
}
foo(...names); // x y z
foo(...name); // 1 2 3
const newNames = [...names, ...name]; // x y z 1 2 3
const info = {
name : "x",
age : 20
};
const obj = {...info, address : "BJ", ...names};
console.log(obj);
...展开运算符实际上进行的是一个浅拷贝
const num1 = 100; // 十进制 100
const num2 = 0b100; // 二进制 4
const num3 = 0o100; // 八进制 64
const num4 = 0x100; // 十六进制 256
// 大数值
const num5 = 10_000_000_000_000_000; // 使用_做分隔符,方便理解大数字的位数
ES6中新增的基本数据类型,翻译为符号
为什么需要Symbol
mixin,如果出现同名属性,必然有一个会被覆盖掉为了解决冲突问题,Symbol可以用来生成一个独一无二的值
const s1 = Symbol()
const s2 = Symbol()
console.log(s1 === s2); // false
// ES10 之后,可以添加Symbol描述
const s3 = Symbol("aa");
console.log(s3.description);
const obj = {
[s1] : "abv",
[s2] : "qwer"
};
obj[s3] = "asd";
const s4 = Symbol();
Object.defineProperty(obj, s4, {
// ...
});
console.log(obj[s1], obj[s2]);
// 不可通过 . 来获取属性
console.log(obj); // 空
// 需要通过下面两个函数的方式来获取所有Symbol的key
console.log(Object.getOwnPropertyNames(obj));
console.log(Object.getOwnPropertySymbols(obj));
// 创建相同的Symbol
const s11 = Symbol.for("aa");
const s22 = Symbol.for("aa");
console.log(s11 === s22);
console.log(Symbol.keyFor("aa"));
不可通过 . 来获取Symbol,因为 . 是把key变成字符串再查找
obj.s1 变成 obj["s1"]
使用Symbol作为key的属性名,在遍历/Object.key等方法是遍历不到这些Symbol值的
Set新增数据结构,可以用来保存数据,类似数组,但是元素不能重复
const s = new Set();
// 添加
s.add(10);
s.add(10);
s.add(20);
s.add(30);
s.add({}); // 字面量A
s.add({}); // 字面量B A、B地址不同 并不是同一个东西
console.log(s);
// 去重
const arr = [10, 10, 20, 30, 40];
const arrSet = new Set(arr);
const newArr = [...arrSet]; // 支持展开运算符
console.log(newArr);
// 常用方法 属性
console.log(arrSet.size); // Set内元素个数
arrSet.delete(10); // 删除Set内的元素
arrSet.has(10); // 判断Set内是否存在某个元素
arrSet.clear(); // 清除Set内所有元素
// 遍历Set
arrSet.forEach(item => {
console.log(item);
})
for(const item of arrSet){
console.log(item);
}
WeakSet类似*Set*,也是内部元素不能重复的数据结构
const weakSet = new WeakSet();
// 不能存放基本数据类型
weakSet.add(10); // TypeError
weakSet.add(value); // 返回weakSet本身
weakSet.delete(value); // 返回boolean类型
weakSet.has(value); // 返回boolean类型
WeakSet不能遍历,因为WeakSet只是对对象的弱引用,如果遍历获取其中的元素,有可能造成对象的不正常销毁
WeakSet在开发中很少用到
WeakSet的应用
const personSet = new WeakSet();
class Person(){
constructor() {
personSet.add(this);
}
running() {
if(!personSet.has(this)){
throw new Error("不能通过非构造方法创建出来的对象调用running方法");
}
console.log("running ", this);
}
}
const p = new Person();
p.running();
p.running.call({name, "why"});
使用WeakSet是因为当Person对象需要被销毁时,不会因为Set的强引用而不会被GC
Map新增数据结构,可以用来保存映射关系
对象映射关系只能用字符串和Symbol作为属性名,如果使用对象,会自动将对象转换成字符串来作为key
// 对象字面量只能用字符串和Symbol作为key的测试用例
const obj1 = {};
const obj2 = {};
const info = {
[obj1] : "a",
[obj2] : "b";
};
console.log(info); // { [Object object] : "b" }
const map = new Map();
map.set(obj1, "a"); // object作为key,插入到map中
map.set(obj2, "b");
console.log(map);
// 常用方法
const map2 = new Map([[obj1, "a"], [obj2, "b"]]); // 用数组作为构造参数
map2.set("key", "value");
console.log(map2.get("key"));
console.log(map2.has("key"));
console.log(map2.delete("key"));
map2.clear();
// 遍历map
map2.forEach((value, key) => {
console.log(value, " ", key);
});
for(const item of map2) {
// 这里item是一个数组 index 0 是key index 1 是value
console.log(item[0], item[1]);
}
// 直接对数组进行解构
for(const [key, value] of map2) {
console.log(key, " ", value);
}
WeakMap也是键值对容器,与Map的区别
const obj1 = {};
const obj2 = {};
const weakMap = new WeakMap();
weakMap.set(1, "b"); // TypeError
weakMap.set(obj1, "a");
console.log(weakMap.get(obj1));
console.log(weakMap.has(obj1));
console.log(weakMap.delete(obj1));
不支持forEach,也不能用其他方式遍历
WeakMap的应用
vue3响应式原理
const obj1 = {
name : "x",
age : 10
};
const obj2 = {
name : "z",
height : 190,
address : "BJ"
}
function obj1NameFn1(){
console.log("Obj1 Name Fn1 被执行");
}
function obj1NameFn2(){
console.log("Obj1 Name Fn2 被执行");
}
function obj2HeightFn1(){
console.log("Obj2 height Fn1 被执行");
}
function obj2HeightFn2(){
console.log("Obj2 height Fn2 被执行");
}
const weakMap = new WeakMap();
const obj1Map = new Map();
const obj2Map = new Map();
obj1Map.set("name", [obj1NameFn1, obj1NameFn2]);
weakMap.set(obj1, obj1Map);
obj2Map.set("height", [obj2HeightFn1, obj2HeightFn2]);
weakMap.set(obj2, obj2Map);
// Proxy / Object.defineProperty 监听obj1或obj2的属性发生改变
obj1.name = "qwer";
// 监听到数据变化后
const targetMap = weakMap.get(obj1);
const fns = targetMap.get("name");
fns.forEach(item => item());
使用WeakMap是因为当obj对象需要被销毁时,不会因为Map的强引用而不会被GC
includes与indexOf的区别:对NaN的判断const names = ['a', 'b', 'c', NaN];
if(names.indexOf('a') !== -1){
// 就版本判断是否包含指定值
}
// Array.includes(searchElement : string, formIndex ?: number): boolean
if(names.includes('a')){
console.log("包含 a")
}
console.log(names.includes('a', 0)); // true
console.log(names.includes('a', 1)); // false
console.log(names.includes('a', 2)); // false
console.log(names.includes(NaN)); // true
console.log(names.indexOf(NaN)); // -1
fromIndex参数表示从第几个开始查找
indexOf无法找到NaN
const result = Math.pow(3, 3);
const result2 = 3 ** 3;
console.log(result, result2);
之前通过Object.keys获取一个对象所有的key,ES8提供Object.values来获取所有的value值
const obj = {
name : "x",
age : 10
}
console.log(Object.keys(obj)); // ['name', 'age']
console.log(Object.values(obj)); // ['x', 10]
// 用的少
console.log(Object.values(["q", "w", "r"])); // ['q', 'w', 'r']
console.log(Object.values("qwer")); // ['q', 'w', 'e', 'r']
通过Object.entries可以获取一个数组,数组中会存放可枚举属性的键值对数组
const obj = {
name : "x",
age : 10
}
console.log(Object.entries(obj)); // [ [ 'name', 'x' ], [ 'age', 10 ] ]
console.log(Object.entries(["a", "b", "c"])); // [ [ '0', 'a' ], [ '1', 'b' ], [ '2', 'c' ] ]
console.log(Object.entries("qwer")); // [ [ '0', 'q' ], [ '1', 'w' ], [ '2', 'e' ], [ '3', 'r' ] ]
本质就是一个二维数组,第一个维度是item,第二个维度是键值对
字符串填充
某些字符串我们需要对其进行前后的填充,去实现某种格式化的效果,ES8中增加了padStart和padEnd方法,分别是队字符串和首位进行填充的
const message = "Hello World";
// string.padStart(maxLength : number, fillString ?: string) : string
const newMessage1 = message.padStart(15);
console.log(newMessage1); // Hello World
const newMessage2 = message.padStart(15, "*");
console.log(newMessage2); // ****Hello World
const newMessage3 = message.padStart(15, "*").padEnd(20, "-");
console.log(newMessage3); // ****Hello World-----
案例
// 身份证号、银行卡号只显示最后四位
const cardNumber = "4532132468321312135433";
const lastFourCardNumber = cardNumber.slice(-4);
const finalCard = lastFourCardNumber.padStart(cardNumber.length, "*");
console.log(finaleCard); // ******************5433
支持函数调用额函数声明中参数列表最后多个逗号
为了有些人习惯、有些语言而添加
function foo(m, n,){ // ES8之前报错
}
foo(1, 2,);
Object.getOwnPropertyDescriptors获取对象的所有属性描述符
前面有写
flat()方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回
// Array.flat(depth? : 1) : (number | number[]) []
const nums = [1, 2, [3, [4, 5], 6], 7, 8, [9, 10]];
const newNums = nums.flat(); // 默认做一次降维
console.log(newNums);
const newNums2 = nums.flat(2);
console.log(newNums2);
flatMap()方法首先使用映射函数映射每个元素,然后将结果压缩成一个新数组
flatMap()是先进行map操作,在做flat操作flatMap()中的flat深度为1// Array.flatMap(callback : (this : undefined, value : number | number[] | number[][], index : number, array : (number | number[] | number[][])[]) => any, thisArg ?: undefined) : any[]
const nums = [1, 2, 3];
const newNum = nums.flatMap(item => {
return item * 2;
});
console.log(newNum); // [2, 4, 6]
const messages = ["Hello World", "Hello Java", "Hello Cpp"];
const words = messages.flatMap(item => {
return item.split(" ");
});
console.log(wrods); // [ "Hello", "World","Hello","Java","Hello","Cpp"]
map()转换为[["Hello", "World"],["Hello", "Java"],["Hello", "Cpp"]]flat()将二维数组转换成一维数组通过entries创建对象
const obj = {
name : "x",
age : 10
};
const entries = Object.entries(obj);
console.log(entries);
const newObj = {};
for(const entry of entries){
newObj[entry[0]] = entry[1];
}
console.log(newObj);
const newObj2 = Object.fromEntries(entries);
console.log(newObj2);
const queryString = "name=w&age=10&height=190";
const queryParams = new URLSearchParams(queryString);
const paramObj = Object.fromEntries(queryParams);
console.log(paramObj); // {"name": "w","age": "10","height": "190"}
去除前、后的空白字符
const message = " hello wworld ";
console.log(message.trim()); //
console.log(message.trimStart()); //
console.log(message.trimEnd()); //
早期JS不能正确的表示过大的数字,大于Number.MAX_SAFE_INTEGER的数值的表示可能是不正确的
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
ES11引入BigInt来表示大数字,就是在数字后加上n
const bigint = 90071992547409910n;
console.log(bigint);
bigint + 10; // TypeError: Cannot mix BigInt and other type
bigint + BigInt(10); // 90071992547409920n
const num = Number(bigint); // 不一定正确 不安全
空值合并操作、空值合并运算
// 空值合并运算 ??
let foo;
let foo1 = 0;
let bar1 = foo1 || "default value";
let bar = foo ?? "default value";
console.log(bar); // default value
console.log(bar1); // default value
针对bar1的值,我们目标肯定是0,但是由于
||运算符将0、false和空字符串认为是false,所以这里使用??是最好的选择
可选链
可选链是ES11新增的一个特性,主要作用是在代码中进行null和undefined判断时更加清晰和简介
const info = {
name : "x",
friend : {
name : "a",
friend : {
name : "q"
}
}
};
console.log(info.friend.friend.name);
// 为了代码健壮性 需要判断info中是否存在friend,info.friend中是否存在friend
if(info && info.friend && info.friend.friend){
console.log(info.friend.friend.name);
}
// 可选链的使用
console.log(info?.friend?.friend?.name);
注意代码的健壮性
获取全局对象的GlobalThis
// 获取某个环境下的全局对象
// 浏览器下正确,但是node环境下错误
console.log(window);
// node下
console.log(global);
let myGlobalObj = undefined;
if(window !== undefined){
myGlobalObj = window;
}
else{
myGlobalObj = global;
}
// ES11后
console.log(globalThis); // 浏览器中等于window,node中等于global
不用写复杂的
if...else去给myGlobalObj赋值,可以直接用globalThis替代
FinalizationRegistry对象可以让你在对象被垃圾回收时请求一个回调
FinalizationRegistry提供:当一个在注册表中注册的对象被回收时,请求在某个时间点上调用一个清理回调(清理回调有时被称为finalizer)register方法,注册任何你想要清理回调的对象,传入该对象和所含的值// FinalizationRegistry
const finalRegistry = new FinalizationRegistry((value) => {
console.log("注册对象被销毁", value);
});
let obj = { name: "w" };
let info = {};
let obj2 = new WeakRef(obj); // 弱引用
finalRegistry.register(obj, "obj_1");
finalRegistry.register(info, "info_1");
console.log(obj2); // WeakRef对象
console.log(obj2.deref()); // 获得弱引用对象
obj = null;
这个用浏览器测试比较方便,而且JS垃圾回收不是实时的可能得等一会
最后应该是输出obj_1和info_1
WeakRef就是弱引用,let obj3 = obj;是强引用,即使obj = null内存也不会被GC,因为obj3还在指向内存块,let obj2 = new WeakRef(obj);是弱引用,obj = null后内存块就会被GC
WeakRef.prototype.deref()函数,如果原指向对象没有被销毁,则返回原指向对象;如果原指向对象被销毁,则返回undefined(搭配前面的可选链进行操作即可)
逻辑赋值运算
// 1. ||= 逻辑或赋值运算
let message = undefined;
message = message || "default value";
message ||= "default value";
// 2. &&= 逻辑与赋值运算
let obj = {
name : "x",
foo : function() {
console.log("run");
return {};
}
};
obj = obj && obj.foo(); // 判断obj是否存在,存在就覆盖obj的值为obj.foo()
obj &&= obj.foo();
// 3. ??= 逻辑空赋值运算
let msg = "";
msg ??= "default vlaue";
msg = msg ?? "default value";
理解为
x = x + 1等价于x += 1即可
监听对象的属性被赋值或获取的操作,去根据这个值进行一些其他的操作(数据驱动框架中常用)
const obj = {
name: "x",
age: 10,
height: 190
};
Object.keys(obj).forEach(key => {
let value = obj[key];
Object.defineProperty(obj, key, {
get: function() {
console.log(`${key} get`);
return value;
},
set: function(newValue) {
value = newValue;
console.log(`${key} set`);
}
});
});
console.log(obj.name);
obj.age = 120;
console.log(obj.age);
可以使用属性描述符来监听属性的赋值和获取操作
Object.defineProperty的缺点
Object.defineProperty的初衷不是监听一个对象属性使用Proxy可以监听到对对象的13种操作
ES6新增Proxy类,如果我们希望监听一个对象的相关操作,我们需要先创建一个Proxy对象,之后该对象的所有操作,都通过Proxy对象来完成,代理对象可以监听我们想要对原对象进行哪些操作
new Proxy()对象,并且传入待处理对象和捕获器对象,可以称之为handlerlet p = new Proxy(target, handler)Proxy对象,因为我们需要在handler进行监听| 序号 | 捕获器对象 | 作用 | 对应操作 |
|---|---|---|---|
| 1 | getPrototypeOf(target) | 当读取被代理对象target的原型prototype时会触发该操作 | Object.getPrototypeOf()方法的捕捉器 |
| 2 | setPrototypeOf(target, prototype) | 给target设置prototype时触发 | Object.setPrototypeOf()方法的捕捉器 |
| 3 | isExtensible(target) | 判断target是否可扩展时触发 | Object.isExtensible()方法的捕捉器 |
| 4 | preventExtensions(target) | 设置target不可扩展时触发 | Object.preventExtensions()方法的捕捉器 |
| 5 | getOwnPropertyDescriptor(target, prop) | 获取target[prop]的属性描述时触发 | Object.getOwnPropertyDescriptor()方法的捕捉器 |
| 6 | defineProperty(target, property, descriptor) | 定义target的某个属性prop的属性描述descriptor时触发 | Object.defineProperty()方法的捕捉器 |
| 7 | has(target, prop) | 当判断target是否拥有属性prop时,触发 | in操作符的捕捉器 |
| 8 | get(target, property, receiver) | 读取target的属性property时触发 | 属性获取操作的捕捉器 |
| 9 | set(target, property, value, receiver) | 设置target的属性property为值value时触发 | 属性设置操作的捕捉器 |
| 10 | deleteProperty(target, property) | 删除target的属性property时触发 | delete操作符的捕捉器 |
| 11 | ownKeys(target) | 获取targeet的所有属性key s时触发 | Object.ownKeys()方法的捕捉器 |
| 12 | apply(target, thisArg, argumentsList) | 当目标target为函数,且被调用时触发 | 函数调用操作的捕捉器 |
| 13 | construct(target, argumentsList, newTarget) | 给target为构造函数的代理对象构造实例时触发 | new操作符的捕捉器 |
get中的receiver为Proxy或继承Proxy的对象
set中的receiver是最初被调用的对象(通常是Proxy本身)
const obj = {
name : 'x',
age : 10
};
let objProxy = new Proxy(obj, {
get : function(target, key, receiver){
// target 是 obj,key 是 取值的键名, receiver 是 代理对象 objProxy
console.log(`${key} get`);
return target[key];
},
set : function(target, key, newValue, receiver){
console.log(`${key} set ${newValue}`);
target[key] = newValue;
},
has : function(target, key) {
console.log(`${key} in Obj?`);
return key in target;
},
deleteProperty : function(target, key){
console.log(`delete ${key} in target`);
delete target[key];
}
});
objProxy.name = "y";
objProxy.age = 15;
console.log(objProxy.name);
console.log(objProxy.age);
console.log("name" in objProxy);
delete objProxy.age;
function foo(){
}
// foo() // 直接调用
// foo.apply({}, []) // 通过apply调用
// new foo(); // new出来调用
const fooProxy = new Proxy(foo, {
apply : function(target, thisArg, argumentsList) {
// target就是函数对象 thisArg是调用函数的对象
console.log(target, thisArg, argumentsList);
target.apply(thisArg, argumentsList);
},
construct : function(target, argList, newTarget){
// 通过new调用的监听
console.log(target, " new function");
return new target(...argList);
}
});
fooProxy.apply({}, ["qw", "er"]); // [Function: foo] {} [ 'qw', 'er' ]
new fooProxy();
Reflect是ES6新增的API,他是一个对象(不用new,直接使用),字面意思是反射
Reflect提供很多操作JS对象的方法,有点像Object中操作对象的方法
Reflect.getPrototypeOf(target)类似Object.getPrototypeOf()
Reflect.defineProperty(target, propertyKey, attributes类似Object.defineProperty()
Reflect
Reflect,将这些操作放到Reflect中一句话概括就是
Object功能太多,新增Reflect分担对象操作功能
| 序号 | 常见方法 | 对应功能 |
|---|---|---|
| 1 | Reflect.getPrototypeOf(target) | 类似Object.getPrototype() |
| 2 | Reflect.serPrototypeOf(target, prototype) | 设置对象原型的函数,返回boolean表示是否更新成功 |
| 3 | Reflect.isExtensible(target) | 类似Object.isExtensible() |
| 4 | Reflect.preventExtensions(target) | 类似Object.preventExtensions(),返回boolean |
| 5 | Reflect.getOwnPropertyDescriptor(target, propertyKey) | 类似Object.getOwnPropertyDescriptor(),如果对象存在,返回对应属性描述符,不存在返回undefined |
| 6 | Reflect.definePeoperty(target, propertyKey, attributes) | 类似Object.definePeoperty(),如果设置成功返回true |
| 7 | Reflect.ownKeys(target) | 类似Object.ownKeys(),不受enumerable影响 |
| 8 | Reflect.has(target, propertyKey) | 判断一个对象是否存在某个属性,和in运算符的功能完全相同 |
| 9 | Reflect.get(target, propertyKey[, receiver]) | 获取对象上某个属性的值,类似target[name] |
| 10 | Reflect.set(target, propertyKey, value[, receiver]) | 将值分配给属性的函数,返回一个Boolean表示是否设置成功 |
| 11 | Reflect.deleteProperty(target, propertyKey) | 作为函数的delete操作符,相当于执行delete target[name] |
| 12 | Reflect.apply(target, thisArgument, argumentsList) | 对一个函数进行调用操作,同时可以传入一个数组作为调用参数,和Function.prototype.apply()功能类似 |
| 13 | Reflect.construct(target,argumentsList[, newTarget]) | 对构造函数进行new操作,相当于执行new target(...args) |
const obj = {
name: 'x',
age: 10
};
let objProxy = new Proxy(obj, {
get: function(target, key, receiver) {
// target 是 obj,key 是 取值的键名, receiver 是 代理对象 objProxy
console.log(`${key} get`);
return Reflect.get(target, key, receiver);
},
set: function(target, key, newValue, receiver) {
console.log(`${key} set ${newValue}`);
Reflect.set(target, key, newValue, receiver);
},
has: function(target, key) {
console.log(`${key} in Obj?`);
return Reflect.has(target, key);
},
deleteProperty: function(target, key) {
console.log(`delete ${key} in target`);
Reflect.deleteProperty(target, key);
}
});
objProxy.name = "y";
objProxy.age = 15;
console.log(objProxy.name);
console.log(objProxy.age);
console.log("name" in objProxy);
delete objProxy.age;
不直接对目标对象进行操作,而是通过
Reflect对象对目标对象进行操作
相对于对对象的直接操作,使用Reflect的好处就是Reflect绝大多数函数都会返回Boolean提醒是否操作成功
tip:Object.freeze(target)冻结对象后不可设置值,这时Reflect的功能就体现出来了
const obj = {
_name : "x",
get name(){
return this._name;
},
set name(value){
this._name = value;
}
}
const objProxy = new Proxy(obj, {
get : function(target, key) {
console.log("get");
return Reflect.get(target, key);
},
set : function(target, key, value){
console.log("set");
Reflect.set(target, key, value);
}
});
objProxy.name = "y";
console.log(objProxy.name);
运行结果:一次set、一次get,
this._name = value没有走代理而是直接设置了obj
如果希望对obj的所有操作都经过objProxy,这个结果就是错误的
receiver就是创建出来的代理对象
receiver === objProxy
const obj = {
_name : "x",
get name(){
return this._name;
},
set name(value){
this._name = value;
}
}
const objProxy = new Proxy(obj, {
get : function(target, key, receiver) {
console.log("get");
return Reflect.get(target, key, receiver);
},
set : function(target, key, value, receiver){
console.log("set");
Reflect.set(target, key, value, receiver);
}
});
objProxy.name = "y";
console.log(objProxy.name);
Reflect.get(target, key, receiver)会让this._name里的this变成receiver对象,而不再是obj对象
// 使用setTimeout模拟网络请求
function requestData(url, succeessCallback, errorCallback){
setTimeout(() => {
// 获得请求结果
if(url === "qwer"){
// 请求成功
let name = "result";
succeessCallback(name);
}else{
// 请求失败
let error = "404";
errorCallback(error);
}
}, 3000);
}
requestData("qwer", (success) => {
console.log(success);
}, (error) => {
console.log(error);
});
如果是自己封装的
requestData必须提前设计好回调函数,并且使用好
如果是别人封装的requestData必须查看源码才知道如何使用回调函数
更好的方案Promise承诺(规范好了所有代码的编写逻辑)
Promise
Promise是一个类,当我们需要给调用者一个承诺:待会会给你回调数据时,可以创建一个Promise的对象new创建Promise对象时,我们需要传入一个回调函数,称之为executorresolve,rejectresolve回调函数时,会执行Promise对象的then方法传入的回调函数reject回调函数时,会执行Promise对象的catch方法传入的回调函数function requestData(url) {
return new Promise((resolve, reject) => {
setTimeout(() => {
// 获得请求结果
if (url === "qwer") {
// 请求成功
let name = "result";
resolve(name);
} else {
// 请求失败
let error = "404";
reject(error);
}
}, 1000);
});
}
let promise = requestData("qwer");
promise.then((res) => {
console.log(`request success ${res}`);
}).catch((err) => {
console.log(`request failed ${err}`);
});
// 等价操作
// promise.then((res) => {
// console.log(`request success ${res}`);
// }, (err) => {
// console.log(`request failed ${err}`);
// });
因为
Promise构造时的传入的回调函数会立即执行,所以异步逻辑直接写入到回调函数中即可
Promise.then支持传入两个回调函数,第一个表示resolve,第二个表示reject,可以省略编写catch
综上可以发现,Promise其实就是JS官方提供的一套回调函数的标准,看到Promise就知道函数应该使用
Promise使用的过程,可以分为三个状态
reslove也没有rejectreslove()reject()new Promise((resolve, reject) => {
console.log("----");
resolve("1111");
console.log("++++");
reject("2222");
}).then((res) => {
console.log(res);
}, (err) => {
console.log(err);
});
强烈建议运行代码查看结果
可以发现 先执行了exector部分的代码,再执行了resolve()代码,又因为整个Promise的状态已经被确定为fulfiled,所以不会执行reject()的方法
resolve()函数的参数
Pedning转到fulfilled状态)Promise对象(当前Promise的状态会由传入的Promise的状态来决定,看下面代码 例子1)then方法,那么会执行该then方法,并且由then方法决定后续状态(下面代码 例子2)// 例子 1
const promise1 = new Promise((resolve, reject) => {});
new Promise((reslove, reject) => {
reslove(promise1);
}).then((res) => {
console.log("reslove");
console.log(res);
}, (err) => {
console.log("reject");
console.log(err);
})
// 例子 2
new Promise((resolve, reject) => {
const obj = {
then : function(resolve, reject){
reject("123");
}
}
resolve(obj);
}).then((res) => {
console.log("resolve", res);
}, (err) => {
console.log("reject", err);
})
强烈建议运行代码查看结果
对于例子1,因为promise1对象处于Pedning状态,所以影响了新new的Promise不执行reslove
对于例子2,因为executor部分的obj存在then方法,而then方法执行了reject(),从而导致new的Promise变成了rejected状态,而不会执行后续的resolve()方法
Promise的状态一旦决定,就无法更改
Promise可以多次调用then方法,而所有绑定的then方法在执行resolve时都会被执行const promise = new Promise((resolve, reject) => {
resolve("--");
});
promise.then(res => {
console.log("1");
});
promise.then(res => {
console.log("2");
});
promise.then(res => {
console.log("3");
});
then传入的回调函数方法是可以有返回值的
Promise的resolve的形参
then都是调用因返回普通值而新建的Promise对象的thenPromise对象,可参见前面resolve()函数的参数这一栏的情况无论如何,
then传入的回调函数的返回值都会出发new Promise,并把返回值传入到新的Promise的resolve的形参中
const test = Promise.resolve({name:"x"});
// 等价于
// const promise = new Promise((resolve, reject) => {
// resolve({name : "x"});
// })
// 样例 1
// 返回值为普通值
const promise = new Promise((resolve, reject) => {
resolve("promise 1 ");
});
promise.then((res) => {
console.log(res);
return "aaa";
}).then(res => {
console.log("new promise 2 : ", res);
return "bbb";
}).then(res => {
console.log("new promise 3 : ", res);
});
// 样例2
// 返回值为Promise
const promise = new Promise((resolve, reject) => {
resolve("promise 1 ");
});
promise.then((res) => {
console.log(res);
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("return Promise");
}, 1000)
});
}).then(res => {
console.log(res);
});
finally方法:无论Promise处于fulfilled还是rejected状态,最终都会执行的代码const promise = new Promise((resolve, reject) => {
reject("err");
});
promise.then(res => {
console.log(res);
}, err => {
console.log(err);
}).finally(() => {
console.log("finally");
})
Promise.all等待所有Promise对象都进入fulfilled状态时触发,返回值是一个数组,数组的item就是resolve函数的参数
如果Promise数组中存在某个Promise触发了reject,则会导致Promise.all立即停止监听,并返回该reject的参数
// 全部都执行 resolve
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 1000);
});
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(2);
}, 2000);
});
const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(3);
}, 3000);
});
Promise.all([p1, p2, p3]).then(res => {
console.log(res);
}, (err) => {
console.log(err);
});
// 某个Promise触发了reject
const p4 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 1000);
});
const p5 = new Promise((resolve, reject) => {
setTimeout(() => {
reject(2);
}, 2000);
});
const p6 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(3);
}, 3000);
});
Promise.all([p4, p5, p6]).then(res => {
console.log(res);
}, (err) => {
console.log(err);
});
Promise.allSettled等待所有的Promise都有结果的时候才会触发
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 1000);
});
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject(2);
}, 2000);
});
const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(3);
}, 3000);
});
Promise.allSettled([p1, p2, p3]).then(res => {
console.log("resolve ", res);
}, (err) => {
console.log("reject", err);
});
// 输出
// resolve [
// { status: 'fulfilled', value: 1 },
// { status: 'rejected', reason: 2 },
// { status: 'fulfilled', value: 3 }
// ]
Promise.race只要有一个Promise变成fulfilled或者rejected状态,那么就结束
如果Promise中有一个触发了reject,就会直接拿到reject的结果并结束
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 1000);
});
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject(2);
}, 2000);
});
const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(3);
}, 3000);
});
Promise.race([p1, p2, p3]).then(res => {
console.log("resolve ", res);
}, (err) => {
console.log("reject", err);
});
// 输出
// resolve 1
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 1000);
});
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject(2);
}, 500);
});
const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(3);
}, 3000);
});
Promise.race([p1, p2, p3]).then(res => {
console.log("resolve ", res);
}, (err) => {
console.log("reject", err);
});
// 输出
// reject 2
ES12的方法
Promise.any会等到一个fulfilled状态,才会决定新的Promise的状态,如果所有的Promise都是reject的,那么也会等到所有的Promise都变成rejected状态
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 1000);
});
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject(2);
}, 500);
});
const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(3);
}, 3000);
});
Promise.any([p1, p2, p3]).then(res => {
console.log("resolve ", res);
}, (err) => {
console.log("reject", err);
});
// 输出
// resolve 1
生成器可以处理异步代码
迭代器可以使用户在容器对象上遍历对象,使用该接口无需关心对象的内部实现细节
JavaScript中,迭代器也是一个具体对象,这个对象需要符合迭代器协议
迭代器协议定义了产生一系列值的标准方式,在js中就是实现特定的
next方法
next方法有一些要求
done(boolean)
value:该值可选,如果done = true,则value作为迭代结束后默认返回值当所有元素都访问完了,最后再访问的时候
done = true,其他时候done = false
// 迭代器对象基本形状
// const iterator = {
// next : function(){
// return {
// done : true,
// value : 123
// }
// }
// };
const names = ["q", "w", "e", "r"];
let index = 0;
const namesIterator = {
next : function() {
if(index < names.length){
return {
done : false,
value : names[index++]
}
}
else{
return {
done : true,
value : undefined
}
}
}
}
console.log(namesIterator.next());
console.log(namesIterator.next());
console.log(namesIterator.next());
console.log(namesIterator.next());
console.log(namesIterator.next());
console.log(namesIterator.next());
// 输出
// { done: false, value: 'q' }
// { done: false, value: 'w' }
// { done: false, value: 'e' }
// { done: false, value: 'r' }
// { done: true, value: undefined }
// { done: true, value: undefined }
function createArrayIterator(arr){
let index = 0;
return {
next : function(){
if(index < arr.length){
return { done : false, value : arr[index++]};
}
else{
return { done : true, value : undefined};
}
}
}
}
const iteratorObj = {
names: ["q", "w", "e"],
[Symbol.iterator]: function() {
let index = 0;
return {
next: () => { // 匿名函数
if (index < this.names.length) {
return { done: false, value: this.names[index++] };
} else {
return { done: true, value: undefined };
}
}
}
}
};
const iterator = iteratorObj[Symbol.iterator]();
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
const iterator1 = iteratorObj[Symbol.iterator]();
console.log(iterator1.next());
console.log(iterator1.next());
console.log(iterator1.next());
console.log(iterator1.next());
const iterator2 = iteratorObj[Symbol.iterator]();
console.log(iterator2.next());
console.log(iterator2.next());
for (let item of iteratorObj) {
console.log(item);
}
// 错误例子
const errorObj = {
names: ["q", "w", "e"],
[Symbol.iterator]: function() {
let index = 0;
return {
next: function() { // 匿名函数
if (index < this.names.length) {
return { done: false, value: this.names[index++] };
} else {
return { done: true, value: undefined };
}
}
}
}
};
for (let item of errorObj) {
console.log(item);
}
[Symbol.iterator]中的next使用的是箭头函数,如果next绑定的是function,那么this指向的就是[Symbol.iterator]返回的对象,而不是iteratorObj对象,而只有iteratorObj对象才有names属性this,而是使用上层作用域作为this,而上层作用域就是iteratorObjthis指向的是{next: function() {if (index < this.names.length) {return { done: false, value: this.names[index++] };} else {return { done: true, value: undefined };}}对象for...of...通过迭代器判断返回值的done是否为true来决定是否停止遍历平时创建的很多原生对象已经实现了可迭代协议,会生成一个迭代器对象:String、Array、Map、Set、arguments对象、NodeList集合
const names = [1, 2, 3, 4, 5];
console.log(names[Symbol.iterator]);
console.log(names[Symbol.iterator]().next());
const set = new Set();
set.add(1);
set.add(2);
set.add(3);
console.log(set[Symbol.iterator]);
console.log(set[Symbol.iterator]().next());
for...of...、展开语法(Spread syntax)、yield*、解构赋值(Destructuring assignment)new Map([Iterable])、new WeakMap([iterable])、new Set([iterable])、new WeakSet([iterable])Promise.all(iterable)、Promise.race(iterable)、Array.from(ietrable)const iteratorObj = {
names: ["q", "w", "e"],
[Symbol.iterator]: function() {
let index = 0;
return {
next: () => { // 匿名函数
if (index < this.names.length) {
return { done: false, value: this.names[index++] };
} else {
return { done: true, value: undefined };
}
}
}
}
};
// 展开语法
const indexs = [1, 2, 3, 4, 5];
const newIndexs = [...indexs, ...iteratorObj];
console.log(newIndexs); // [1, 2, 3, 4, 5, 'q', 'w', 'e']
// 结构语法
const [index1, index2, index3] = iteratorObj;
console.log(index1, index2, index3); // ['q', 'w', 'e']
// 创建一些对象
const set = new Set(iteratorObj); // 可以通过可迭代对象创建Set
console.log(set); // Set(3) { 'q', 'w', 'e' }
const array = Array.from(iteratorObj);
console.log(array); // [ 'q', 'w', 'e' ]
class ClassRoom {
constructor(address, name, students) {
this.address = address;
this.name = name;
this.students = students;
}
entry(newStudent) {
this.students.push(newStudent);
}
[Symbol.iterator]() {
let index = 0;
return {
next: () => {
if (index < this.students.length) {
return { done: false, value: this.students[index++] };
} else {
return { done: true, value: undefined };
}
},
// 监听迭代器终止
return : () => {
console.log("迭代器终止");
return { done : true, value : undefined };
}
}
}
};
const c1 = new ClassRoom("", "", [1, 2, 3]);
for (let s of c1) {
console.log(s);
}
for (let s of c1) {
if (s == 2) {
break;
}
console.log(s);
}
可以通过添加
return属性监听迭代器的迭代终止,注意返回值
比较特殊的迭代器
ES6中新增的一种函数控制、使用的方方案,它可以让我们更加灵活的控制函数什么时候继续执行、暂停执行
return 虽然可以暂停函数执行,但后续代码无法继续执行
生成器对象是由生成器函数产生的
function的后面加一个符号:*yield关键字来控制函数的执行流程Generator(生成器)function* foo() {
console.log("start");
const v1 = 100;
console.log(v1);
const n1 = yield v1;
const v2 = 200;
console.log(v2, n1);
const n2 = yield v2;
const v3 = 300;
console.log(v3, n2);
const n3 = yield v3;
console.log("end", n3);
return "123";
}
foo(); // 直接执行foo,不会执行任何代码
const generator = foo();
console.log("---------");
// 开始执行第一段代码 看下图
console.log("---------", generator.next(666));
// 开始执行第二段代码
console.log("---------", generator.next(777));
// 开始执行第三段代码
console.log("---------", generator.next(888));
// 开始执行第四段代码
console.log("---------", generator.next(999));
// 运行结果
// ---------
// start
// 100
// --------- { value: 100, done: false }
// 200 777
// --------- { value: 200, done: false }
// 300 888
// --------- { value: 300, done: false }
// end 999
// --------- { value: '123', done: true }
生成器函数以yiled为分界线,分段执行
通过上述代码的next的返回值可见,返回值的结构与迭代器的next结构相同,可见生成器就是特殊的迭代器
当生成器函数遇到yield的时候停止执行,done的值为false;当生成器函数遇到return的时候,done的值就变成true了
yield v1;表示返回生成器的值为v1的值
const n1 = yield v1;表示用n1接受next传入参数的值
// return
const generator = foo();
console.log(generator.next(10));
console.log(generator.return(20));
console.log(generator.next(30));
console.log(generator.next(40));
使用return(20)相当于在对应给yield前面加上了return 20,不仅当前yield部分不执行,后续所有部分都不会执行
// throw
function* foo() {
console.log("start");
const v1 = 100;
try {
yield v1;
} catch (err) {
console.log("err", err);
}
const v2 = 200;
console.log("第二部分");
yield v2;
console.log("end");
}
const generator = foo();
console.log(generator.next(10));
console.log(generator.throw(20));
console.log(generator.next(30));
throw()抛出异常
代码运行可知,如果有try...catch捕获到了异常,代码仍然会继续执行
function* createArrayIterator(arr) {
for (const item of arr) {
yield item;
}
}
function* createArrayIterator2(arr) {
yield* arr;
}
const names = [1, 2, 3, 4];
const namesIterator = createArrayIterator(names);
console.log(namesIterator.next());
console.log(namesIterator.next());
console.log(namesIterator.next());
console.log(namesIterator.next());
console.log(namesIterator.next());
const namesIterator1 = createArrayIterator2(names);
console.log(namesIterator1.next());
console.log(namesIterator1.next());
console.log(namesIterator1.next());
console.log(namesIterator1.next());
console.log(namesIterator1.next());
createArrayIterator2就是createArrayIterator的简易写法,yield*后面必须跟上一个可迭代对象
class ClassRoom {
constructor(address, name, students) {
this.address = address;
this.name = name;
this.students = students;
}
entry(newStudent) {
this.students.push(newStudent);
}
[Symbol.iterator] = function*() {
yield* this.students;
}
};
const c1 = new ClassRoom("", "", [1, 2, 3]);
for (let s of c1) {
console.log(s);
}
通过生成器,简化迭代器的写法
模拟:通过 用户ID->用户信息->部门信息->其他信息,这种链式请求
function requestData(url){
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(url);
}, 1000);
})
}
// 方案1
requestData("test").then(res => {
requestData(res + "aa").then(res => {
requestData(res + "bb").then(res => {
console.log(res);
})
})
})
// 方案2
requestData("test").then(res=>{
return requestData(res + "aa")
}, () => {}).then(res => {
return requestData(res + "bb")
}, () => {}).then(res => {
console.log(res);
}, () => {});
// 方案3 Promise + generator
function* getData() {
let val1 = yield requestData("test");
let val2 = yield requestData(val1 + "aa");
let val3 = yield requestData(val2 + "bb");
console.log(val3);
}
const generator = getData();
// generator返回的是对象,它的value是Promise对象
generator.next().value.then(res => {
generator.next(res).value.then(res => {
generator.next(res).value.then(res => {
generator.next(res);
})
})
})
// 方案4 (方案3的升级版)
function execGeneractor(generatorFunc) {
const generactor = generatorFunc();
function exec(res) {
const result = generactor.next(res);
if (result.done === true) {
return result.value;
}
result.value.then(res => {
exec(res);
});
}
exec();
}
execGeneractor(getData);
// 方案5
async function getData(){
const res1 = await requestData("test");
const res2 = await requestData(res1 + "aa");
const res3 = await requestData(res2 + "bb");
console.log(res3);
}
getData();
async + await组合(Promise和生成器组合的语法糖)async关键字用于声明一个异步函数
async是asynchronous单词的缩写,异步、非同步sync是synchronous单词的缩写,同步,同时async function foo1(){
}
const foo2 = async() => {
}
class Foo{
async foo3(){
}
}
异步函数的写法
异步函数的代码在函数中没有特殊内容时,函数的执行流程跟普通函数是一样的
async function foo(){
console.log("1");
console.log("2");
console.log("3");
console.log("4");
}
foo(); // 1 2 3 4
异步函数的返回值一定是Promise(没有返回值也是Promise)
// 返回普通值
async function foo(){
console.log("start...");
console.log("mid code...");
console.log("end...");
// 没有返回值时 默认return undefined
return 123;
}
// 返回带then方法对象
async function foo1(){
console.log("start...");
console.log("mid code...");
console.log("end...");
// 没有返回值时 默认return undefined
return {
then : function(resolve, reject){
resolve("hhh");
}
};
}
// 返回Promise
async function foo2(){
console.log("start...");
console.log("mid code...");
console.log("end...");
// 没有返回值时 默认return undefined
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("www");
}, 1000);
});
}
const promise = foo();
promise.then(res => {
console.log("promise run code", res);
})
const promise1 = foo1();
promise1.then(res => {
console.log("promise1 run code", res);
})
const promise2 = foo2();
promise2.then(res => {
console.log("promise run resolve", res);
}, (err) => {
console.log("promise run err", err);
});
promise执行的时机是foo()函数return的时候
如果异步函数返回的是含有then方法的对象,跟Promise返回含有then方法对象一样(resolve()函数的参数<=搜索关键字)
如果异步函数返回值是Promise,会等待resolve或者reject执行完毕才会出发then
异步函数的异常,会被捕获为Promise的reject的值
async function foo2() {
console.log("foo2 start");
throw new Error("error message");
console.log("foo2 end");
}
foo2().catch(err => {
console.log(err);
});
console.log("other code...");
继续执行后续代码,打印
other code...
function foo1() {
console.log("foo2 start");
throw new Error("error message");
console.log("foo2 end");
}
foo1();
console.log("other code...");
普通函数遇到异常,直接整个中断不会执行最后的输出
other code...
async函数另外一个特殊之处,就是它可以在内部使用await关键字,而在普通函数中不可以使用
await if only valid in async function
一般而言await后面会跟着也给表达式,并且返回一个Promise
await 表达式(Promise)
function requestData(){
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("1234");
}, 10000)
});
}
// await跟上表达式
async function foo(){
let res = await requestData(); // 在调用resolve时给到res结果
console.log("-----", res);
console.log("-----");
let res = await requestData(); // 在调用resolve时给到res结果
console.log("+++++", res);
console.log("+++++");
}
foo();
await之后的代码会等待await有返回值之后才会执行
可以把await后面的代码理解为是在Promise里面then回调中执行的
// await跟上其他的值
async function foo1(){
const res1 = await 123;
console.log(res1);
const res2 = await {
then : function(resolve, reject) {
resolve(234);
}
}
console.log(res2);
}
foo1();
await后面跟普通的值会立即返回
如果返回对象中含有名为then的function,会执行then方法,并返回resolve的值
function requestData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject("1234");
}, 100)
});
}
async function foo2() {
const res2 = await requestData();
console.log(res2);
console.log("------------");
}
foo2().catch(err => {
console.log(err);
});
如果
await后面的Promise执行reject函数,那么整个foo2都会立即终止,并且触发异步函数的catch
async function bar(){
console.log(222);
return new Promise((resolve, reject) => {
resolve();
});
}
async function foo(){
console.log(111);
await bar();
console.log(333);
}
foo();
console.log(444);
// 111
// 222
// 444
// 333
function foo1(){
console.log(111);
bar().then(() => {
console.log(333);
}, () => {
console.log(333);
});
}
比较
foo()和foo1()理解await可以把await后面的代码理解为是在Promise里面then回调中执行的
JavaScript是单线程的,但是JavaScript的线程应该有自己的容器进程:浏览器或者Node
浏览器
JavaScript是在一个单独的线程中执行的
真正耗时的操作,实际上并不是JavaScript线程在执行
如果在执行JavaScript代码的过程中,需要异步操作(比如setTimeout),这个函数会被放入到调用栈中,执行会立即结束,并不会阻塞后续代码的的执行
蓝色为JavaScript线程
黄色为浏览器其他进程
绿色为任务队列
在执行任何一个宏任务之前,都需要保证微任务队列已经被清空
一般而言都是微任务先执行,在执行宏任务
main script中的代码优先执行(编写的顶层script代码),然后再判断执行宏任务/微任务
// setTimeout1
setTimeout(function (){
console.log("setTimeout1");
new Promise((resolve, reject) => {
resolve();
}).then(function() {
new Promise(function (resolve, reject){
resolve()
}).then(function() {
console.log("then4");
});
console.log("then2");
});
});
new Promise(function(resolve, reject) {
console.log("promise1");
resolve();
}).then(function() {
console.log("then1");
});
setTimeout( function() {
console.log("setTimeout2");
})
console.log(2);
queueMicrotask(() => {
console.log("queueMicrotask");
})
new Promise(function(resolve, reject){
resolve();
}).then(function() {
console.log("then3");
});
// promise1
// 2
// then1
// queueMicrotask
// then3
// setTimeout1
// then2
// then4
// setTimeout2
Promise的exector回调函数不会进入任务队列,而是直接执行
建议运行查看代码执行流程
| 序号 | 执行顺序 | 注释 |
|---|---|---|
| 1 | setTimeout回调函数加入到宏任务队列 |
默认设置为0s,但是不会立即执行回调函数内容,而是加入到宏任务队列,因为此时main script代码并未执行完 |
| 2 | console.log("promise1"); |
Promise的exector回调函数不会进入任务队列,而是直接执行 |
| 3 | promise1的resolve(),将promise1的then加入到微任务队列 |
代码顺序执行,而根据浏览器规定,Promise的then加入到微任务 |
| 4 | setTimeout回调函数加入到宏任务队列 |
与第一个setTimeout相同处理 |
| 5 | console.log(2) |
main script代码直接执行 |
| 6 | queueMicrotask将回调加入到微任务队列 |
浏览器规定queueMicrotask加入到微任务队列 |
| 7 | 执行promise的resolve,将该Promise的then添加到微任务 |
Promise的then添加到宏任务 |
| 8 | 先清空微任务队列,依次执行console.log("then1")、console.log("setTimeout2")、console.log("then3"); |
在执行任何一个宏任务之前,都需要保证微任务队列已经被清空 |
| 9 | 执行宏任务中第一个setTimeout,new一个新的Promise并将then加入到微任务 |
|
| 10 | 因为添加了新的微任务,此时微任务列表不为空,所以执行微任务新增Promise的then中的console.log("then2")以及new Promise并加入新任务到微任务队列 |
微任务不为空,时刻注意 |
| 11 | 因为微任务队列不为空,所以执行console.log("then4") |
|
| 12 | 微任务空,执行宏任务中最后一个setTimeout |
async function async1(){
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2(){
console.log("async2");
}
console.log("main script start");
setTimeout(function(){
console.log('setTimeout');
}, 0);
async1();
new Promise(function(resolve, reject) {
console.log("promise1");
resolve();
}).then(() => {
console.log("promise2");
}, () => {})
console.log("main script end");
/**
* main script start
* async1 start
* async2
* promise1
* main script end
* async1 end
* promise2
* setTimeout
*/
| 序号 | 执行顺序 | 注释 |
|---|---|---|
| 1 | console.log("main script start"); |
main script 优先执行 |
| 2 | setTimeout加入到宏任务队列 |
前面案例有讲,浏览器规则 |
| 3 | 执行async1函数,直接console.log("async1 start")然后将async2作为Promise的exector直接执行console.log("async2")并将后续代码作为Promise的then |
async函数运行规则前面有讲 |
| 4 | 直接执行Promise的exector——console.log("promise1")并将then加入到微任务队列 |
Promise的exector直接执行 |
| 5 | console.log("main script end") |
main script代码直接执行 |
| 6 | 根据微任务队列顺序执行console.log("async2 end")、console.log("async2 end") |
优先清空微任务 |
| 7 | 执行console.log('setTimeout') |
清空宏任务 |
Promise.resolve().then(() => {
console.log(0);
// return Promise.resolve(4);
// return 4;
return {
then : function(resolve, reject){
resolve(4);
}
}
}).then((res) => {
console.log(res);
});
Promise.resolve().then(() => {
console.log(1);
}).then(() => {
console.log(2);
}).then(() => {
console.log(3);
}).then(() => {
console.log(5);
}).then(() => {
console.log(6);
});
分别执行
Promise的三种return,比较差异
return {then : function() {}}的then方法会被推移到微任务的下一次轮询中执行
return Promise.resolve()因为return不是普通的值会往后推移一次,又因为Promise.resolve会再往后推移一次,所以累计往后推移两次
浏览器中的事件循环(event loop)是根据HTML5定义的规范来实现的,不同的浏览器可能会有不同的实现,而Node中是由libuv实现的
Node的架构图
libuv中主要维护EventLoop(事件循环)和worker threads(线程池)EventLoop负责调用系统的一些其他操作:文件IO、Network、child-processes等libuv是一个多平台的专注于异步IO的库,最初是为Node开发的,现在也用到Luvit、Julia、pyuv等地方
事件循环像是一个桥梁,连接着应用程序和JavaScript和系统调用之间的通道
完整的事件循环Tick分成很多阶段
setTimeout和setInterval的调度回调函数setImmediate()回调函数在这里执行socket.on('close', ...)Node的任务队列也区分宏任务、微任务
Node事件循环Tick的每个阶段执行任务都会按照优先清空微任务队列,再处理宏任务
但是Node的事件循环不只是 微任务队列和宏任务队列
所以在每次事件循环的tick中,会按照如下的顺序执行代码
function sum(num1, num2){
// 目标num1、num2是数字,对于其他数据类型希望抛出异常
if (typeof num1 !== "number" || typeof num2 !== "number"){
throw "parameters is error type";
}
return num1 + num2;
}
console.log(sum({}, true)); // 一次错误的函数调用
封装了工具函数或者其他函数库,想告诉外界封装函数在某些情况下出现了错误,并且想要调用者知道这个错误,就需要通过throw抛出错误信息
throw语句用于抛出一个用户自定义异常,当遇到throw语句时,当前函数的执行会被停止(throw后面的语句不会执行)
class MyError{
constructor(errorcode, errorMessage){
this.errorMessage = errorMessage;
this.errorcode = errorcode;
}
}
class MyError2 extends Error {
}
function foo(type){
console.log("start");
if(type === 0){
throw "param can't 0"; // 直接返回字符串
} else if (type === 1){
throw { errorcode : -1, errorMessage : "type 不能为 1"}; // 返回对象,包含更多信息
} else if (type === 2){
throw new MyError(-2, "type 不能为2"); // 返回指定类型对象
} else if (type === 3){
throw new Error("type不能为3"); // 使用系统提供的Error对象,打印信息更多(函数调用栈)
} else if (type === 4){
throw new TypeError("类型错误"); // 抛出Error的子类
}
console.log("end");
}
foo(0);
console.log("other code");
Error的一些子类
针对异常的两种处理方法
try...catch...捕获异常function foo() {
throw new Error("foo err");liu'lan
}
function bar() {
try {
foo();
} catch (err){
console.log(err);
} finally {
console.log("bar end");
}
}
catch参数中的err就是foo抛出的Error对象
finally不管是否发生异常,最后finally的代码一定会执行
一般来说一个文件就是一个模块
因为JavaScript是在ES6(2015年)才支持模块化,所以在此之前有很多其他的社区规范:AMD、CMD、CommonJS等
CommonJS用的还是很多,AMD和CMD现在用的少,其他的规范就很少使用了
CommonJS是一个规范,最初叫ServerJS,后来为了体现它的广泛性改名CommonJS,简称CJS
Node中对CommonJS进行了支持和实现
exports和module.exports可以负责对模块中的内容进行导出
require函数可以导入其他模块(自定义模块、系统模块、第三方库模块)中的内容
// run1.js
const name = "x";
const age = 19;
function sum(num1, num2){
return num1 + num2;
}
// 1. module.export
module.exports = {
name,
age,
sum,
aaa : "aaa"
}
module是run.js模块本身的对象,exports也是一个对象
// run2.js
const {name, age, sum, aaa} = require("./run1.js");
console.log(name); // x
console.log(age); // 19
console.log(sum(1, 2)); // 3
console.log(aaa); // aaa
module.exports和require(file)指向的同一块内存区域
// run1.js
const name = "x";
const age = 19;
function sum(num1, num2){
return num1 + num2;
}
// 2.exports
exports.name = name;
exports.age = age;
exports.sum = sum;
exports.aaa = "aaa";
// Error
exports = {
name,
age,
sum
}
CommondJS底层中是先
module.exports = {}然后exports = module.exports
最后导出出去的肯定是module.exports,所以重新给exports赋值不会影响到module.exports从而导致导出失败
require(XX))
require("path"))require("./abc.js"))node_modules查找loaded,ture表示已经加载,false表示未加载暂停 跳过 工作暂时不涉及 为时间紧张暂不学习
JSON是非常重要的数据格式,轻量级资料交换格式
JSON使用场景
JSON的顶层支持三种类型的值
123
"123"
null
true
{
"name" : "x",
"frien" : {
"name" : "y"
},
"hobbies" : ["1", "2"]
}
[
"abc",
1234,
{
"name" : "x"
}
]
JSON注释不能写
最后一个item后面不能加上逗号
const obj = {
name : "x",
age : 10,
hobbies : ["1", "2"]
}
// 将obj转成JSON格式的字符串
const objString = JSON.stringify(obj);
console.log(objString);
// JSON格式转成对象
const info = JSON.parse(objString);
console.log(info);
const obj = {
name : "x",
age : 10,
hobbies : ["1", "2"]
}
// JSON.stringify(value : any, replacer ?: (this : any, key : string, value : any) => any, splace ? : string | number) : string
// 直接转换
const objString = JSON.stringify(obj);
console.log(objString);
// stringify的第二个参数 replacer
// replacer传入数组 设定哪些key是需要转换的
const objString2= JSON.stringify(obj, ['name', 'age']);
console.log(objString2);
// 传入回调函数 遍历整个kv对,对数据进行处理
const objString3 = JSON.stringify(obj, (key, value) => {
if(key === "age"){
value += 1;
}
return value;
});
console.log(objString3);
// stringify的第三个参数用于美化,第二个参数设null表示使用默认的
// 传入数字,表示缩进空格数目
const jsonString4 = JSON.stringify(obj, null, 2);
console.log(jsonString4)
const jsonString5 = JSON.stringify(obj, null, 4);
console.log(jsonString5)
// 传入字符,表示缩进不使用空格,而是指定字符串
const jsonString6 = JSON.stringify(obj, null, ".");
console.log(jsonString6)
JSON.stringify方法的一些解释和用法
const obj = {
name : "x",
age : 10,
hobbies : ["1", "2"],
toJSON : function() {
return "myself JSON String";
}
}
const objString = JSON.stringify(obj);
console.log(objString);
const objString3 = JSON.stringify(obj, (key, value) => {
if(key === "age"){
value += 1;
}
return value;
});
console.log(objString3);
当对象中存在
toJSON方法时,JSON.stringify会直接使用toJSON的返回值
const objString = '{"name":"x","age":10,"hobbies":["1","2"]}';
const obj = JSON.parse(objString);
// JSON.parse(text : string, reviver : (this : any, key : string, value : any) => any) : any
// 第二个参数 reviver,对每个key、value进行操作
const obj2 = JSON.parse(objString, (key, value) => {
if(key === "age"){
value += 1;
}
return value;
});
console.log(obj2);
就是利用JSON.stringify和JSON.parse做对象转换
x = y)x = [...y])// 简单的深拷贝
let s1 = Symbol();
const boj = {
name : "x",
foo : function() {
return 1;
}
[s1] : "qwe",
}
boj.inner = obj; // 某些情况需要自己引用自己
const info = JSON.parse(JSON.stringify(boj));
利用JSON深拷贝对象时,对函数无法进行处理
如果Symbol作为key,也无法处理
不支持循环引用
Symbol的key进行处理function isObject(value){
const valueType = typeof value;
return (value !== null) && (valueType === "object" || valueType === "function");
}
// 因为deepClone可能在多个地方调用,所以将noteObjs声明为局部变量
// 又因为Map是强引用会影响对象的销毁,所以需要使用WeakMap弱引用
function deepClone(originValue, noteObjs = new WeakMap()){
// 判断是否是set类型 这里写的是浅拷贝 一般而言够用
if (originValue instanceof Set){
return new Set([...originValue]);
}
// 判断是否是map类型 这里写的是浅拷贝 一般而言够用
if (originValue instanceof Map){
return new Map([...originValue]);
}
// 一般而言函数可以复用 直接返回就行
if(typeof originValue === "function"){
return originValue;
}
// 普通类型直接return 普通类型是值拷贝
if(!isObject(originValue)){
return originValue;
}
if(noteObjs.has(originValue)){
return noteObjs.get(originValue);
}
// 不做判断,数组直接赋值给对象是错误的效果
// const newObj = {};
const newObj = Array.isArray(originValue) ? [] : {};
noteObjs.set(originValue, newObj); // 记录一下 表示该对象本次被处理了 防止对象中循环引用
for(const key in originValue){
newObj[key] = deepClone(originValue[key], noteObjs); // 递归拷贝复制
}
// 对Symbol的key做特殊处理,因为Symbol作为key时遍历不到
const symbolKeys = Object.getOwnPropertySymbols(originValue);
for(const sKey of symbolKeys){
// 一般而言Symbol是为了防止对象内部key的冲突,不同对象倒不影响
// const newsKey = Symbol(sKey.description);
// newObj[newsKey] = deepClone(originValue[sKey]);
newObj[sKey] = deepClone(originValue[sKey], noteObjs);
}
return newObj;
}
const s1 = Symbol("aaa");
const s2 = Symbol("bbb");
const obj = {
name : "x",
age : 10,
friend : {
name : "y",
age : 11,
address : {
city : "BJ"
}
},
hobbies : [1, 2, 3],
foo : function(){
console.log("qwer");
},
[s1] : "aaa",
s2key : s2,
set : new Set([1, 2, 3, 4]),
map : new Map([["x", 1], ["y", 2]])
}
obj.inner = obj; // 自己引用自己
const newObj = deepClone(obj);
console.log(newObj);