关于前端:JavaScript基础之混合对象类

4次阅读

共计 11517 个字符,预计需要花费 29 分钟才能阅读完成。

本文是我学习《你所不晓得的 javaScript 上卷》的读书笔记的整顿。

更多具体内容,请 微信搜寻“前端爱好者 戳我 查看

混合对象 —“类”

面向类的设计模式:

  • 实例化(instantiation)
  • 继承(inheritance)
  • (绝对)多态(polymorphism)

1. 什么是“类“

类 / 继承形容了一种代码的组织构造模式——一种在软件中对真实世界中问题畛域的“建模办法”

面向对象编程强调的是 数据和操作数据的行为实质上是相互关联的(当然,不同的数据有不同的行为)。

数据结构 — 把数据以及和它相干的行为打包(或者说封装)起来。

所有字符串都是 String 类的一个实例,也就是说它是一个包裹,蕴含字符数据和咱们能够利用在数据上的函数。

次要概念:

  • — ECMAScript 2015 中引入的 JavaScript 类(classes) 本质上是 JavaScript 现有的基于原型的继承的语法糖。

    类语法不是向 JavaScript 引入一个新的面向对象的继承模型。

    JavaScript 类提供了一个更简略和更清晰的语法来创建对象并解决继承。

  • 继承 — 继承的对象函数并不是通过复制而来,而是通过原型链继承(通常被称为 原型式继承 —— prototypal inheritance)。
  • 实例化 — 实例化一个对象 就是为对象开拓内存空间,或者是不必申明,间接应用, 用某个类创立一个对象,就叫对象实例化。

     function test(num){this.num = num;}  
     var number = new test(123);  
  • 多态 — 父类的通用行为能够被子类用更非凡的行为重写。

实际上,绝对多态性容许咱们从重写行为中援用根底行为。

类实践强烈建议父类和子类应用雷同的办法名来示意特定的行为,从而让子类重写父类。

咱们来看一个常见的例子:

“汽车”能够被看作“交通工具”的一种特例,后者是更宽泛的

咱们能够在软件中定义一个 Vehicle  和一个 Car  来对这种关系进行 建模

Vehicle 的定义可能蕴含推进器(比方引擎)、载人能力等等,这些都是 Vehicle 的行为。咱们在 Vehicle 中定义的是(简直)所有类型的交通工具(飞机、火车和汽车)都蕴含的货色。

在咱们的软件中,对不同的交通工具反复定义“载人能力”是没有意义的。相同,咱们只在 Vehicle 中定义一次,定义 Car 时,只有申明它 继承(或者扩大)了 Vehicle 的这个根底定义就行。Car 的定义就是对通用 Vehicle 定义的特殊化。

尽管 Vehicle 和 Car 会定义雷同的办法,然而 实例 中的数据可能是不同的,比方每辆车举世无双的 VIN(Vehicle Identification Number,车辆辨认号码),等等。

这就是类、继承和实例化。

1.1“类”设计模式

面向对象设计模式:

  • 迭代器模式、
  • 观察者模式、
  • 工厂模式、
  • 单例模式,
  • 等等

你可能素来没把类作为设计模式来对待

类并不是必须的编程根底,而是一种可选的代码形象。

1.2 JavaScript 中的“类”

有些语言(比方 Java)并不会给你抉择的机会,类并不是可选的——万物皆是类。

其余语言(比方 C/C++ 或者 PHP)会提供过程化和面向类这两种语法,开发者能够抉择其中一种格调或者混用两种格调。

JavaScript 属于哪一类呢?

在相当长的一段时间里,JavaScript 只有一些近似类的语法元素
(比方 new 和 instanceof),不过在起初的 ES6 中新增了一些元素,比方 class 关键字。

这是不是意味着 JavaScript 中实际上有类呢?简略来说:不是。

JavaScript 中实际上没有类。

JavaScript 的机制其实和类齐全不同

在软件设计中类是一种可选的模式,你须要本人决定是否在 JavaScript 中应用它。

