一名【合格】前端工程师的自检清单(原型和原型链篇)

    xiaoxiao2022-07-04  117

     

    ##原型和原型链

     

    ###1. 理解原型设计模式以及JavaScript中的原型规则

     

    ####原型设计模式

     

    JavaScript是一种基于对象的语言, JavaScript中的所有对象, 都具有prototype属性。 prototype属性返回对象的所有属性和方法, 所有 JavaScript 内部对象都有只读的 prototype 属性, 可以向其原型中动态添加属性和方法, 但该对象不能被赋予不同的原型。 但是自定义的对象可以被赋给新的原型。

     

    对象分为函数对象和普通对象, 区分: 凡是通过 new Function() 创建的对象都是函数对象, 其他的都是普通对象。 (Object , Function 是JS自带的函数对象)

     

    1. 原型对象: prototype

     

    在JS 中, 函数对象其中一个属性: 原型对象 prototype。 普通对象是没有prototype属性, 但有__proto__属性。

     

    原型的作用就是给这个类的每一个对象都添加一个统一的方法, 在原型中定义的方法和属性都是被所以实例对象所共享。

     

      例:

     

         var person = function(name){

           this.name = name

         };

         person.prototype.getName = function(){//通过person.prototype设置函数对象属性

     

    return this.name;

     

         }

         var zxj= new person(‘zhangxiaojie’);

        zxj.getName(); //zhangxiaojie     //zxj继承上属性

     

    2. 原型链

     

    __proto__: js创建对象的内置属性, 用于指向创建它的函数对象的原型对象prototype。 (是JS内部使用寻找原型链的属性。 当实例化一个对象时候, 内部会新建一个__proto__属性并把prototype赋值给它)

     

    下图为原型链运行图解, 当我们实例一个对象之后, 调用它的内部方法, 他的运行顺序是先找自身有没有该方法, 如果没有就会通过__proto__属性想上层寻找, 一直寻找到Object对象中, 如果还没有才会报错null

     

    p._proto_----->Persion._proto_---->object._proto_----->null

     

    3. 设计模式

     

    (1)工厂模式: 在函数内创建一个对象, 给对象赋予属性及方法再将对象返回。

     

       function Parent(){

     

    var Child = new Object();

    Child.name = "ZXJ";

    Child.age = "24";

     

         Child.sex=function(){

     

    return "女";

     

         };

     

    return Child;

    };

     

    调用:

    var x = Parent();

     

       alert(x.name); //ZXJ

       alert(x.sex()); //女

     

    (2)构造函数模式:无需再函数内部重建创建对象, 而使用this指代

     

         function Parent(){

     

    var Child = new Object();

    this.name = "ZXJ";

    this.age = 24 ";

    this.lev = lev;

     

          Child.sex=function(){

     

    return "女";

     

         };

     

    return Child;

    };

     

    调用:

    var x = new Parent();

     

       alert(x.name); //ZXJ

       alert(x.sex()); //女

     

      (3)原型模式: 函数中不对属性进行定义, 利用prototype属性对属性进行定义, 可以让所有对象实例共享它所包含的属性及方法。

     

        function Parent(){ };

     

    Parent.prototype.name = "ZXJ";

    Parent.prototype.age = "24";

    Parent.prototype.sex = function() {

     

            var s="女";

     

            alert(s);

     

          };

     

         调用:

     

    var x = new Parent();

    alert(x.name); //ZXJ

     

       alert(x.sex()); //女

     

      (4)混合模式: 原型模式+构造函数模式。 这种模式中, 构造函数模式用于定义实例属性, 而原型模式用于定义方法和共享属性。

     

         function Parent(){

     

    this.name = "ZXJ";

    this.age = 24;

    };

     

       Parent.prototype.sayname=function(){

     

    return this.name;

     

       };

     

    调用:

     

       var x =new Parent();

       alert(x.sayname()); //ZXJ  

     

      (5)动态原型模式:将所有信息封装在了构造函数中, 而通过构造函数中初始化原型, 这个可以通过判断该方法是否有效而选择是否需要初始化原型。

     

        function Parent(){

     

    this.name = "ZXJ";

    this.age = 24;

    if (typeof Parent._sayname == "undefined") {

    Parent.prototype.sayname = function() {

    return this.name;

    }

    Parent._sayname = true;

    }

     

    };

    调用:

    var x = new Parent();

    alert(x.sayname());

     

    ####原型规则

     

    A. 所有的引用类型(数组、 对象、 函数), 都具有对象特性, 即可自由扩展属性;

      var arr = [];

      arr.a = 1;

    B. 所有的引用类型(数组、 对象、 函数), 都有一个_proto_属性(隐式原型), 属性值是一个普通的对象;

    C. 所有的函数, 都具有一个prototype(显式原型), 属性值也是一个普通对象;

    D. 所有的引用类型(数组、 对象、 函数), 其隐式原型指向其构造函数的显式原型; (obj._proto_ === Object.prototype);

    E. 当试图得到一个对象的某个属性时, 如果这个对象本身没有这个属性, 那么会去它的_proto_(即它的构造函数的prototype)中去寻找;

     

    ###2.instanceof的底层实现原理, 手动实现一个instanceof

     

    ####instanceof底层是如何工作的:

     

    function instance_of(L, R) {//L 表示左表达式, R 表示右表达式

     

    var O = R.prototype; // 取 R 的显示原型

     

    L = L.__proto__; // 取 L 的隐式原型

     

    while (true) {

     

    if (L === null)

     

    return false;

     

    if (O === L) // 当 O 显式原型 严格等于 L隐式原型 时,返回true

     

    return true;

     

    L = L.__proto__;

     

    }

     

    }

     

    ####手动实现一个instanceof (用到constructor)

     

    function instance_of(L, R) {//L 表示左表达式, R 表示右表达式

    var O = R;

    L = L.__proto__;

    while (true) {

    if (L === null)

    return false;

    if (O === L.constructor) // 这里重点: 当 O 严格等于 L 时, 返回 true

    return true;

    L = L.__proto__;

    }

    }

    // 开始测试

    var a = []

    var b = {}

     

    function Foo(){}

    var c = new Foo()

     

    function child(){}

    function father(){}

    child.prototype = new father()

    var d = new child()

     

    console.log(instance_of(a, Array)) // true

    console.log(instance_of(b, Object)) // true

    console.log(instance_of(b, Array)) // false

    console.log(instance_of(a, Object)) // true

    console.log(instance_of(c, Foo)) // true

    console.log(instance_of(d, child)) // false 这里就是无法用于判断继承的

    console.log(instance_of(d, father)) // true

     

    ####4. 实现继承的几种方式以及他们的优缺点

    见: 理解原型设计模式以及JavaScript中的原型规则

    ####优缺点:

     

    1. 原型链继承 -》缺点: 构造函数原型上的属性在所有该构造函数构造的实例上是共享的, 即属性没有私有化, 原型上属性的改变会作用到所有的实例上。

    2. 构造函数继承 -》优缺点: 实现了属性的私有化, 但是子类无法访问父类原型上的属性。

    3. 组合继承 -》 利用构造函数和原型链的方法, 可以比较完美的实现继承

     

    function Super(){

     

    this.flag = true;

     

    }

    Super.prototype.getFlag = function(){

     

    return this.flag; //继承方法

     

    }

    function Sub(){

     

    this.subFlag = flase

    Super.call(this) //继承属性

     

    }

    Sub.prototype = new Super;

    var obj = new Sub();

    Sub.prototype.constructor = Sub;

    Super.prototype.getSubFlag = function(){

     

    return this.flag;

     

    }

    注:

     

    这里还有个小问题, Sub.prototype = new Super; 会导致Sub.prototype的constructor指向Super; 然而constructor的定义是要指向原型属性对应的构造函数的, Sub.prototype是Sub构造函数的原型, 所以应该添加一句纠正: Sub.prototype.constructor = Sub;

     

    4. 寄生继承 -》 即将sub.prototype=new super改为sub.prototype=Object.creat(supper.prototype), 避免了组合继承中构造函数调用了两次的弊端。

     

    ###5. 至少说出一种开源项目(如Node)中应用原型继承的案例

    不理解

     

    ###6. 可以描述new一个对象的详细过程, 手动实现一个new操作符

     

    ####述new一个对象的详细过程:

     

    使用new关键字调用函数(new ClassA(…))的具体步骤:

     

    1. 创建空对象;

     

      var obj = {};

     

    2. 设置新对象的constructor属性为构造函数的名称, 设置新对象的__proto__属性指向构造函数的prototype对象;

     

      obj.__proto__ = ClassA.prototype;

     

    3. 使用新对象调用函数, 函数中的this被指向新实例对象:

     

      ClassA.call(obj);   //{}. 构造函数();

     

    4. 将初始化完毕的新对象地址, 保存到等号左边的变量中

     

    注意: 若构造函数中返回this或返回值是基本类型(number、 string、 boolean、 null、 undefined)的值, 则返回新实例对象; 若返回值是引用类型的值, 则实际返回值为这个引用类型。

     

    var foo = "bar";

    function test () {

      this.foo = "foo";

    }

    new test();         //test中的this指新对象, 并未改变全局的foo属性

    console.log(this.foo); // "bar"

    console.log(new test().foo); // "foo";

     

    ####手动实现一个new操作符:

     

    第一种方法:

     

    function new1(func) {

     

    // 创建一个继承自func.prototype的新对象

    var newObj = Object.create(func.prototype);

     

    //截取new1函数第二个以及第二个之后的参数,在newObj作用域内执行改造函数func

    var returnObj = func.apply(newObj, Array.prototype.slice.call(arguments, 1));

     

    //如果传入参数中的构造函数执行后的returnObj是“对象”类型(比如new1(Object)),那么这个对象会取代newObj作为返回的对象

    if ((typeof returnObj === "object" || typeof returnObj === "function") && ret !== null) {

    return returnObj;

    }

     

    return newObj;

    }

     

    第二种方法:

     

    function new2(func) {

     

    return function() {

     

    // 新生成一个对象,且新对象的原型对象继承自构造对象的原型对象

    let newObj = {

    __proto__: func.prototype

    }

     

    // 以第二次执行函数的参数,在obj作用域中执行func

    var returnObj = func.apply(obj, arguments)

     

    //同理,returnObj是“对象”类型(比如new1(Object)),那么这个对象会取代newObj作为返回的对象

    if ((typeof returnObj === "object" || typeof returnObj === "function") && returnObj !== null) {

    return returnObj;

    }

     

    return newObj

    }

    }

     

    第三种方法:

     

    function objectFactory() {

     

    // 通过arguments类数组打印出的结果,我们可以看到其中包含了构造函数以及我们调用objectfactory时传入的其他参数

    // 由于arguments是类数组,没有直接的方法可以供其使用,我们可以有以下两种方法:

    // 1. Array.from(arguments).shift(); //转换成数组 使用数组的方法shift将第一项弹出

    // 2.[].shift().call(arguments); // 通过call() 让arguments能够借用shift方法

     

    let Constructor = [].shift.call(arguments);

     

    const obj = new Object();

     

    obj.__proto__ = Conctructor.prototype;

     

    // 接下来的想法 给obj这个新生对象的原型指向它的构造函数的原型

    // 给构造函数传入属性,注意:构造函数的this属性

    // 参数传进Constructor对obj的属性赋值,this要指向obj对象

    // 在Coustructor内部手动指定函数执行时的this 使用call、apply实现

    Constructor.call(obj, ...arguments);

     

    return obj;

    }

     

    ###7. 理解es6 class构造以及继承的底层实现原理

     

    ####class构造:

    Class本质上是一个特别的函数

    Class 在语法上更加贴合面向对象的写法

    Class 实现继承更加易读、 易理解

    更易于写 java 等后端语言的使用

    本质还是语法糖, 使用 prototype

     

    es5:

    function Fun(a, b) {

     

    this.a = a;

    this.b = b;

     

    }

    Fun.prototype.showA = function () {

     

    console.log(this.a)

     

    }

    var fun = new Fun(1, 2);

    fun.showA(); //1

     

    es6则引用了class的概念, 使得更接近java、 c++等语言, 更加直观。 如:

    class Fun {

     

    constructor(a, b) {

    this.a = a;

    this.b = b;

    }

    showA() {

    console.log(this.a);

    }

     

    }

    var fun = new Fun(1, 2);

    fun.showA(); //1

     

    这两种写法是一样的, 在es6中, class可以理解为一个语法糖, 只是让这种写法更加直观。

    要注意的是, es6中声明新的实例必须要用new声明。

    其中constructor为类的默认方法, 通过new的调用可以执行这个方法。 每个类都必须要有这个方法, 如果没有显示定义,

    则一个空的constructor被添加到类里面。 constructor方法默认返回实例对象, 即this。 也可以返回其他对象。 这事, 新的实例instanceof当前class就会报错。

     

    ####class继承的底层实现原理:

     

    下面我们借助babel来探究es6类和继承的实现原理。

     

    #####1. 类的实现

     

    转换前:

     

    class Parent {

    constructor(a){

     

    this.filed1 = a;

     

    }

    filed2 = 2;

    func1 = function(){}

    }

     

    转换后:

    function _classCallCheck(instance, Constructor) {

    if (!(instance instanceof Constructor)) {

     

    throw new TypeError("Cannot call a class as a function");

     

    }

    }

     

    var Parent = function Parent(a) {

    _classCallCheck(this, Parent);

     

    this.filed2 = 2;

     

    this.func1 = function () { };

     

    this.filed1 = a;

    };

     

    解析:

     

    1. 调用_classCallCheck方法判断当前函数调用前是否有new关键字。

     

    构造函数执行前有new关键字, 会在构造函数内部创建一个空对象, 将构造函数的proptype指向这个空对象的_proto_, 并将this指向这个空对象。

    如上, _classCallCheck中: this instanceof Parent 返回true。

    若构造函数前面没有new则构造函数的proptype不会不出现在this的原型链上, 返回false。

     

    2. 将class内部的变量和函数赋给this。

     

    3. 执行constuctor内部的逻辑。

     

    4.return this (构造函数默认在最后我们做了)。

     

    ######2. 继承实现

    转换前:

    class Child extends Parent {

     

    constructor(a, b) {

    super(a);

    this.filed3 = b;

    }

     

    filed4 = 1;

    func2 = function(){}

    }

     

    转换后:

     

    function _inherits(subClass, superClass) {

     

    // (1) 校验父构造函数。

    // (2) 典型的寄生继承:用父类构造函数的proptype创建一个空对象,并将这个对象指向子类构造函数的proptype。

    // (3) 将父构造函数指向子构造函数的_proto_(这步是做什么的不太明确,感觉没什么意义。)

     

    if (typeof superClass !== "function" && superClass !== null) {

     

    throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);

     

    }

    subClass.prototype = Object.create(superClass && superClass.prototype, {

     

    constructor: {

    value: subClass,

    enumerable: false,

    writable: true,

    configurable: true

    }

     

    });

    if (superClass){

     

    Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;

     

    }

    }

     

    function _possibleConstructorReturn(self, call) {

    // 这里的Child.proto || Object.getPrototypeOf(Child)实际上是父构造函数(_inherits最后的操作),

    // 然后通过call将其调用方改为当前this, 并传递参数。 (这里感觉可以直接用参数传过来的Parent)

    // 校验this是否被初始化, super是否调用, 并返回父类已经赋值完的this。

    if (!self) {

     

    throw new ReferenceError("this hasn't been initialised - super() hasn't been called");

     

    }

    return call && (typeof call === "object" || typeof call === "function") ? call : self;

    }

     

    var Child = function (_Parent) {

    _inherits(Child, _Parent);

     

    function Child(a, b) {

     

    _classCallCheck(this, Child);

     

    var _this = _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, a));

     

    _this.filed4 = 1;

     

    _this.func2 = function() {};

     

    _this.filed3 = b;

    return _this;

     

    }

     

    return Child;

    }(Parent);

     

    以上代码做了:

     

    1. 调用_inherits函数继承父类的proptype。

    2. 用一个闭包保存父类引用, 在闭包内部做子类构造逻辑。

    3. new检查。

    4. 用当前this调用父类构造函数。

    5. 将行子类class内部的变量和函数赋给this。

    6. 执行子类constuctor内部的逻辑。

     

    可见, es6实际上是为我们提供了一个“组合寄生继承”的简单写法。

     

    最新回复(0)