Web前端最全JavaScript进阶知识汇总~,字节前端高工面试

react和vue的比较

相同
1)vitual dom
2)组件化
3)props,单一数据流

不同点
1)react是jsx和模板;(jsx可以进行更多的js逻辑和操作)
2)状态管理(react)
3)对象属性(vue)
4)vue:view——medol之间双向绑定
5)vue:组件之间的通信(props,callback,emit)

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

function Fun() {}
// undefined
fun = new Fun();
// Fun {}
fun.__proto__ == Fun.prototype
// true    对象的__proto__ 指向了构造函数的prototype

  1. 调用对象/函数属性的时候,js引擎会沿着 __ proto __ 的顺序一直往上方查找,找到window.Object.prototype 为止,Object 为原生底层对象,到这里就停止了查找, 如果没有找到,就会报错或者返回 undefined
function Fun(){}
// undefined
var fun = new Fun();
// undefined
fun.xxx
// undefined   因为xxx不存在所以返回undefined, 如果没有找到它会跟着原型链进行查找
fun.__proto__
// {constructor: ƒ}       没有xxx继续向下一个__proto__查找
fun.__proto__.__proto__
// {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
// fun.__proto__.__proto__.__proto__      没有xxx继续再向下一个__proto__查找
// null    因为继承的关系, 所有对象都继承自Object, 如果还是没有xxx就返回null

  1. 构造函数(也就是函数)的 __ proto __ 指向 Function.prototype,它返回" ƒ () { [native code] } " (空函数,我习惯把它叫构造器函数);当然 Function也是一个函数,所以Function.__ proto __也指向空函数
function Fun(){}
// undefined       
Fun.__proto__        
// ƒ () { [native code] }     Fun构造函数的prototype返回一个空函数
Function.prototy pe               
// ƒ () { [native code] }     Function构造函数的prototype返回一个空函数          
Function 
// ƒ Function() { [native code] }          Function本身是一个函数
Function.__proto__ == Function.prototype
// true        所以Function的隐式原型等于Function的显示原型 

  1. 空函数的 __ proto __ 指向 Object.prototype, 即空函数" ƒ() { [native code] } " 的隐式原型指向Object的显示原型
function Fun(){}
// undefined       
Fun.__proto__        
// ƒ () { [native code] }   空函数
Function.prototype               
// ƒ () { [native code] }   空函数
Object
// ƒ Object() { [native code] }   可以看出Object也是一个函数
Object.prototype
// {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
Fun.__proto__.__proto__
// {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}
Fun.__proto__.__proto__ == Object.prototype
// true     空函数的隐式原型指向Object显示原型 

  1. __ proto __是浏览器厂商实现的,W3C规范中并没有这个东西【所以不同浏览器的表现可能不同】
  1. constructor(构造器)是 prototype 的反向。
function Fun(){};
// undefined
Fun.prototype.constructor  
// ƒ Fun(){}
Fun.prototype.constructor == Fun
// true  可以看出构造函数Fun的显示原型的构造器等于构造函数本身

  1. 任何函数的构造器都是Function,Function也不列外
function Fun(){};
// undefined
Fun.constructor
// ƒ Function() { [native code] }   函数的构造器数是Function
Function
// ƒ Function() { [native code] }   Function本身是一个函数
Function.constructor
// ƒ Function() { [native code] }   Function的构造器是Function
Function.constructor == Function 
// true     可以看出Function函数的构造器就是它本身
Fun.constructor == Function
// true     得出Fun函数的构造器也是Function

  1. 实例化对象的构造器返回的本对象的构造函数
function Fun(){}    // 定义Fun函数
// undefined
fun = new Fun();    // 实例化一个fun对象
// Fun {}
fun.constructor    // 等同于 fun.__proto__.constructor
// ƒ Fun(){}     
fun.constructor == Fun
// true
// 原理是构造函数的显示原型里边有一个构造器, 当创建对象之后, 对象就会去找隐式原型里边的构造器返回构造函数本身

  1. 对于很多原生构造函数(实例化对象)以 Number为例:

(1).__ proto __ 指向 Number.prototype

但是 Number.prototype 也是一种 Number的实例化对象

所以 (1).__ proto __.constructor == Number.prototype.constructor

但是注意: (1).__ proto . proto __ 指向的是 Object.prototype

(1).__proto__ == Number.prototype   
// true   可以近似看成 (1) ~= new Number(1) 这样 
Number.prototype
// Number {0, constructor: ƒ, toExponential: ƒ, toFixed: ƒ, toPrecision: ƒ, …}   看上去也是一个对象
(1).__proto__.constructor ==  Number.prototype.constructor
// true
(1).__proto__.__proto__ ==  Number.prototype.__proto__
// true
(1).__proto__.__proto__ ==  Object.prototype
// true  是因为继承关系
// 可以理解为原生的构造函数在浏览器上已经已经实例化为我们封装好了一些常用的方法, 所以Number.prototype看上去也是一个对象

3) instanceof
  • 用于检测构造函数的prototype 属性是否出现在某个实例对象的原型链上
  • 语法:object instanceof constructor
    • object:某个实例对象
    • constructor:某个构造函数
function Fun() {}
// undefined
fun = new Fun()
// Fun {}
fun instanceof Fun
// true

2.this

  • this是一个关键字,在浏览器中,全局的this指向的是window;而node.js中this指向的是global。

  • this的五种绑定方式 :

    • 默认绑定(非严格模式下this指向全局对象,严格模式下this会绑定到undefined)
    • 隐式绑定(当函数引用有上下文对象时,如 obj.foo()的调用方式,foo内的this指向obj)
    • 显式绑定(通过call()或者apply()方法直接指定this的绑定对象,如foo.call(obj))
    • new绑定(针对new关键词的绑定,当构造函数被实例化后的绑定形式)
    • 箭头函数绑定(this的指向由外层作用域决定的)
  • 严格模式:严格模式是在 ECMAScript5(ES5)中引入的,在严格模式下,JavaScript 对语法的要求会更加严格,一些在正常模式下能够运行的代码,在严格模式下将不能运行。

    • JavaScript 脚本的开头添加"use strict";'use strict';指令即可开启js严格模式代码。
    • 严格模式的特征:
      1. 不允许使用未声明的变量
      2. 不允许删除变量或函数
      3. 函数中不允许有同名的参数
      4. eval 语句的作用域是独立的
      5. 不允许使用 with 语句
      6. 不允许写入只读属性
      7. 不允许使用八进制数(只禁用了 0,没有禁用 0o)
      8. 不能在 if 语句中声明函数
      9. 禁止使用 this 表示全局对象(但是全局this不受影响,只影响函数内部的this)
1) 默认绑定
// 1.非严格模式
(function f() { console.log(this) }())
// Window {window: Window, self: Window, document: document, name: 'window', location: Location, …}
// 严格模式
'use strict';
(function f() { console.log(this) }())
// undefined
// 【总结:】当函数没有所属对象直接调用时,this指向的是全局对象。

// ================================================================================================================
// 2.典例
// eg1:
debugger ;
var a = 1;
function foo() {
    var a = 2;
    console.log(this);
    console.log(this.a);
}
foo()   // 因为foo函数调用时处于全局环境下(全局window)
// Window {window: Window, self: Window, document: document, name: 'window', location: Location, …}
// 1

// eg2:
debugger ;var a = 1;
function foo() {
    var a = 2;
    function inner() {
        console.log(this.a)
    }
    inner()
}
foo()   // 因为foo函数调用时处于全局环境下(全局window)
// 1 
//【总结:】环境对象的this: 谁调用我,我就指向谁

2) 隐式绑定
// 1.将函数放到对象中
debugger ;function foo() {
    console.log(this.a)
}
var obj = {
    a: 1,
    fun: foo
};
var a = 2;
obj.foo();  
// 1   obj的fun属性值指向foo函数,而fun的调用体是obj对象,此时的this就指向obj对象。
// 等同于
var obj = {
    a: 1,
    foo: function() { console.log(this.a) }
};
var a = 2;
obj.foo()
// js引擎会将函数单独保存在内存中,然后再将函数地址赋值给foo属性的value属性;
// {
//     foo: {
//         [[value]]: 函数的地址
//         ...
//     }
// }
// 由于函数是一个单独的值,所以它可以在不同的环境(上下文)执行。

// ================================================================================================================
// 2.隐式丢失
// 隐式丢失其实就是被隐式绑定的函数在特定的情况下会丢失绑定对象。
// 当使用另一个变量来给函数取别名或者将函数作为参数传递时会被隐式赋值,回调函数丢失this绑定
// 隐式丢失案例
var obj = {
  name: '金誉',
  info: function () {
    return '姓名:'+ this.name;
  }
};
var name = 'tzw';
var f = obj.info;      // 只要函数被赋给另一个变量,this的指向就会变
f()   // "姓名:tzw"  obj.info被赋值给变量f,内部的this就会指向f运行时所在的对象