2.    类的机制

2.1    建造

“类”和“实例”的概念来源于屋宇建造。

一个类就是一张蓝图。

为了取得真正能够交互的对象,咱们必须依照类来 建造 (也能够说 实例化 )一个货色,这个货色通常被称为 实例

有需要的话,咱们能够间接在实例上调用办法并拜访其所有私有数据属性。

这个对象就是类中形容的所有个性的一份正本。

构造函数

2.2 构造函数

类实例是由一个非凡的类办法结构的,这个办法名通常和类名雷同,被称为构造函数。

这个办法的工作就是初始化实例须要的所有信息(状态)。

举例来说,思考上面这个对于类的伪代码(假造进去的语法):

class CoolGuy {  
  specialTrick = nothing  
  
  CoolGuy(trick) {specialTrick = trick}  
  
  showOff() {output( "Here's my trick: ", specialTrick)  
  }  
}  

咱们能够调用类构造函数来生成一个 CoolGuy 实例:

1Joe = new CoolGuy("jumping rope")  
2Joe.showOff() // 这是我的绝技:跳绳  

留神:

  • CoolGuy 类有一个 CoolGuy() 构造函数,执行 new CoolGuy() 时实际上调用的就是它。
  • 构造函数会返回一个对象(也就是类的一个实例),之后咱们能够在这个对象上调用
    showOff() 办法,来输入指定 CoolGuy 的专长。

总结:

  • 类构造函数属于类,而且通常和类同名。
  • 构造函数大多须要用 new 来调用

3. 类的继承

定义好一个子类之后,绝对于父类来说它就是一个独立并且齐全不同的类。

子类会蕴含父类行为的原始正本,然而也能够重写所有继承的行为甚至定义新行为。

十分重要的一点是,咱们探讨的父类和子类 并不是实例

父类和子类的比喻容易造成一些误会,实际上咱们该当把父类和子类称为父类 DNA 和子类 DNA。咱们须要依据这些 DNA 来创立(或者说实例化)一个人,而后能力和他进行沟通。

思考上面对于类继承的伪代码

 class Vehicle {  
   engines = 1   
   ignition() {output( "Turning on my engine.");  
   }   
  drive() {ignition();  
     output("Steering and moving forward!")  
   }  
} 

class Car inherits Vehicle {  
  wheels = 4  
  drive() {inherited:drive()  
    output("Rolling on all", wheels, "wheels!")  
  }  
}  

class SpeedBoat inherits Vehicle {  
  engines = 2  
  ignition() {output( "Turning on my", engines, "engines.")  
  }  
  pilot() {inherited:drive()  
    output("Speeding through the water with ease!")  
  }  
}

3.1 多态

Car 重写了继承自父类的 drive() 办法,然而之后 Car 调用了 inherited:drive() 办法,这表明 Car 能够援用继承来的原始 drive() 办法。

快艇的 pilot() 办法同样援用了原始 drive() 办法。

这个技术被称为多态或者虚构多态。

在许多语言中能够应用 super 来代替本例中的 inherited:,
它的含意是“超类”
(superclass),示意以后类的父类 / 先人类。

多态:

  • 任何办法都能够 援用继承 档次中高层的办法(无论高层的办法名和以后办法名是否雷同)。
  • 在继承链的不同档次中一个办法名能够被 屡次定义,当调用办法时会主动抉择适合的定义。

super

在传统的面向类的语言中 super 还有一个性能,就是从子类的构造函数中通过 super 能够间接调用父类的构造函数。

通常来说这没什么问题,因为对于 真正的类 来说,构造函数是属于类的。

在 JavaScript 中恰好相反——实际上“类”是属于构造函数的(相似 Foo.prototype… 这样的类型援用)。

因为 JavaScript 中父类和子类的关系只存在于两者构造函数对应的 .prototype 对象中,
 因而 它们的构造函数之间并不存在间接分割,从而无奈简略地实现两者的绝对援用(在 ES6 的类中能够通过 super 来“解决”这个问题)。

