迷惑的知识点:
作用域
函数、闭包
面向对象
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(){
message = "Hello Bar";
foo();
}
bar(); // 输出 Hello Global
GlobalObject对象foo的父级作用域(parent scope)就是GlobalObject
bar的父级作用域(parent scope)也是GlobalObject
赋值。。。
function foo(){
m = 100;
}
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或者其他可以记录的类型)
不管什么语言,在代码执行的过程中都需要分配内存,不同的是某些语言需要手动管理内存,某些编程语言可以帮助管理内存
一般而言,内存管理存在如下的生命周期
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();
函数第一公民 可以作为形式参数和返回值
副作用:在执行一个函数时,除了返回函数值以外,对调函数产生了附加的影响,比如修改了全局变量、修改参数或者改变外部的存储
副作用往往是产生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为truewritable为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()方法