// ================================================================================================================
// 典例
// eg1:
var val = 10;
obj = {
    val: 1,
    fun: function() {
        console.log(this)              // {val: 1, fun: ƒ} this指向调用对象
        var foo = function(){
            console.log(this);  // Window {window: Window, self: Window, document: document, name: 'window', location: Location, …} this指向window,匿名函数赋值给了变量foo,回调函数丢失this绑定就指向了全局window
            console.log(this.val);    // 10 取得是全局window对象的val属性值
        }
        foo();
        return this.val        // 1 fun是通过obj对象去调用的, 所以返回的this.val指向obj对象的val
    }
}
obj.fun(); 

// ================================================================================================================
// eg2:
debugger ;function foo() {
    console.log(this.a);      // 2 在函数deFOO调用时, obj对象的foo属性作为实参, foo本身是一个函数, 将函数作为参数传递时this会丢失,所以this.a就指向了全局的变量a
}
function doFoo(fn) {
    console.log(this);    // {a: 3, doFoo: f} 因为是obj2对象调用的,所以this指向obj2对象
    fn()                
}
var obj = {
    a: 1,
    foo
};
var a = 2;
var obj2 = {
    a: 3,
    doFoo
};
obj2.doFoo(obj.foo)
//【总结:】函数的定义位置不影响其this指向,this指向只和调用函数的对象有关;
// 多层嵌套的对象,内部方法的this指向离被调用函数最近的对象。

3) 显式绑定

在逆向中极其极其极其常用,补环境,抠代码读代码都常用

  • 可以强行使用某些方法,改变函数内this的指向

  • 通过call()、apply()或者bind()方法直接指定this的绑定对象

    • 使用.call()或者.apply()的函数是会直接执行的
    • bind()是创建一个新的函数,需要手动调用才会执行
    • 如果call、apply、bind接收到的第一个参数是空或者null、undefined的话,则会忽略这个参数。
    debugger ;function foo() {
        console.log(this.a)
    }
    var a = 2;
    foo.call();           // 2    
    foo.call(null);       // 2
    foo.call(undefined)   // 2
    
    
(1) call()
  • call(对象, arg1, arg2...) 第一个参数是新的this指向,从arg1参数开始之后是传递给参数的实参,可以是数字、字符串和数组等类型的数据类型都可以。
debugger;const obj = { a: 100 };
function sum(x, y) {
    console.log(this.a + x + y);
}
sum(1, 2)   // underfined+1+2=NaN this指向window,window下面没有a属性,所以window.a是undefined
sum.call(obj, 1, 2)   // 100+1+2=103 通过call改变this,让其指向obj,obj下面具有a属性,所以obj.a是100

(2) apply()
  • apply(对象, [arg1, arg2...])第一个参数就是新的this指向, 第二个参数是一个数组或者类数组,里面的值依然是函数自身的参数。
debugger ;var obj = {
    name: 'tzw'
};
window.name = 'window';
var getName = function(age) {
    alert(this.name);
    console.log('姓名:' + this.name + '\n' +'年龄:' + age)
};
getName();      // 姓名:window 年龄:undefined     this指向window,没有传参所以age是undefined
getName.apply(obj, [26])  // 姓名:tzw 年龄:26     通过apply改变this,让其指向obj下面的name属性,所以name是tzw
// 注意中括号是apply的第二个参数必须是数组或者类数组。

(3) bind()
  • bind(对象, arg1, arg2...) 语法和call()一模一样,区别在于立即执行还是等待执行,bind不兼容IE6~8
  • bind 返回的是一个函数体(新的函数),并不会执行函数
debugger; 
window.name = 'tzw';
const obj = {
    name: 'qdd'
}
function fun() {
    console.log(this.name);
}
fun.bind(obj)      // fun() { console.log(this.name); }  函数没有调用返回一个新函数
fun.bind(obj)()    // 'qdd'

// 典例
// eg1:
debugger ;
var obj2 = {
    a: 2,
    foo1: function() {
        console.log(this.a)
    },
    foo2: function() {
        setTimeout(function() {
            console.log(this);
            console.log(this.a)
        }, 0)
    }
};
var a = 3;
obj2.foo1();      // 2          
obj2.foo2();      // setTimeout 是浏览器自带对象方法,所以this指向了window对象
// Window {window: Window, self: Window, document: document, name: 'tzw', location: Location, …}
// 3 

// ===============================================================================================================
// eg2:
debugger ;var obj1 = {
    a: 1
};
var obj2 = {
    a: 2,
    foo1: function() {
        console.log(this.a)
    },
    foo2: function() {
        setTimeout(function() {
            console.log(this);
            console.log(this.a)
        }.call(obj1), 0)
    }
};
var a = 3;
obj2.foo1();    // 2
obj2.foo2();    
// {a:1}            
// 1

// ===============================================================================================================
// eg3:
debugger ;var obj1 = {
    a: 1
};
var obj2 = {
    a: 2,
    foo1: function() {
        console.log(this.a)
    },
    foo2: function() {
        function inner() {
            console.log(this);
            console.log(this.a)
        }
        inner()         
    }
};
var a = 3;
obj2.foo1();     // 2   
obj2.foo2();
// Window {window: Window, self: Window, document: document, name: 'tzw', location: Location, …}
// 3

// ===============================================================================================================
// eg4:
debugger ;var obj1 = {
    a: 1
};
var obj2 = {
    a: 2,
    foo1: function() {
        console.log(this.a)
    },
    foo2: function() {
        function inner() {
            console.log(this);
            console.log(this.a)
        }
        inner.call(obj1)
    }
};
var a = 3;
obj2.foo1();   // 2
obj2.foo2()  
// {a: 1}
// 1

// ===============================================================================================================
// eg5:
debugger ;function foo() {
    console.log(this.a);
    return function() {
        console.log(this.a)
    }
}
;var obj = {
    a: 1
};
var a = 2;
foo();           // 2
foo.bind(obj);     // 不会打印在控制台上,中间被吞了的原因是因为控制台只会返回最后一个
foo().bind(obj)  
// 2
// (){console.log(this.a)} 

4) new绑定
  • 当使用 new 关键字调用函数时,函数中的 this 一定是js创建的新对象

  • 使用new调用函数时,会执行如下步骤:

    • 创建(或者构造)一个全新的对象
    • 这个新对象会被执行[[prototype]]连接
    • 这个新对象会绑定到函数调用的this
    • 如果函数没有返回其它对象,那么表达式中的函数调用会自动返回这个新对象。一句话概括就是:当用 new 运算符调用函数时,该函数总会返回一个对象,通常情况下,构造函数里的this就指向返回的这个对象。
debugger; let Myclass = function() {
    this.name = 'tzw';
};
let obj = new Myclass();   // 构造函数里的this就指向返回的这个对象
obj.name;    // 'tzw'
// *如果构造函数显式地返回了一个 object 类型的对象,那么此次运算结果最终会返回这个对象,而不是我们之前的this。
let Info = function(){ 
     this.name = 'qdd'; 
     return { //显式地返回一个对象
        name: 'tzw' 
     } 
}; 
let obj = new Info(); 
obj.name;    // 'tzw'
// *如果构造函数不显式地返回任何数据,或者是返回一个非对象类型的数据,就不会造成上述问题,主要是new关键字调用函数,函数内部隐式返回this造成的。

// 典例
// eg1:
debugger ;function Person(name) {
    this.name = name;
    this.foo1 = function() {
        console.log(this.name)
    }
    ;
    this.foo2 = function() {
        return function() {
            console.log(this.name)
        }
    }
}
;var person1 = new Person('person1');
person1.foo1();      // 1
person1.foo2()();    // ''
// *wind.name 不会被回收,刷新页面依然还在,有些网站会设置这个属性并检测

// ===============================================================================================================
// eg2:
debugger ;var name = 'window';
function Person(name) {
    this.name = name;
    this.foo = function() {
        console.log(this.name);
        return function() {
            console.log(this.name)
        }
    }
}
;var person1 = new Person('person1');
var person2 = new Person('person2');
person1.foo.call(person2)(); 
// person2  
// window
person1.foo().call(person2);
// person1
// person2   

5) 箭头函数绑定
  • 创建箭头函数时,就已经确定了它的 this 指向。
  • 箭头函数没有自己的this指向,它会捕获自己定义所处的外层执行环境,并且继承这个this值,指向当前定义时所在的对象。箭头函数的this指向在被定义的时候就确定了,之后永远都不会改变。即使使用 call()、 apply() 、 bind()等方法改变this指向也不可以。
  • 箭头函数的重要特征:箭头函数中没有this和arguments。