在子类(而不是它们创立的实例对象!)中也能够绝对援用它继承的父类,这种绝对援用通常被称为 super。

详见下图:

留神这些实例(a1、a2、b1 和 b2)和继承(Bar),箭头示意复制操作。

从概念上来说,子类 Bar 该当能够通过绝对多态援用(或者说 super)来拜访父类 Foo 中的行为。

须要留神,子类失去的仅仅是继承自父类行为的一份正本。

子类对继承到的一个办法进行“重写”,不会影响父类中的办法,这两个办法互不影响,

因而能力应用绝对多态援用拜访父类中的办法(如果重写会影响父类的办法,那重写之后父类中的原始办法就不存在了,天然也无奈援用)。

多态并不示意子类和父类有关联,子类失去的只是父类的一份正本。

类的继承其实就是复制。(javaScript 中不存在类)

3.2 多重继承

有些面向类的语言容许你继承多个“父类”。

多重继承意味着所有父类的定义都会被复制到子类中。

钻石问题

在钻石问题中,子类 D 继承自两个父类(B 和 C),这两个父类都继承自 A。如果 A 中有 drive() 办法并且 B 和 C 都重写了这个办法(多态),那当 D 援用 drive() 时该当抉择哪个版本呢(B:drive() 还是 C:drive())?

这些问题远比看上去要简单得多。

相比之下,JavaScript 要简略得多:

JavaScript 自身并不提供“多重继承”性能。

4.    混入

在继承或者实例化时,JavaScript 的对象机制 并不会主动执行复制行为

JavaScript 中只有对象,并不存在能够被实例化的“类”。

一个对象并不会被复制到其余对象,它们会被 关联 起来

因为在其余语言中类体现进去的都是复制行为,因而 JavaScript 开发者也想出了一个办法来模仿类的复制行为,这个办法就是 混入

次要两种类型的混入:显式和隐式。

4.1    显式混入

咱们来回顾一下之前提到的 Vehicle 和 Car。

因为 JavaScript 不会主动实现 Vehicle 到 Car 的复制行为,所以咱们须要 手动实现复制性能

这个性能在许多库和框架中被称为extend(..),然而为了不便了解咱们称之为 mixin(..)

 // 非常简单的 mixin(..) 例子 :  
  function mixin(sourceObj, targetObj) {for (var key in sourceObj) {  
     // 只会在不存在的状况下复制  
     if (!(key in targetObj)) {targetObj[key] = sourceObj[key];  
    }  
   }  
   return targetObj;  
}  
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!");  
  }  
}
);

一点须要留神,咱们解决的曾经不再是类了,因为在 JavaScript 中不存在类,Vehicle 和 Car 都是对象,供咱们别离进行复制和粘贴。

当初 Car 中就有了一份 Vehicle 属性和函数的正本了。

从技术角度来说,函数实际上没有被复制,复制的是函数援用。

所以,Car 中的属性 ignition 只是从 Vehicle 中复制过去的对于 ignition() 函数的援用。相同,属性 engines 就是间接从 Vehicle 中复制了值 1。

Car 曾经有了 drive 属性(函数),所以这个属性援用并没有被 mixin 重写,从而保留了 Car 中定义的同名属性,实现了“子类”对“父类”属性的重写(参见 mixin(..) 例子中的 if 语句)。

4.1.1 再说多态

咱们来剖析一下这条语句:Vehicle.drive.call(this)。这就是我所说的 显式多态

还记得吗,在之前的伪代码中对应的语句是 inherited:drive(),咱们称之为 绝对多态

JavaScript(在 ES6 之前)并没有绝对多态的机制。所以,因为 Car 和 Vehicle 中都有 drive() 函数,为了指明调用对象,咱们必须应用相对(而不是绝对)援用。咱们通过名称显式指定 Vehicle 对象并调用它的 drive() 函数。

然而如果间接执行 Vehicle.drive(),函数调用中的 this 会被绑定到 Vehicle 对象而不是 Car 对象,这并不是咱们想要的。

