Loading

你不知道的JavaScript——this全面解析(下)

混入

JavaScript和传统的面向对象编程语言有很大差异,实际上它没有类,只有对象,那么自然也就不存在继承、多态这些东西。

混入是在JS中实现传统面向对象模式的一个办法。

function mixin(parentObj,childObj){
    for(var key in parentObj){
        if(!(key in childObj))
            childObj[key] = parentObj[key];
    }
    return childObj;
}

如上,混入就是赋予子元素所有在它身上没有的他爹身上的属性,这不就是继承嘛。

看个例子:

var Vehicle = {
    engines: 1,
    ignition: function() {
        console.log("Turning on my engine.");
    },
    drive: function() {
        this.ignition();
        console.log("Steering and moving forward");
    }
};

var Car = mixin(Vehicle,{
    wheels: 4,
    drive: function(){
        Vehicle.drive.call(this);
        console.log("Rolling on all "+this.wheels+" wheels!");
    }
});


Car.drive();


/*
Turning on my engine.
Steering and moving forward
Rolling on all 4 wheels!
*/

分析,Car通过mixin,获得了Vehicleengine属性(值)和ignition方法(引用),而drive则保留自己的,实现了子类重写父类方法,并且子类新增了一个wheels属性。

Vehicle.drive.call(this)就是一个多态模式,因为父类和子类都有drive的定义,这里是使用了父类的drive方法,并将自己作为上下文绑定进去,如果不使用call而是直接调用,那就是在Vehicle对象的上下文中调用。显然对这个示例没什么影响,但是这不是我们所期望的。

寄生继承

寄生继承更像是委托设计模式。

// 定义Vehicle对象
function Vehicle(){
    this.engines = 1;
}

Vehicle.prototype.ignition = function(){
    console.log("Turning on my engine.");
}
Vehicle.prototype.drive = function(){
    this.ignition();
    console.log("Steering and moving forward!");
}

// 定义Car对象
function Car(){
    // Car是一个Vehicle
    var car = new Vehicle();

    // 新增属性
    car.wheels = 4;

    // 保留父类方法
    var vehDrive = car.drive;

    // 重写父类方法
    car.drive = function(){
        // 调用父类方法
        vehDrive.call(this);
        console.log("Rolling on all " + this.wheels + " wheels!");
    }

    // 返回对象
    return car;
}

var myCar = new Car();
myCar.drive();

只是个人觉得这样写挺丑。

原型

前面提到了JS中没有类,只有对象,也介绍了两种用来在JS中描述类之间结构关系的办法,那JS有没有啥自带的办法??

好像每个JS对象都有toString方法,valueOf方法......这是典型的继承操作嘛,所以JS肯定有类似的东西,那就是Prototype——原型。

JS中的每个对象都有一个__proto__属性,它也是一个对象(后文称它proto对象),一般这个对象不会为空,这个对象描述了JS中对象之间的继承关系。

通过Object.create(obj)可以显式的把一个对象obj作为一个新对象的proto对象。

var parent = { a: 10 };
var child = Object.create(parent);

当我们查看这个child对象时,你会清楚的看到parent对象成为了这个对象的proto对象,这时你就可以访问child.a了,当访问一个对象中并不存在的属性时,就会去检查proto对象中是否有相应的属性。

而且child的proto对象中也有一个proto对象,这是再上一级的父对象,这一层一层的proto对象就构成了JS中的原型链,原型链的末尾是Object.prototype,也就是这里定义了toStringvalueOf这些方法。访问属性时会沿着原型链向上查找。当使用for inkey in value时也会检查原型链。

显然这个例子中再上一级的proto对象就是Object.prototype了,因为没有其他的继承关系了。

引用!引用!

注意,原型链并不复制对象,而是直接保存引用。

可以看到,child.__proto__parent根本就是同一个对象。

再看赋值操作

obj.a = 10;
  1. 先检查obj中是否有a这个属性,如果有直接修改赋值。
  2. 检查obj的原型链中是否有a这个属性,如果有且writable == true的话,在obj中创建a属性并赋值
  3. 如果obj的原型链中有这个属性,并且writable == false的话,不会在obj中创建新属性,如果运行在非严格模式,忽略该条语句,如果在严格模式,TypeError
  4. 如果obj的原型链中这个a属性有setter的话,调用setter,忽略是否可写
  5. 都不满足,说明在obj中和它的原型链中都不存在a属性,那么在obj中创建a属性并赋值

我们把它和传统的面向对象做一个类比。

  1. 相当于子类中特有的属性
  2. 相当于父类中有这个属性,但子类要覆盖或重写
  3. 相当于父类中有这个属性,但父类已经为这个属性设定了一个很高的访问级别(即不允许写),那么子类自然无法低于这个级别
  4. 没什么可以类比的,如果非要类比,可以想成,父类对一个属性的实际存储方式已经规划好了,所有的子类必须要遵守这个规定,所以要调用原型链当中的setter
  5. 也相当于子类中特有的属性