debugger ;var obj = {
    name: 'obj',
    foo1: ()=>{
        console.log(this.name)
    }
    ,
    foo2: function() {   
        console.log(this.name);
        return ()=>{       // 指向外层作用域
            console.log(this.name)
        }
    }
};
var name = 'window';
obj.foo1();
// window   foo1的对象是obj, obj是全局下定义的对象
obj.foo2()();
// obj
// obj  // 因为返回的是箭头函数,而它又是foo2属性的匿名函数的返回值,所以指向了foo2属性当前作用域下的name属性
obj2 = {
    name: 'obj2'
};
obj.foo1.call(obj2);
// window  因为call对箭头函数没有影响
//【总结:】箭头函数内的this是由外层作用域决定的

// 补充,如果箭头函数被赋给了一个变量
// function Fun() {
    this.name = () => { console.log(this); }   // 等同于 this.name = function() {console.log(this);}
}
fun = new Fun();
fun.name()     // Fun {name: ƒ}  这个this指向构造函数本身

原型链中的this: this这个值在一个继承机制中,仍然是指向它原本属于的对象,而不是从原型链上找到它时,它 所属于的对象。

总结

1.函数外面的this,即全局作用域的this指向window

**2.函数里面的this总是指向直接调用者;如果没有直接调用者,隐含的调用者是window **

**3.用new调用一个函数,这个函数即为构造函数。构造函数里面的this是和实例对象沟通 的桥梁,它指向实例对象 **

**4.事件回调里面,this指向绑定事件的对象,而不是触发事件的对象。当然这两个可以是一样的 **

5.箭头函数内的this由外层作用域决定

简单来说 this 的指向跟函数的调用位置紧密相关,要想知道函数调用时 this 到底引用了什么,就应该明确函数的调用位置。

3. 面向对象

1) 封装
  • 封装就是将变量和方法包装在一个单元中,其唯一目的是从外部类中隐藏数据。这使得程序结构更易于管理,因为每个对象的实现和状态都隐藏在明确定义的边界之后。
  • 封装,在ES6之前的使用的是构造函数,ES6之后用的是class【写前端常用但是逆向少见】

ES6的class实际就是一个语法糖,在ES6之前,是没有类这个概念的,因此是借助于原型对象和构造函数来实现。

  1. 私有属性和方法:只能在构造函数内访问不能被外部所访问(在构造函数内使用var等声明的属性)
  2. 公有属性和方法(或实例方法):对象外可以访问到对象内的属性和方法(在构造函数内使用this设置,或者设置在构造函数原型对象上比如Cat.prototype.xxx)
  3. 静态属性和方法:定义在构造函数上的方法(比如Foo.xxx),不需要实例就可以调用
// 1.静态属性方法和公有属性方法
debugger ;function Foo(arg1, arg2) {
    var private1 = 'pri1';         // *在函数内用var等定义的就是私有的
    var private2 = 'pri2';
    var private3 = function() {
        console.log(private1 + private2)
    };
    this.pub1 = arg1;             //*在函数内用this承接的就是公有的
    this.pub2 = arg2;
    this.pub3 = function() {
        private3();
        console.log('finish')
    }
}
Foo.descript = '1这是一段讲述静态属性方法的代码1';
Foo.descript2 = function() {
    console.log('2这是一段讲述静态属性方法的代码2')
}
;
Foo.prototype.descript3 = function() {
    console.log('1这是一段讲述公有属性方法的代码1')
}
;
var foo = new Foo('arg1','arg2');
console.log(Foo.descript);       // 1这是一段讲述静态属性方法的代码1
Foo.descript2();                 // 2这是一段讲述静态属性方法的代码2
console.log(foo.descript);       // undefined
foo.descript3();                 // 1这是一段讲述静态属性方法的代码1 
// 在构造函数上也就是使用Foo.xxx定义的是静态属性和方法,静态属性指的是Class本身的属性,而不是定义在实例对象(this)上的属性。
// 在构造函数内使用this设置,或者设置在构造函数原型对象上比如Foo.prototype.xxx,就是公有属性和方法(实例方法)

// ===============================================================================================================
// 2.定义在构造函数原型对象上的属性和方法不能直接表现在实例对象上,但是实例对象可以访问或者调用它们

// ===============================================================================================================
// 3. 在ES6之后,新增了class 这个关键字。它可以用来代替构造函数,达到创建“一类实例”的效果;
// 并且类的数据类型就是函数,所以用法上和构造函数很像,直接用new命令来配合它创建一个实例:
// 类的所有方法都定义在类的prototype属性上面。
debugger ;class Foo {
    constructor() {       
        // constructor()方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法;
        // 一个类必须有constructor()方法,如果没有显式定义,一个空的constructor()方法会被默认添加;
        // constructor()方法默认返回实例对象(即this),完全可以指定返回另外一个对象。
        var p1 = 'luck';
        this.p2 = 'foo';
        this.p3 = function() {}
    };
    p4 = 'white';
    p5 = function() {
        console.log('我追你如果我追到你')
    };
    p6() {       // *这个方法定义当前类的原型上
        console.log('我就把你嘿嘿嘿')
    }
}
var foo = new Foo();
console.log(foo);   // { p4: 'white', p2: 'foo', p3: f, p5: f}
foo.p5();       // '我追你如果我追到你'
foo.p6();       // '我就把你嘿嘿嘿'

// ===============================================================================================================
// 4.可以使用static标识符表示它是一个静态的属性或者方法,
// 加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。

// ===============================================================================================================
// 5.class的变量不会提升

// ===============================================================================================================
// 6.作用域
debugger ;class Cat {
    constructor() {
        this.name = 'guaiguai';
        var type = 'constructor'
    }
    type = 'class';    
    // *这里的type是新写法,等同于this.type, 新写法定义的属性是实例对象自身的属性,而不是定义在实例对象的原型上面;
    // 不带this的写法的优先级比带this的优先级高
    getType = function() {
        console.log(this.type);
        console.log(type)      // 这里的type只的是全局下的
    }
}
var type = 'window';
var guaiguai = new Cat();
guaiguai.getType();
// 'class'
// 'window'    去全局里边的取得type, class的type属性是cat的原型对象里边的

// ===============================================================================================================
// 7.闭包加深
debugger ;class Cat { 
    constructor() {          
        this.name = 'guaiguai';
        var type = 'constructor';
        this.getType = ()=>{
            console.log(this.type);
            console.log(type)
        }
    }
    type = 'class';
    getType = ()=>{
        console.log(this.type);
        console.log(type)
    }
}
var type = 'window';
var guaiguai = new Cat();
guaiguai.getType();      // constructor this.getType()取得是闭包里边携带的type变量
console.log(guaiguai);   // Cat {type: 'class', name: 'guaiguai', getType: ƒ}

2) 继承
  • 继承是指从多种实现类中抽象出一个基类,使其具备多种实现类的共同特性。比如从猫类、狗类、虎类中可以抽象出一个动物类,具有猫、狗、虎类的共同特性(吃、跑、叫等)。
(1) 原型链继承
function Parent () {
  this.name = 'Parent'
  this.sex = 'boy'
}
Parent.prototype.getName = function () {
  console.log(this.name)
  console.log(this.sex)
}
function Child () {
  this.name = 'child'
}
Child.prototype = new Parent()      // 将Child.prototype 指向Parent的实例
child1 = new Child()                // 创建了一个Child对象child1
//  Child {name: 'child'}
child1.__proto__.__proto__ == Parent.prototype
// true  child1对象的原型(__proto__)指向了Child.prototype, 而Child.prototype的原型(__proto__)指向了Parent.prototype
child1.getName()
// child       // 这个this.name 是从child1对象获取的
// boy         // child1对象没有sex属性,
console.log(child1)
// Child {name: 'child'} [[Prototype]]: { Parent name: "Parent" sex: "boy" } [[Prototype]]: Object
child1.sex = 'girl'   // 直接修改对象上的属性,是给本对象上添加一个新属性,不会修改原型引用上的sex
console.log(Child.prototype.__proto__ == Parent.prototype)
// true        // 这种方式就叫做原型链继承,将子类的原型对象指向父类的实例

// ===============================================================================================================
// 缺陷
function A () {}
function B () {
  this.name = 'anlan'
}
function C () {}
B.prototype = new C()
A.prototype = new B()
a = new A()
A.prototype.__proto__ == B.prototype    // true
a.__proto__     // C {name: 'anlan'}    // 这是一个显示问题,正常来讲a.__proto__ 指向的应该是B的原型 