因而,咱们会应用 .call(this) 来确保 drive() 在 Car 对象的上下文中执行。

应用伪多态通常会导致代码变得更加简单、难以浏览并且难以保护,
因而该当尽量避免应用显式伪多态,因为这样做往往得失相当。

4.1.2 混合复制

回顾一下之前提到的 mixin(..) 函数:

 // 非常简单的 mixin(..) 例子 :  
 function mixin(sourceObj, targetObj) {for (var key in sourceObj) {  
     // 只会在不存在的状况下复制  
     if (!(key in targetObj)) {targetObj[key] =   sourceObj[key];  
     }  
   }  
   return targetObj;  
}

mixin(..) 的工作原理:

它会遍历 sourceObj(本例中是 Vehicle)的属性,如果在 targetObj(本例中是 Car)没有这个属性就会进行复制。

因为咱们是在指标对象初始化之后才进行复制,因而肯定要小心不要笼罩指标对象的原有属性。

复制操作实现后,Car 就和 Vehicle 拆散了,向 Car 中增加属性不会影响 Vehicle,反之亦然。

因为两个对象援用的是同一个函数,因而这种复制(或者说混入)实际上并不能齐全模仿面向类的语言中的复制。

JavaScript 中的函数无奈(用规范、牢靠的办法)真正地复制,所以你只能复制对共享函数对象的援用(函数就是对象)。

如果你批改了共享的函数对象(比方 ignition()),比方增加了一个属性,那 Vehicle 和 Car 都会受到影响。

显式混入是 JavaScript 中一个很棒的机制,不过它的性能也没有看起来那么弱小。尽管它能够把一个对象的属性复制到另一个对象中,然而这其实并不能带来太多的益处,无非就是少几条定义语句,而且还会带来咱们方才提到的函数对象援用问题。

肯定要留神,只在可能进步代码可读性的前提下应用显式混入,防止应用减少代码了解难度或者让对象关系更加简单的模式。

如果应用混入时感觉越来越艰难,那或者你应该停止使用它了。实际上,如果你必须应用一个简单的库或者函数来实现这些细节,那就标记着你的办法是有问题的或者是不必要的。

4.1.3 寄生继承

显式混入模式的一种变体被称为“寄生继承”,它既是显式的又是隐式的。

 //“传统的 JavaScript 类”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 进行定制  
   car.wheels = 4;  
   // 保留到 Vehicle::drive() 的非凡援用  
   var vehDrive = car.drive;  
   // 重写 Vehicle::drive()  
  car.drive = function() {vehDrive.call( this);  
    console.log("Rolling on all" + this.wheels + "wheels!");  
  return car;  
}  
var myCar = new Car();  
myCar.drive();  
// 动员引擎。// 手握方向盘!// 全速前进!

首先咱们复制一份 Vehicle 父类(对象)的定义,而后混入子类(对象)的定义(如果需要的话保留到父类的非凡援用),而后用这个复合对象构建实例。

4.2    隐式混入

隐式混入和之前提到的显式伪多态很像,因而也具备同样的问题。

 var Something = {cool: function() {  
     this.greeting = "Hello World";  
     this.count = this.count ? this.count + 1 : 1;  
   }  
 };  
 
 Something.cool();  
 Something.greeting; // "Hello World"  
 Something.count; // 1  
var Another = {cool: function() {  
    // 隐式把 Something 混入 Another  
     Something.cool.call(this);  
  }  
};  

Another.cool();  
Another.greeting; // "Hello World"  
Another.count; // 1(count 不是共享状态)

通过在结构函数调用或者办法调用中应用 Something.cool.call(this),咱们实际上“借用”了函数 Something.cool() 并在 Another 的上下文中调用了它(通过 this 绑定)。最终的后果是 Something.cool() 中的赋值操作都会利用在 Another 对象上而不是 Something 对象上。