隐式屏蔽

var parent = { a: 10 };
var child = Object.create(parent);

child.a++;

console.log(child);
console.log(child.__proto__);

该代码有个坑,child.a应该读取到的是parent中的a属性,那么对它进行自增,应该操作的是parent里面的a,预期的输出结果应该是:

{}
{ a: 11 }

但实际的输出结果是:

{ a: 11 }
{ a: 10 }

因为自增操作实际上是会被转换成如下代码:

child.a = child.a + 1;

所以,根据对象赋值的规则,会在child中新建一个a,并且把11赋给它,而parent中的a并未受影响。

跳出面向对象的模式

上面运用了大量的面向对象的类比来介绍原型链,其实不过是方便理解罢了,但实际上JS中并没有类似继承、类、多态这些概念,它只有对象。同时,本书的作者也对社区中的一些通用的过于面向对象风的术语嗤之以鼻。

再次重申,JS中没有类,只有对象,刚刚你看到的不过是通过一系列对象的组合引用,使用一种类似委托的技术创造出了原型链模式,来方便的在对象间复用逻辑,也就是定义一个对象时,只考虑它特有的属性和方法,其他的属性和方法留空,然后再使用别的对象填满。后面我们会尽量少的使用传统面向对象的术语。

类函数

下面是被滥用了很多年的用JS中的函数模拟传统面向对象的“类”特性的写法,至今还能在一些成熟的框架中看到影子。

function People(name){
    this.name = name;
}

People.prototype.eat = function(){
    console.log(this.name + ",eating...");
}
People.prototype.drink = function(){
    console.log(this.name + ",drinking...");
}


var p1 = new People("张三");
p1.eat();
p1.drink();

前面说了,JS中对象都有一个proto属性,而函数在JS中也是一个特殊的对象,它自然也有proto属性,并且当你使用new关键字时,会返回一个对象(见上一篇博文),JS会自动将这个函数的proto属性作为返回对象的proto属性。所以p1.__proto__ === People.prototype是成立的。

这里已经完全是在模仿传统面向对象中的类的思路了,提供一套模板方法和属性,每当实例化一个对象时,就将这些模板方法和属性复制进去。虽然这里没有发生复制,但是整体的效果已经一致了。

原型继承

你也看到了,在JS中如果你想实现传统面向对象当中的继承,直接让它的proto属性指向要继承的对象就好了。而使用的方法就是Object.create

下面展示的是一个例子:

function Foo(name){
    this.name = name;
}

Foo.prototype.myName = function(){
    return this.name;
}


function Bar(name,label){
    Foo.call(this,name);
    this.label = label;
}

Bar.prototype = Object.create(Foo.prototype);

Bar.prototype.myLabel = function(){
    return this.label;
}

var a = new Bar('a','obj a');

console.log(a.myName()); // a
console.log(a.myLabel()); // obj a

关键代码就是Bar.prototype = Object.create(Foo.prototype)

这行代码执行完毕,Bar默认的proto属性会被替换,替换成如下形式:

Bar.prototype == {
    __proto__: {
        myName: function(){...}
    }
}

相当于它继承了Foo的所有属性和方法,并且我们可以通过Bar.prototype随意添加和覆盖其中的方法。

最终的Bar.prototype是这样

{
    myLabel: function(){...},
    __proto__: {
        myName: function(){...}
    }
}

最终的对象a是这样:

a == {
    label: "obj a",
    name: "a",
    __proto__: {
        myLabel: function(){...},
        __proto__: {
            myName: function(){...}
        }
    }
}

注意,上面的办法和如下两种办法是完全不同的,并且下面的办法不推荐使用:

Bar.prototype = Foo.prototype;

这种办法的问题在于Bar.prototype被直接指向了Foo.prototype,它俩变成了同一个对象,虽然确实能借此访问到父对象中的方法和属性,但是你对Bar.prototype的修改也都会应用到Foo.prototype,也就是说你放弃了所有重写和覆盖父对象中方法和属性的权力。

Bar.prototype = new Foo();

之前说过,new关键字会创建一个新的对象,并把Foo.prototype设置为这个新对象的proto属性并返回,所以a对象实际的结构是这样的:

a == {
    label: "obj a",
    name: "a",
    __proto__: {
        name: undefined, // [+] 多了这行
        myLabel: function(){...},
        __proto__: {
            myName: function(){...}
        }
    }
}

也就是说,Foo函数中对this进行的操作都会反映在原型链中,如果此处有些破坏性的操作也都会反映到最终对象的原型链中。