// ===============================================================================================================
function Parent (name) {
  this.name = name
  this.sex = 'boy'
  this.colors = ['white', 'black']   // 引用类型,取得是内存地址
}
function Child (name) {
  this.name = name
  this.feature = ['cute']
}
var parent = new Parent('parent')
Child.prototype = parent
var child1 = new Child('child1')
child1.sex = 'girl'              // 是给本对象上添加一个新属性,不会修改原型引用上的sex
child1                           // {"name": "child1", "feature": ["cute"],"sex": "girl"}
child1.colors.push('yellow')     // Parent原型的colors属性新增了一个yellow 
parent                           // {"name": "parent", "sex": "boy", "colors": ["white", "black", "yellow"]}
child1.feature.push('sunshine')  // child1对象本身有feature,所以是在本对象的属性上添加了一个sunshine
child1                           // {"name": "child1", "feature": ["cute", "sunshine"],"sex": "girl"}
var child2 = new Child('child2')
child2                           // Child {name: 'child2', feature: Array(1)}feature : ['cute']name : "child2" [[Prototype]]:Parent colors : (3) ['white', 'black', 'yellow']name : "parent"sex : "boy" [[Prototype]]:Object  可以看出原型对象的所有属性都被共享了
// isPrototypeOf() 方法用于检查一个对象是否存在于另一个对象的原型链中。
// isPrototypeOf 实际上是instanceof 的反向。它是用来判断指定对象object1是否存在于另一个对象object2的原型链中,是则返回true,否则返回false。
Child.prototype.isPrototypeOf(child1)        // true
Parent.prototype.isPrototypeOf(child1)       // true
Object.prototype.isPrototypeOf(child1)       // true
// 原型链继承优缺点
// 优点:继承了父类的模板,又继承了父类的原型对象
// 缺点:1. 如果要给子类的原型上新增属性和方法,就必须放在Child.prototype = new Parent()这样的语句后面
//      2. 无法实现多继承, 多个原型指向同一个实例,当有多个实例化子对象时,修改一个会影响其他对象
//      3. 来自原型对象的所有属性都被共享了【浅拷贝】
//      4. 创建子类时,无法向父类构造函数传参数

(2) 构造继承
function Parent (name) {
  this.name = name
}
function Child () {
  this.sex = 'boy'
  Parent.call(this, 'child')      // 这一步操作相当于把Parent里边的this.name作为Child的属性并赋值, this.name = 'child'
  // 这里的call换成apply和bind也可以,同样的效果  
}
var child1 = new Child()
console.log(child1)               // {"sex": "boy", "name": "child"}

// ===============================================================================================================
// 变种
function Parent (name) {
  this.name = name
}
function Child () {
  this.sex = 'boy'
  Parent.call(this, 'good boy')   // 等同于 this.name = 'good boy'
  this.name = 'bad boy'           // 相当于给this.name进行重新赋值 
}
var child1 = new Child()
console.log(child1)              // {sex: 'boy', name: 'bad boy'}

// ===============================================================================================================
// 涉及引用类型
function Parent (name, sex) {
  this.name = name
  this.sex = sex
  this.colors = ['white', 'black']
}
function Child (name, sex) {
  Parent.call(this, name, sex)
}
var child1 = new Child('child1', 'boy')
child1.colors.push('yellow')
child1        // {"name": "child1", "sex": "boy", "colors": ["white", "black", "yellow"]}
var child2 = new Child('child2', 'girl')
child2        // {"name": "child2", "sex": "girl", "colors": ["white", "black"]}  
// child1和child2可以看出,是一个类似深拷贝的过程

// ===============================================================================================================
function Parent (name) {
  this.name = name
}
Parent.prototype.getName = function () {
  console.log(this.name)
}
function Child () {
  this.sex = 'boy'
  Parent.call(this, 'good boy')
}
Child.prototype.getSex = function () {
  console.log(this.sex)
}
var child1 = new Child()
console.log(child1)          // {sex: 'boy', name: 'good boy'}
child1.getSex()              // 'boy'    
child1.getName()             // TypeError: child1.getName is not a function 
// 缺点1:构造继承只能继承父类的实例属性和方法,不能继承父类原型的属性和方法
child1 instanceof Parent     // fasle   
// 缺点2:实例并不是父类的实例,只是子类的实例

(3) 组合继承(原型链继承 + 构造继承)
// 使用原型链继承来保证子类能继承到父类原型中的属性和方法;
// 使用构造继承来保证子类能继承到父类的实例属性和方法
function Parent (name) {                // call()和new的时候会被调用两次产生无意义的内存开销, new后的实例中name值为undefined
  this.name = name                      
}
Parent.prototype.getName = function () {
  console.log(this.name)
}
function Child (name) {
  this.sex = 'boy'
  Parent.call(this, name)                // 等同于this.name = name,使用call() 调用父类的属性(显式调用)
}
Child.prototype = new Parent()           // 将Child.prototype 指向Parent的实例, 等同于Child.prototype.__proto__ == Parent.prototype
Child.prototype.getSex = function () {   
  console.log(this.sex)
}
var child1 = new Child('child1')        
child1                  // {sex: 'boy', name: 'child1'}
// child1原型链:Child {sex: 'boy', name: 'child1'}name : "child1"sex : "boy" [[Prototype]]:Parent getSex : ƒ ()name : undefined [[Prototype]]:Object getName : ƒ ()constructor : ƒ Parent(name) [[Prototype]]:Object   
child1.getName()        // child1 取的是Parent原型对象上的方法, 因为child1的原型对象指向了Parent实例
child1.getSex()         // boy 取的是Child对象的原型对象Child的getSex方法
child1.constructor      // f Parent (name) {this.name = name}
// 正常来将child1.constructor指向构造函数Child本身,由于Child.prototype的继承Parent实例,导致Child.prototype.constructor被切断,沿着原型链找,最终指向了Parent.prototype的constructor, 也就是Parent (name) {this.name = name} 
// constructor 其实只是个标识作用,再实际的代码中并没有实际意义,所以是否在组合继承中修复这个地方取决于自己
Child.prototype.constrcutor = Child       // 认亲爹

// ===============================================================================================================
// 如何修改隐式原型(可以修改,但是在任何时候都不建议去人工修改隐式原型)
// 经典调用案例
var a;
(function () {
  function A () {
    this.a = 1
    this.b = 2
  }
  A.prototype.logA = function () {       
    console.log(this.a)
  }
  a = new A()   // 实例化A, 赋值给全局window的a
})()
a.logA()        // 1
// 如何在匿名函数外给A这个构造函数的原型对象中添加一个方法logB用以打印出this.b
a.constructor.prototype.logB = function() { console.log(this.b) }    
a.logB()        // 2  a.constructor指向A构造函数本身
// 用隐式原型也可以, a的__proto__指向A.prototype
a.__proto__.logB = function() { console.log(this.b) }

// ===============================================================================================================
function Parent (name, colors) {
  this.name = name
  this.colors = colors
}
Parent.prototype.features = ['cute']
function Child (name, colors) {
  Parent.apply(this, [name, colors])
}
Child.prototype = new Parent()
Child.prototype.constructor = Child
var child1 = new Child('child1', ['white'])
child1.colors.push('yellow')
child1.features.push('sunshine')
var child2 = new Child('child2', ['black'])
child1.colors         // ['cute', 'sunshine']
child2.colors         // ['white', 'yellow']
child1.features       // ['cute', 'sunshine']
child2.features       // ['cute', 'sunshine']
// features是定义在父类构造函数原型对象中的,是比new Parent()还要更深一层的对象;
// 它只能解决原型(匿名实例)中引用属性共享的问题。features是Parent.prototype上的属性,相当于是爷爷那一级
// 组合继承的优点
// 优点:
//		1.可以继承父类实例属性和方法,也能够继承父类原型属性和方法
//      2.弥补了原型链继承中引用属性共享的问题(注意原型的原型的属性依旧会共享给实例)
//      3.可传参,可复用
// 缺点:原型链继承和构造继承的时候都会调用一次
// 		1.使用组合继承时,父类构造函数会被调用两次
// 		2.并且生成了两个实例,子类实例中的属性和方法会覆盖子类原型(父类实例)上的属性和方法,所以增加了不必要的内存。

(4) 寄生组合继承
  • 核心:解决多次调用父类构造函数问题