5.    小结

  • 类是一种设计模式。
  • 许多语言提供了对于面向类软件设计的原生语法。JavaScript 也有相似的语法,然而和其余语言中的类齐全不同。
  • 类意味着复制。– 传统的类被实例化时,它的行为会被复制到实例中。类被继承时,行为也会被复制到子类中。
  • JavaScript 并不会(像类那样)主动创建对象的正本。
  • 多态(在继承链的不同档次名称雷同然而性能不同的函数)看起来仿佛是从子类援用父类,然而实质上援用的其实是复制的后果。
  • 混入模式(无论显式还是隐式)能够用来模仿类的复制行为,然而通常会产生俊俏并且软弱的语法,比方显式伪多态(OtherObj.methodName.call(this, …)),这会让代码更加难懂并且难以保护。

Javascript 定义类(class)的三种办法

将近 20 年前,Javascript 诞生的时候,只是一种简略的网页脚本语言。现在,它变得简直无所不能,从前端到后端,有着各种匪夷所思的用处。程序员用它实现越来越宏大的我的项目。

Javascript 代码的复杂度也直线回升。单个网页蕴含 10000 行 Javascript 代码,早就司空见惯。2010 年,一个工程师走漏,Gmail 的代码长度是 443000 行!

编写和保护如此简单的代码,必须应用 模块化策略

目前,业界的支流做法是采纳 ”面向对象编程“。

因而,Javascript 如何实现面向对象编程,就成了一个热门课题。

麻烦的是:

Javascipt 语法不反对 ” 类 ”(class),

导致传统的面向对象编程办法无奈间接应用。

程序员们做了很多摸索,钻研如何用 Javascript 模仿 ” 类 ”。

本文总结了 Javascript 定义 ” 类 ” 的三种办法,探讨了每种办法的特点。

1. 构造函数法

这是经典办法,它用构造函数模仿 ” 类 ”,在其外部用 this 关键字指代实例对象。

function Cat() {this.name = "大毛";}  

生成实例的时候,应用 new 关键字。

var cat1 = new Cat();  
alert(cat1.name); // 大毛  

类的属性和办法,还能够定义在构造函数的 prototype 对象之上。

Cat.prototype.makeSound = function(){alert("喵喵喵");  
}  

毛病: — 比较复杂,用到了 this 和 prototype,编写和浏览都很费劲。

2. Object.create()法

Javascript 的国际标准 ECMAScript 第五版(ES5),提出了一个新的办法Object.create()

用这个办法,” 类 ” 就是一个对象,不是函数。

var Cat = {  
  name: "大毛",  
  makeSound: function(){alert("喵喵喵");  
  }  
};  

而后,间接用 Object.create()生成实例,不须要用到 new。

var cat1 = Object.create(Cat);  
alert(cat1.name); // 大毛  
cat1.makeSound(); // 喵喵喵  

目前,各大浏览器的最新版本(包含 IE9)都部署了这个办法。如果遇到老式浏览器,能够用上面的代码自行部署。

if (!Object.create) {Object.create = function (o) {function F() {}  
    F.prototype = o;  
    return new F();};  
}  

长处:

  • 比 ” 构造函数法 ” 简略

毛病:

  • 能实现公有属性和公有办法
  • 实例对象之间也不能共享数据,对 ” 类 ” 的模仿不够全面。

3. 极简主义法

3.1 封装

这种办法不应用 this 和 prototype,代码部署起来非常简单,这大略也是它被叫做 ” 极简主义法 ” 的起因。

首先,它也是用一个对象模仿 ” 类 ”。在这个类外面,定义一个构造函数 createNew(),用来生成实例。