但是Bar.prototype = Object.create(Foo.prototype)有一个问题就是,原本的Bar.prototype貌似丢失了,被一个新创建的对象替换掉了,虽然也没啥问题,它给垃圾处理器造成的压力几乎可以忽略不计,但是感觉还是不是内回事儿。

ES6中新增了一个方法规避了这个问题。

Object.setPrototypeOf( Bar.prototype, Foo.prototype );

判断是否是父对象

JS中有instanceof关键字,但是它只能判断一个对象和一个函数的关系,实际上它是在判断这个对象的原型链中是否包含这个函数的prototype属性,它无法应对判断一个对象是否是另一个对象的原型(也就是是不是另一个的父对象)。

b.isPrototype(c) // 返回b是否是c的原型

也可以将上面的代码解释为查看b是否在c的原型链中出现过

__proto__

我们几乎用了一整篇这个属性,但是很遗憾,它并非标准的,而且原书中也几乎没用,只是简单的介绍了一下,直到刚刚我才知道它是非标准的。

标准的获取原型对象的办法是Object.getPrototypeOf,设置原型对象的则是Object.setPrototypeOf

__proto__只被部分的浏览器支持,它的实现大致如下:

Object.defineProperty( Object.prototype, "__proto__", {
    get: function() {
        return Object.getPrototypeOf( this );
    },
    set: function(o) {
        // ES6 中的 setPrototypeOf(..)
        Object.setPrototypeOf( this, o );
        return o;
    }
} );

行为委托

现在开始忘掉传统面向对象中那些面向类的模式。

JS中的模式被称为面向委托的模式,在JS中,一个对象不再有一个父类,因为压根就没有类,所以我们再也看不到一个纵深的树状的继承结构了。

在JS中,你如果要描述动物和猫的关系,你要做的不是创建一个Animal,描述动物都有的属性和行为,再创建一个Cat作为Animal的子类,描述一只猫特有的属性和行为。你要做的是创建AnimalCat,分别描述它俩所特有的行为,然后Cat自己干不了活,它要依赖它的哥们儿Animal才能干活,所以要把Animal委托给它。

这是委托和传统面向类的模式思想上的不同。

var Animal = function(typeName){
    return {
        typeName: typeName,
        run: function(){
            console.log(this.typeName+' is running...');
        },
        hunt: function(){
            console.log(this.typeName+' is hunting...');
        }
    }
}
// 委托Animal类
var Cat = Object.create(Animal('猫'));

Cat.catchMouse = function(){
    this.hunt();
    console.log('Finding mouses...');
}

Cat.washFace = function(){
    console.log('The cat is washing her face now.');
}

Cat.catchMouse();
Cat.washFace();

上面的代码就是使用委托风格写的代码,其中你可以看到,Cat提供了一个catchMouse方法,其中委托了Animal的hunt进行处理。在传统的面向对象的中,这里应该使用方法重写,所以catchMouse理应该被改名为hunt,但上面的代码中如果改名了,当执行到this.hunt时就会造成循环引用,不会去找原型链中的hunt,因为此处已经有一个了。这里你就要去想办法找到Animal中的hunt,并通过call绑定上下文调用它,要么就取一个新的名字。

委托风格推荐你取新名字,而不是进行掩盖,然后再手动去找要委托的方法。

我实话实说,上面的代码还有点面向类,我们再改改。

var Animal = {
    setTypeName: function(typeName){
        this.typeName = typeName;
    },
    run: function(){
        console.log(this.typeName+' is running...');
    },
    hunt: function(){
        console.log(this.typeName+' is hunting...');
    }
}

var Cat = Object.create(Animal);
Cat.setTypeName('猫');
Cat.catchMouse = function(){
    this.hunt();
    console.log('Finding mouses...');
}

Cat.washFace = function(){
    console.log('The cat is washing her face now.');
}

Cat.catchMouse();
Cat.washFace()

这里改了Animal,使用对象文法进行声明,提供一个setTypeName,而不是像传统的面向对象中的使用类似构造器的语法传递进去。这和刚刚有啥区别呢?注意,如果你使用Cat.setTypeName,那么这个属性是被设置到Cat上的,而刚刚你使用那种类构造器的语法,那么那个属性会被设置到Animal上,委托模式推荐你使用上面的办法将所有属性设置到委托者身上,而不是委托目标。

至少在JS中,委托模式比面向类的模式要简洁清晰很多,因为JS本来就是为此而设计的,但开发者们非要在这个模式下去模仿传统的面向类设计,那肯定会更加繁杂啊。

posted @ 2021-07-18 16:01  yudoge  阅读(69)  评论(0编辑  收藏  举报