// 1.Object.create(proto, propertiesObject) 创建一个新对象,新对象的原型链将指向指定的原型对象的方法。
// 参数一:需要指定的新对象的原型对象,即新对象通过原型链继承了原型对象的属性和方法
// 参数二:可选参数,给新对象自身添加新属性以及描述器
// 2.Object.setPrototypeOf(obj, prototype) 用于设置一个对象的原型的方法
// 参数一:要设置原型的对象
// 参数二:该对象的新原型 
// 标准的寄生组合继承
// 使用 Object.create()
function Parent (name) {
  this.name = name
}
Parent.prototype.getName = function () {
  console.log(this.name)
}
function Child (name) {
  this.sex = 'boy'
  Parent.call(this, name)
}
// 与组合继承的区别
Child.prototype = Object.create(Parent.prototype)   // 创建了一个空对象,并且这个对象的__proto__属性是指向Parent.prototype
var child1 = new Child('child1')
child1                  // Child {sex: 'boy', name: 'child1'} 
child1.getName()        // child1 
child1.constructor      // Parent (name) this.name = name  指向继承的Parent
console.log(child1.__proto__)    
// 原型链:Parent {}[[Prototype]]:Object getName : ƒ ()constructor : ƒ Parent(name) [[Prototype]]:Object

// ===============================================================================================================
// 使用 Object.setPrototypeOf()
function Parent (name) {
  this.name = name
}
Parent.prototype.getName = function () {
  console.log(this.name)
}
function Child (name) {
  this.sex = 'boy'
  Parent.call(this, name)
}
// 与组合继承的区别
Object.setPrototypeOf(Child.prototype, Parent.prototype) // 将Child的原型设置为Parent的对象
var child2 = new Child('child2')
child2                 // Child {sex: 'boy', name: 'child1'}
child2.getName()       // child2
child2.constructor     // Child (name) {this.sex = 'boy'Parent.call(this, name)}
child2.__proto__      
// 原型链:Parent {constructor: ƒ}constructor : ƒ Child(name) [[Prototype]]:Object getName : ƒ ()constructor : ƒ Parent(name) [[Prototype]]:Object
// 可以得出Object.setPrototypeOf()方法设置对象的原型更加完美一些,它修复了子类原型中constructor的指向问题

// ===============================================================================================================
// function Parent(name){
  this.name = name
  this.face = 'cry'
  this.colors = ['white', 'black']
}
Parent.prototype.features = ['cute']
Parent.prototype.getFeatures = function (){
  console.log(this.features)
}
function Child(name){
  Parent.call(this, name)
  this.sex = 'boy'
  this.face = 'smile'
}
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
var child1 = new Child('child1')
child1.colors.push('yellow')
var child2 = new Child('child2')
child2.features = ['sunshine']
console.log(child1)
// 原型链:Child {name: 'child1', face: 'smile', colors: Array(3), sex: 'boy'}colors : (3) ['white', 'black', 'yellow']face : "smile"name : "child1"sex : "boy" [[Prototype]]:Parent constructor : ƒ Child(name) [[Prototype]]:Object features : ['cute']getFeatures : ƒ ()constructor : ƒ Parent(name) [[Prototype]]:Object
console.log(child2)
// 原型链:Child {name: 'child2', face: 'smile', colors: Array(2), sex: 'boy', features: Array(1)}colors : (2) ['white', 'black']face : "smile"features : ['sunshine']name : "child2"sex : "boy" [[Prototype]]:Parent constructor : ƒ Child(name) [[Prototype]]:Object features : ['cute']getFeatures : ƒ ()constructor : ƒ Parent(name) [[Prototype]]:Object
// 寄生组合继承算是ES6之前一种比较完美的继承方式。
// 它避免了组合继承中调用两次父类构造函数,初始化两次实例属性的缺点
// 它拥有了原型链继承、构造继承和组合继承的所有继承方式的优点
// 只调用了一次父类构造函数,只创建了一份父类属性
// 子类可以用到父类原型链上的属性和方法
// 能够正常的使用instanceOf和isPrototypeOf方法
// Object.setPrototypeOf()方法会在运行时动态地改变对象的原型,这可能会对性能产生一些影响;
// 在创建对象时就应该使用Object.create()来设置原型链,而不是后期动态地改变原型链。

(5) 原型式继承
// 原理是创建一个构造函数,构造函数的原型指向对象,然后调用new 操作符创建实例,并返回这个实例,本质是一个浅拷贝
var cat = {
  heart: '❤️',
  colors: ['white', 'black']
}
var guaiguai = Object.create(cat)
var huaihuai = Object.create(cat)
console.log(guaiguai)    
// 原型链:{}[[Prototype]]:Object colors : (2) ['white', 'black']heart : "❤️" [[Prototype]]:Object
console.log(huaihuai)
// 原型链:{}[[Prototype]]:Object colors : (2) ['white', 'black']heart : "❤️" [[Prototype]]:Object
cat.colors.push('blue')     // 在原对象上个修改colors属性
guaiguai.colors             // ['white', 'black', 'blue'] 浅拷贝
// 同样的,对于 setPrototypeOf而言也是
var cat = {
  heart: '❤️',
  colors: ['white', 'black']
}
res1 = {}        // 因为Object.setPrototypeOf()没有返回值,所以先定义好一个对象
Object.setPrototypeOf(res1, cat);       // res1的原型指向cat对象

(6) 寄生式继承
// 在原型式继承的基础之上进行了一下优化
var cat = {
  heart: '❤️',
  colors: ['white', 'black']
}
function createAnother (original) {         // 在原型式继承的基础上再封装一层,来增强对象,之后将这个对象返回。
    var clone = Object.create(original);
    clone.actingCute = function () {
      console.log('我是一只会卖萌的猫咪')
    }
    return clone;
}
var guaiguai = createAnother(cat)
var huaihuai = Object.create(cat)
huaihuai.heart             // '❤️'
guaiguai.actingCute()      // 我是一只会卖萌的猫咪

(7) 混入式继承(多继承)
// 我们一直都是以一个子类继承一个父类,而混入方式继承就是教我们如何一个子类继承多个父类的。
// 需要用到ES6中的方法Object.assign()
// 它的作用就是可以把多个对象的属性和方法拷贝到目标对象中,若是存在同名属性的话,后面的属性会把前面的覆盖掉
// Object.assign(target, ...sources) 静态方法将一个或者多个源对象中所有可枚举的自有属性复制到目标对象,并返回修改后的目标对象
// 参数一:需要应用源对象属性的目标对象,修改后将作为返回值
// 参数二:一个或多个包含要应用的属性的源对象
function Parent (sex) {
  this.sex = sex
}
Parent.prototype.getSex = function () {
  console.log(this.sex)
}
function OtherParent (colors) {
  this.colors = colors
}
OtherParent.prototype.getColors = function () {
  console.log(this.colors)
}
function Child (sex, colors) {
  Parent.call(this, sex)               
  OtherParent.call(this, colors)       // 新增的父类
  this.name = 'child'
}
Child.prototype = Object.create(Parent.prototype)
Object.assign(Child.prototype, OtherParent.prototype) // 新增的父类原型对象
Child.prototype.constructor = Child
var child1 = new Child('boy', ['white'])
console.log(child1) 
// 原型链:Child {sex: 'boy', colors: Array(1), name: 'child'}colors: ['white']name: "child"sex: "boy"[[Prototype]]: Parentconstructor: ƒ Child(sex, colors)getColors: ƒ ()arguments: nullcaller: nulllength: 0name: ""prototype: {constructor: ƒ}[[FunctionLocation]]: VM23262:10[[Prototype]]: ƒ ()[[Scopes]]: Scopes[1][[Prototype]]: ObjectgetSex: ƒ ()constructor: ƒ Parent(sex)[[Prototype]]: Object
child1.__proto__               // Parent {getColors: ƒ, constructor: ƒ}
child1.__proto__.__proto__     // {getSex: ƒ, constructor: ƒ}
// 可以看出Parent.prototype.getSex()和OtherParent.prototype.getColors(),他们不在同一个原型上, 正常来讲是在同一个原型上

// ===============================================================================================================
function Parent (sex) {
  this.sex = sex
}
Parent.prototype.getSex = function () {
  console.log(this.sex)
}
function OtherParent (colors) {
  this.colors = colors
}
OtherParent.prototype.getColors = function () {
  console.log(this.colors)
}
function Child (sex, colors) {
  Parent.call(this, sex)
  OtherParent.call(this, colors) // 新增的父类
  this.name = 'child'
}
Object.assign(Parent.prototype, OtherParent.prototype) // 新增的父类原型对象,其它原型上的属性添加的目标原型上
Child.prototype = Object.create(Parent.prototype)      // 创建一个Parent的新对象,新对象指向Child的原型
// Object.setPrototypeOf(Child.prototype, Parent.prototype)
Child.prototype.constructor = Child          // 认亲爹
var child1 = new Child('boy', ['white'])
console.log(child1)                     
// 原型链:Child {sex: 'boy', colors: Array(1), name: 'child'}colors : ['white']name : "child"sex : "boy" [[Prototype]]:Parent constructor : ƒ Child(sex, colors) [[Prototype]]:Object getColors : ƒ ()getSex : ƒ ()constructor : ƒ Parent(sex) [[Prototype]]:Object
child1.__proto__.__proto__           
// {getSex: ƒ, getColors: ƒ, constructor: ƒ}  getSex和getColors现在已经在同一个原型上了
console.log(Child.prototype.__proto__ === Parent.prototype)           // true
console.log(Child.prototype.__proto__ === OtherParent.prototype)      // false
console.log(child1 instanceof Parent)                                 // true
console.log(child1 instanceof OtherParent)                            // false
// 混入式继承的缺点:对于两个或者多个父类,只能维持一个instanceof正常。