var Cat = {createNew: function(){// some code here}  
};  

而后,在 createNew()外面,定义一个实例对象,把这个实例对象作为返回值。

 var Cat = {createNew: function(){var cat = {};  
     cat.name = "大毛";  
     cat.makeSound = function(){alert("喵喵喵");  
     };  
     return cat;  
   }  
};

应用的时候,调用 createNew()办法,就能够失去实例对象。

var cat1 = Cat.createNew();  
cat1.makeSound(); // 喵喵喵  

这种办法的益处是,容易了解,构造清晰优雅,合乎传统的 ” 面向对象编程 ” 的结构,因而能够不便地部署上面的个性。

3.2 继承

让一个类继承另一个类: 只有在前者的 createNew()办法中,调用后者的 createNew()办法即可。

先定义一个 Animal 类。

var Animal = {createNew: function(){var animal = {};  
      animal.sleep = function(){alert("睡懒觉");  
      };  
      return animal;  
    }  
};  

而后,在 Cat 的 createNew()办法中,调用 Animal 的 createNew()办法。

 var Cat = {createNew: function(){var cat = Animal.createNew();  
     cat.name = "大毛";  
     cat.makeSound = function(){alert("喵喵喵");  
     };  
     return cat;  
   }  
};

这样失去的 Cat 实例,就会同时继承 Cat 类和 Animal 类。

var cat1 = Cat.createNew();  
cat1.sleep(); // 睡懒觉  
3.3 公有属性和公有办法

在 createNew()办法中,只有不是定义在 cat 对象上的办法和属性,都是公有的。

 var Cat = {createNew: function(){var cat = {};  
       var sound = "喵喵喵";  
       cat.makeSound = function(){alert(sound);  
       };  
       return cat;  
     }  
};

上例的外部变量 sound,内部无奈读取,只有通过 cat 的私有办法 makeSound()来读取。

var cat1 = Cat.createNew();  
alert(cat1.sound); // undefined  
3.4 数据共享

有时候,咱们须要所有实例对象,可能读写同一项外部数据。

这个时候,只有把这个外部数据,封装在 类对象的外面、createNew()办法的里面 即可。

 var Cat = {  
   sound : "喵喵喵",  
   createNew: function(){var cat = {};  
     cat.makeSound = function(){alert(Cat.sound);  
     };  
     cat.changeSound = function(x){Cat.sound = x;};  
    return cat;  
  }  
};

而后,生成两个实例对象:

var cat1 = Cat.createNew();  
var cat2 = Cat.createNew();  
cat1.makeSound(); // 喵喵喵  

这时,如果有一个实例对象,批改了共享的数据,另一个实例对象也会受到影响。

cat2.changeSound("啦啦啦");  
cat1.makeSound(); // 啦啦啦  

4. 工厂模式

 // 工厂模式    
 function oj(){var lio = new Object();    
     lio.name = 'lio';    
     lio.attr = '男';    
     lio.hobby = function(){var li = document.createElement("p");    
         var txt = document.createTextNode("三妹");    
         li.appendChild(txt);    
        document.body.appendChild(li);    
    };    
    return lio;    
}    
var person = oj();    
//alert(person.name);

5. 原型模式

 function oj3(){//this.name = 'lio';}    
 oj3.prototype.name = 'lio';    
 oj3.prototype.love = function (name) {alert("爱"+name);    
 };    
 var person3=new oj3();  
 // 检测是在实例中还是在原型中    
alert(person3.hasOwnProperty("name"));   //false    
alert(person3.hasOwnProperty("rename")); //false    
person3.love('三妹');

6. 混合模式

 // 混合模式    
 function oj4(age) {    
     this.age = age;    
     this.rename = 'aaaa';    
 };    
 oj4.prototype = {    
     constructor:oj4,    
     name:'lio',    
     age:123,    
    love: function (name) {alert(name+"爱三妹");    
    }    
};    
var person4 = new oj4(18);    
alert(person4.hasOwnProperty("age")); //true    
person4.love('lio');

参考链接

  • http://www.ruanyifeng.com/blog/2012/07/three\_ways\_to\_define\_a\_javascript\_class.html
  • https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Refer…
  • http://blog.csdn.net/theowl/article/details/47361175
  • https://segmentfault.com/q/1010000008041012?_ea=1530468
  • http://bonsaiden.github.io/JavaScript-Garden/zh/
  • 《你所不晓得的 javaScript 上卷》
正文完
 0