(8) Class 继承
  • 主要是依靠两个关键字:extendsuper
class Parent {
  constructor (name) {
    this.name = name
  }
  getName () {
    console.log(this.name)
  }
}
class Child extends Parent {        // class通过extends关键字实现继承,让子类继承父类的属性和方法
  constructor (name) {
    super(name)                     // super在这里表示父类的构造函数,用来新建一个父类的实例对象
    this.sex = 'boy'
  }
}
var child1 = new Child('child1')
console.log(child1)    
// 原型链:Child {name: 'child1', sex: 'boy'}name : "child1"sex : "boy" [[Prototype]]:Parent constructor : class Child[[Prototype]]:Object constructor : class Parent getName:ƒ getName() [[Prototype]]:Object
// 从完整的原型链可以看出class的继承方式完全满足于寄生组合继承

// ===============================================================================================================
class Parent {
  constructor () {
    this.name = 'parent'
  }
}
class Child extends Parent {
  constructor () {
    // super(name) // super注释掉   
  }
}
var child1 = new Child()    // Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor 
/* 
【重点】
	1.ES6规定,子类必须在constructor()方法中调用super(),否则就会报错。
	  这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,
   	  然后再对其进行加工,添加子类自己的实例属性和方法。如果不先调用super()方法,子类就得不到自己的this对象。
    2.如果使用了extends实现继承的子类内部没有constructor方法,则会被默认添加constructor和super;
      若子类中有constructor方法,必须得在constructor中调用一下super函数,否则会报错。
    4.需要注意的地方是,在子类的constructor方法中,只有调用super()之后,才可以使用this关键字,否则会报错。
      这是因为子类实例的构建,必须先完成父类的继承,只有super()方法才能让子类实例继承父类。
      可以理解为,新建子类实例时,父类的构造函数必定会先运行一次
*/

// ===============================================================================================================
// super关键字,既可以当作做函数使用,也可以当做对象使用。
// 1.super当成函数调用时,代表父类的构造函数,且返回的是子类的实例,也就是此时super内部的this指向子类
class Parent {
  constructor () {
    console.log(new.target.name)     // 在构造函数内部使用new关键字创建对象时,返回该构造函数的名称。
  }
}
class Child extends Parent {
  constructor () {
    var instance = super()          // 在子类的constructor中super()就相当于是Parent.prototype.constructor.call(this)
    /* 调用super()的作用是形成子类的this对象,把父类的实例属性和方法放到这个this对象上面;
       子类在调用super()之前,是没有this对象的,任何对this的操作都要放在super()的后面。*/
    console.log(instance)
    console.log(instance === this)
  }
}
var child = new Child()   
// Child         
// Child {}  super当做函数来使用时,虽然它代表着父类的构造函数,但是返回的却是子类的实例,也就是说super内部的this指向的是Child。  
// true
/*
super当做函数使用时的限制:	 
	1.子类constructor中如果要使用this的话就必须放到super()之后
	2.super当成函数调用时只能在子类的construtor中使用
*/

// super()在子类构造方法中执行时,子类的属性和方法还没有绑定到this,如果存在同名属性,此时拿到的是父类的属性。
class A {
  name = 'A';
  constructor() {
    console.log(this.name);
  }
}
class B extends A {
  name = 'B';
}
new B();  // A  原因在于super()执行时,B的name属性还没有绑定到this,所以this.name拿到的是A的name属性

// super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
// 在普通方法中:
class A {
  p() {
    console.log('tzw');
  }
}
class B extends A {
  constructor() {
    super();
    super.p();         // 2 就是将super当作一个对象使用, super.p()就相当于A.prototype.p()
    // 需要注意的是由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的。
    // 或者说定义在父类原型对象上的属性和方法,super就可以取到
  }
}
new B();
// ES6规定,在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前的子类实例。
class A {
  constructor() {
    this.x = 1;
    this.y = 1;
  }
  print() {
    console.log(this.x);
  }
}
class B extends A {
  constructor() {
    super();         
    this.x = 2;
    console.log(this.y)      // 1
    super.y = 2;    // 由于this指向子类实例,所以如果通过super对某个属性赋值,这时super就是this,赋值的属性会变成子类实例的属性。
    console.log(super.y)     // undefined
    // super.x赋值2,这时等同于对this.x赋值为3。而当读取super.x的时候,读的是A.prototype.x,所以返回undefined
	console.log(this.y)      // 2
  }
  m() {
    super.print();           // 2 调用的是A.prototype.print(),但是A.prototype.print()内部的this指向子类B的实例,导致输出的是2,而不是1。也就是说,实际上执行的是super.print.call(this)
  }
}
let b = new B();
b.m()     // 2

// ===============================================================================================================
// 2.super作为对象时用在静态方法之中,这时super将指向父类;在普通方法之中指向父类的原型对象。
class Parent {
  constructor (name) {
    this.name = name
  }
  getName () {
    console.log(this.name)
  }
}
Parent.prototype.getSex = function () {
	console.log('boy')              // child1
}
Parent.getColors = function () {
  console.log(['white'])
}
class Child extends Parent {
  constructor (name) {
    super(name)
    super.getName()
    // console.log(super);         // 报错 要能清晰地表明super的数据类型(作为函数还是对象使用)才不会报错
  }
  instanceFn () {
    super.getSex()                 // super在普通方法之中指向父类的原型对象
  }
  static staticFn () {
    super.getColors()   // 在子类的静态方法中通过super调用父类的方法时,方法内部的this指向当前的子类,而不是子类的实例。  
  }
}
var child1 = new Child('child1') 
child1.instanceFn()  // boy 在普通方法child1.instanceFn里面,super.getSex指向父类的原型对象
Child.staticFn()     // ['white'] 静态方法Child.staticFn里面,super.staticFn指向父类的静态方法。这个方法里面的this指向的是Child,而不是Child的实例。
//由于对象总是继承其他对象的,所以可以在任意一个对象中,使用super关键字
// extends后面接着的继承目标不一定要是个class,只要父类是一个有prototype属性的函数就能被子类继承

3) 多态性
  • 多态性是指具体多种形态或者实现方式,Java中的多态性允许类的子类定义它们自己的唯一行为,并且还共享父类的一些相同功能。
// 多态最根本的作用就是通过把过程化的条件语句转化为对象的多态性,从而消除这些条件分支语句。
// 实际上是一种编程的思想
// 对于JS是具有与生俱来的多态性它的变量类型在运行期是可变的,程序并没有要求我指定它的类型,也就是它并不存在这种类型之间的耦合关系。

4) 代理与反射

在JavaScript中通过代理和反射,开发人员可以更加灵活地操作和控制对象的行为,从而实现更高级的功能和自定义行为。

  • 先了解什么是属性描述符?

JavaScript中的属性描述符("Property Descriptor"本质上是一个JavaScript 普通对象)是用于描述一个属性的相关信息。每个属性都有一个相关联的属性描述符,它由以下几个属性组成:

​ 1.value:属性的值。可以是任何有效的JavaScript值。默认为undefined

​ 2.writable:该属性是否可以被重新赋值存取器属性。如果设置为true,则属性的值可以被修改。默认为true

​ 注意:属性描述符中如果配置了 get 和 set 中的任何一个,则该属性不再是一个普通属性,而变成了存取器属性。

​ get()读值函数:如果一个属性是存取器属性,则读取该属性时,会运行 get 方法,并将 get 方法得到的返回值作为属性值;

​ set(newVal)存值函数:如果给该属性赋值,则会运行 set 方法,newVal 参数为赋值的值。

​ * value 和 writable 属性不能与 get 和 set 属性共存,二者只能选其一

​ 存取器属性最大的意义,在于可以控制属性的读取和赋值,在函数里可以进行各种操作。

​ 3.enumerable:表示属性是否可枚举。如果设置为true,则属性可以在for..in循环中被枚举。默认为true

​ 4.configurable:表示属性是否可配置。如果设置为true,则允许修改属性的描述符和删除属性。默认为true

/*
1.获取对象属性描述符
   	Object.getOwnPropertyDescriptor(对象, 属性名)   获取一个对象的某个属性的属性描述符
   	Object.getOwnPropertyDescriptors(对象)         获取某个对象的所有属性描述符
2.为某个对象添加属性时 或 修改属性时,配置其属性描述符,使用以下这两种方法
	Object.defineProperty(对象, 属性名, 描述符)      设置一个对象的某个属性
	Object.defineProperties(对象, 多个属性的描述符);  设置一个对象的多个属性
需要注意的是,如果尝试修改不可配置的属性描述符,或者通过`Object.defineProperty()`方法定义已经存在的属性,严格模式下会抛出错误,非严格模式下会被忽略。
*/
// 主要介绍下设置多个属性的描述符
var obj = {
    property1: false,
    property2: false,
};
Object.defineProperties(obj, {
  'property1': {
    value: true,
  },
  'property2': {
    value: 'Hello',
    enumerable: false,      // 设置为不可枚举
  }
});
for (let i in obj) { console.log(i) }     // property1 property2不会被遍历,因为for...in遍历的是对象所有可遍历(enumerable)的属性 


// ===============================================================================================================
// 描述符修改的几种情况
// 1.
let person1 = {};
Object.defineProperty(person1, "name", {
    configurable: true,                                       // 允许修改属性的描述符和删除属性
    writable: true,                                           // 属性的值可以被修改
    value: "abc",
})
Object.defineProperty(person1, "name", {writable: false})     // 第二次将writeable修改为第一次设置的相反值
console.log(person1.name);      // abc
// 2.
let person2 = {};
Object.defineProperty(person2, "name", {
    configurable: true,                                       // 允许修改属性的描述符和删除属性
    writable: false,                                          // 属性的值不可以被修改
    value: "def",
})
Object.defineProperty(person2, "name", {writable: true})     // 第二次将writeable修改为第一次设置的相反值
console.log(person2.name);      // abc
// 3.
let person3 = {};
Object.defineProperty(person3, "name", {
    configurable: false,                                    // 不允许修改属性的描述符和删除属性
    writable: true,                                         // 属性的值可以被修改
    value: "123",
})
Object.defineProperty(person3, "name", {writable: false})   // 第二次将writeable修改为第一次设置的相反值
console.log(person3.name);     // 123     
// 4.
let person4 = {};
Object.defineProperty(person4, "name", {
    configurable: false,
    writable: false,
    value: "456",
})
Object.defineProperty(person4, "name", {writable: true})   // 报错 TypeError: Cannot redefine property:
/*
	1.当configurable为true 时,一切都是可以修改的
	2.当configurable为false时,第二次修改为原值时不报错,如果第二次修改为相反值时存在一种特殊情况
	  configurable 与 writable 有一个值为true时value的值可修改 writable--->false
    3.当configurable 为 false 时, 除了第2种情况之外,其它的修改都会报错。
    ----------------------------------------------------------------------------------------
    enumerable 看 configurable 
      (configurable和enumerable:这两个选项之间没有直接的互斥关系,但它们与其他属性描述符选项存在相互关系)
	value 看 configurable和writable
	writable 看 configurable和writable
	value和writable 与 get与set 互斥
	get和set:如果同时设置了get和set选项,则不能再设置value或writable选项;
	  get和set方法提供了属性的自定义读取和写入行为,因此value和writable选项变得无意义。
*/

ES6 新增的代理和反射为开发者提供了拦截并向基本操作嵌入额外行为的能力。具体地说,**可以给目标对象(target)定义一个关联的代理对象,而这个代理对象可当作一个抽象的目标对象来使用。**因此在对目标对象的各种操作影响到目标对象之前,我们可以在代理对象中对这些操作加以控制,并且最终也可能会改变操作返回的结果。

(1) 代理
  • 代理(Proxy)是一种机制,允许你创建一个代理对象来包装目标对象,并拦截该对象的操作。通过使用代理,你可以在目标对象上定义自定义的行为,例如拦截属性读取、属性设置、函数调用等操作。这使得你可以对对象的访问和修改进行更精细的控制。
  • 代理(Proxy)能使我们开发者拥有一种间接修改底层方法的能力,从而控制用户的操作【在逆向中多用来进行hook】。
  • 语法: let target = { /目标对象的属性/ }; //目标对象 let handler = { /用来定制拦截操作/ }; //拦截层对象 let proxy = new Proxy(target, handler); //实例化
const target = {                  // 目标对象
  name: 'John'
};
const handler = {                 // 处理器对象
  // 捕获器在处理程序对象中以方法名为键
  get(target, property, receiver) {
    console.log(`Reading property '${property}'`);
    return target[property];      // 返回目标对象的属性值
  },
  set(target, property, value, receiver) {
    console.log(`Setting property '${property}' to '${value}'`);
    target[property] = value;     // 设置目标对象的属性值
    return true;                  // 设置成功返回true
  }
};
const proxy = new Proxy(target, handler);     // 创建代理对象,代理对象使用Proxy构造函数将目标对象和处理程序对象handler结合起来
proxy.name               // 查看代理对象的属性
// Reading property 'name'  
// john                  // get捕获器返回值
proxy.age = 26           // 修改代理对象设置属性
// Setting property 'age' to '26'  
// 26                    // set捕获器返回值

/*
常见的代理捕获方法:
	1.get(target, property, receiver):捕获属性的读取操作。
	2.set(target, property, value, receiver):捕获属性的赋值操作。
	3.has(target, property):捕获in运算符的操作。
	4.deleteProperty(target, property):捕获delete运算符的操作。
	5.apply(target, thisArg, argumentsList):捕获函数调用的操作
*/

(2) 反射
  • 反射(Reflection)是指通过内置的Reflect对象,提供一组方法来操作和查询对象的行为。Reflect 它提供了一系列方法,可以让开发者通过调用这些方法,访问一些 JS 底层功能;由于它类似于其他语言的反射,因此取名为 Reflect
  • 使用 Reflect 可以实现诸如:属性的赋值与取值、调用普通函数、调用构造函数、判断属性是否存在与对象中 等等功能。
const obj = {
  name: 'tzw',
  greeting() {
    return `Hello, ${this.name}!`;
  }
};
console.log(Reflect.get(obj, 'name'));    // tzw 获取obj对象的name属性
Reflect.set(obj, 'age', 26);              // true 设置obj对象的age属性
console.log(Reflect.has(obj, 'age'));     // true 查看obj对象是否有age属性
Reflect.deleteProperty(obj, 'age');       // true 删除obj对象的age属性
console.log(obj.age)                      // undefined
const result = Reflect.apply(obj.greeting, { name: 'qdd' }, []);    // 改变obj对象的greeting方法的this指向
console.log(result)                       // qdd 
/*
常见的代理捕获方法
	1.Reflect.get(target, property, receiver):获取对象的属性值。
	2.Reflect.set(target, property, value, receiver):设置对象的属性值。
	3.Reflect.has(target, property):检查对象是否具有指定属性。
	4.Reflect.deleteProperty(target, property):删除对象的属性。
	5.Reflect.apply(func, thisArg, args):调用函数并传递参数。
*/

// 典例
// eg1:(hook)
test6 = {a: 10}
var test6 = new Proxy(test6, {
        defineProperty: function(target, property, descriptor) {
        debugger;
        console.log(arguments)
        return Reflect.defineProperty(...argume   nts)
    }
});
attributes = {
    configurable: false,
    enumerable: false,
    writable: false,
    value: 1000,
}
Object.defineProperty(test6, 'a', attributes)

// eg2:
var proxy = {};
var target = proxy
var handler = {
    getPrototypeOf: function (){console.log(arguments);return Reflect.getPrototypeOf(this)}
}
proxy1 = new Proxy(target, handler)

4. 异步

1) 单线程模型

单线程模型指的是,JavaScript 只在一个线程上运行。也就是说,JavaScript 同时只能执行一个任务,其他任务都必须在后面排队等待。 注意,JavaScript 只在一个线程上运行,不代表 JavaScript 引擎只有一个线程。事实上,JavaScript 引擎有多个线程,单个脚本只能在一个线程上运行(称为主线程),其他线程都是在后台配合。

2) 同步任务和异步任务

程序里面所有的任务,可以分成两类:同步任务(synchronous)和异步任务(asynchronous)。 同步任务是那些没有被引擎挂起、在主线程上排队执行的任务。只有前一个任务执行完毕,才能执行后一个任务。 异步任务是那些被引擎放在一边,不进入主线程、而进入任务队列的任务。只有引擎认为某个异步任务可以执行了(比如 Ajax 操作从服务器得到了结果),该任务(采用回调函数的形式)才会进入主线程执行。排在异步任务后面的代码,不用等待异步任务结束会马上运行,也就是说,异步任务不具有“堵塞”效应。

3) 任务队列和事件循环

JavaScript 运行时,除了一个正在运行的主线程,引擎还提供一个任务队列(task queue),里面是各种需要当前程序处理的异步任务。 首先,主线程会去执行所有的同步任务。等到同步任务全部执行完,就会去看任务队列里面的异步任务。如果满足条件,那么异步任务就重新进入主线程开始执行,这时它就变成同步任务了。等到执行完,下一个异步任务再进入主线程开始执行。一旦任务队列清空,程序就结束执行。 JavaScript 引擎在不停地检查,一遍又一遍,只要同步任务执行完了,引擎就会去检查那些挂起来的异步任务,是不是可以进入主线程了。这种循环检查的机制,就叫做事件循环(Event Loop)。

4) Event Loop 事件循环机制

浏览器或Node的一种解决javaScript单线程运行时不会阻塞的一种机制,也就是我们经常使用异步的原理。

event loop 执行顺序: 1.一开始整个脚本作为一个宏任务执行(可以是一个js脚本,或者script标签) 2.执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列 3.当前宏任务执行完出队,检查微任务列表,有则依次执行,直到全部执行完 4.执行浏览器UI线程的渲染工作 5.检查是否有Web Worker任务,有则执行 6.执行完本轮的宏任务,回到2,依此循环,直到宏任务和微任务队列都为空

什么是宏任务队列、微任务队列? 宏任务队列,也叫宏队列:script 、setTimeout、setInterval 、setImmediate 、I/O 、UI rendering 微任务队列,也叫微队列:MutationObserver、Promise.then()或catch()、Promise为基础开发的其它技术,比如fetch API、V8的垃圾回收过程、 async 、Node独有的process.nextTick等。 需要重点关注的是:setTimeout、setInterval 、Promise.then、 async 在所有任务开始的时候,由于宏任务中包括了script,所以浏览器会先执行一个宏任务,在这个过程中你看到的延迟任务(例如setTimeout)将被放到下一轮宏任务中来执行。

5) Promise对象
  • Promise(是一个对象,也是一个构造函数)是 JavaScript 的异步操作解决方案,为异步操作提供统一接口。
  • Promise 简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
  • Promise 的设计思想是,所有异步任务都返回一个 Promise 实例。
  • Promise 实例有一个then方法,用来指定下一步的回调函数。
(1) Promis的用法
// 创建一个异步对象
var promise = new Promise((resolve, reject) => {    // 回调函数写成更简介的箭头函数,是一个标准的promise
  // ...
  if (/* 异步操作成功 */){
    resolve(value);
  } else { /* 异步操作失败 */
    reject(new Error());
  }
});
/*
  1.Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己实现。
  2.resolve函数的作用是,将Promise实例的状态从“未完成”变为“成功”(即从pending变为fulfilled),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去。
  3. reject函数的作用是,将Promise实例的状态从“未完成”变为“失败”(即从pending变为rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
*/

Promise 对象通过自身的状态,来控制异步操作。Promise 实例具有三种状态:

  • 异步操作未完成(pending)
  • 异步操作成功(fulfilled)
  • 异步操作失败(rejected)

这三种状态里边,fulfilled 和rejected 合在一起称为resolved(已定型)。 这三种的状态的变化途径只有两种:

  • 从“未完成”到“成功 ”
  • 从“未完成”到“失败”

一旦状态发生变化,就凝固了,不会再有新的状态变化。这也是 Promise 这个名字的由来,它的英语意思是“承诺”,一旦承诺成效,就不得再改变了。这也意味着,Promise 实例的状态变化只可能发生一次。

所以,Promise 的最终结果只有两种:

  • 异步操作成功,Promise 实例传回一个值(value),状态变为fulfilled
  • 异步操作失败,Promise 实例抛出一个错误(error),状态变为rejected
// 初始态
var callback = function(resolve, reject){}
const promise1 = new Promise(callback)
console.log(promise1)
// Promise {<pending>}

// 成功态
var callback = function(resolve, reject){resolve()}
const promise1 = new Promise(callback)
console.log(promise1)
// Promise {<fulfilled>: undefined}

// 失败态
var callback = function(resolve, reject){reject()}
const promise1 = new Promise(callback)
console.log(promise1)
// Promise {<rejected>: undefined}
// 以上就是promise1的三种状态

// 打印顺序是什么?
const promise1 = new Promise((resolve, reject) => {    // Promise 不是一个异步操作
  console.log('promise1')
})
console.log('1', promise1);
// promise1
// 1 Promise {<pending>}
/*
	创建promis对象的时候,此时,它是一个同步操作,promise1.then()才是一个异步操作;
	所以,执行顺序从上至下,先遇到new Promise,执行该构造函数中的代码,打印promise1;
	然后执行同步代码打印1,此时promise1没有被resolve或者reject,因此状态还是pending。
*/
/*
promise1 原型:Promise.prototype
	constructor: ƒ Promise()   这玩意没啥说的
	then: ƒ then()         	   主要看这个
	catch: ƒ catch()           看名字就应该知道它应该是错误回收
	finally: ƒ finally()       看名字就应该知道它是最后执行的
*/

(2) Promise.then
  • Promise 实例的then方法,用来添加回调函数。
// 1. promise是成功态时
const promise = new Promise((resolve, reject) => {
  console.log(1);
  resolve('success')
  console.log(2);
});
promise.then(() => {    
  console.log(3);
});
console.log(4);
console.log(promise);
// 1
// 2
// 4
// Promise {<fulfilled>: 'success'}
// 3
/* 
执行步骤:
	1.从上至下,先遇到new Promise,执行其中的同步代码打印1
	2.再遇到resolve('success'), 将promise的状态改为了fulfilled(成功态)并且将值保存下来


###  最后

其实前端开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。



这里再分享一个复习的路线:(以下体系的复习资料是我从各路大佬收集整理好的)

《前端开发四大模块核心知识笔记》  

![](https://img-blog.csdnimg.cn/img_convert/b6ecb2c71421efff2a0cd1db5a9d4251.png)



最后,说个题外话,我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。

**[开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】](https://bbs.csdn.net/topics/618166371)**

我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在IT学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。


const promise1 = new Promise(callback)
console.log(promise1)
// Promise {<pending>}

// 成功态
var callback = function(resolve, reject){resolve()}
const promise1 = new Promise(callback)
console.log(promise1)
// Promise {<fulfilled>: undefined}

// 失败态
var callback = function(resolve, reject){reject()}
const promise1 = new Promise(callback)
console.log(promise1)
// Promise {<rejected>: undefined}
// 以上就是promise1的三种状态

// 打印顺序是什么?
const promise1 = new Promise((resolve, reject) => {    // Promise 不是一个异步操作
  console.log('promise1')
})
console.log('1', promise1);
// promise1
// 1 Promise {<pending>}
/*
	创建promis对象的时候,此时,它是一个同步操作,promise1.then()才是一个异步操作;
	所以,执行顺序从上至下,先遇到new Promise,执行该构造函数中的代码,打印promise1;
	然后执行同步代码打印1,此时promise1没有被resolve或者reject,因此状态还是pending。
*/
/*
promise1 原型:Promise.prototype
	constructor: ƒ Promise()   这玩意没啥说的
	then: ƒ then()         	   主要看这个
	catch: ƒ catch()           看名字就应该知道它应该是错误回收
	finally: ƒ finally()       看名字就应该知道它是最后执行的
*/

(2) Promise.then
  • Promise 实例的then方法,用来添加回调函数。
// 1. promise是成功态时
const promise = new Promise((resolve, reject) => {
  console.log(1);
  resolve('success')
  console.log(2);
});
promise.then(() => {    
  console.log(3);
});
console.log(4);
console.log(promise);
// 1
// 2
// 4
// Promise {<fulfilled>: 'success'}
// 3
/* 
执行步骤:
	1.从上至下,先遇到new Promise,执行其中的同步代码打印1
	2.再遇到resolve('success'), 将promise的状态改为了fulfilled(成功态)并且将值保存下来


###  最后

其实前端开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。



这里再分享一个复习的路线:(以下体系的复习资料是我从各路大佬收集整理好的)

《前端开发四大模块核心知识笔记》  

![](https://img-blog.csdnimg.cn/img_convert/b6ecb2c71421efff2a0cd1db5a9d4251.png)



最后,说个题外话,我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。

**[开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】](https://bbs.csdn.net/topics/618166371)**

我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在IT学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。

  • 14
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值