JavaScript-9类和模块

JavaScript-9类和模块

概况

  • 通过定义对象的类,让每个对象都共享某些属性
  • 类的属性,用以存放或定义状态,或行为
  • 9.4 类的一个重要特性是“动态可继承”
  • 9.5 检测对象的类的方式,“鸭式辩型”
  • 9.6 实现类的方法
  • 9.7 类的继承
  • 9.8 ECMAScript 2015 中的类
  • 模块

9.1类和原型

  • 在JS中,类的所有实例对象都从同一个原型对象上继承属性。因此,原型对象是类的核心。通常,类的实例还需要进一步的初始化,通常通过定义一个函数来创建并初始化这个对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function range(from,to) {
var r = Object.create(range.methods);
r.from = from;
r.to = to;
return r;
}
range.methods = {
//在不在这个range当中
includes: function (x) { return this.from<=x &&x<=this.to;},

foreach: function (f) {
for(var x = Math.ceil(this.from);x<=this.to;x++) f(x);},
//range起点到终点
toString: function () {return "("+ this.from+"..."+this.to+")";}
}
var r=range(1,3);
r.includes(2); //true
r.foreach(console.log);//1,2
console.log(r); //from:1 to:3
  • 工厂函数方法,没有定义构造函数。
  • 使用关键词new 来调用构造函数,使用new调用构造函数会自动创建一个新对象。因此构造函数本身只需要初始化这个新对象的状态即可
  • 调用构造函数的一个重要特征是,构造函数的protype属性被用作新对象的原型。这意味着通过同一个构造函数创建的所有对象都继承自一个相同的对象,因此他们都是同一个类的成员
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//这是一个构造函数,用以初始化新创建的“范围对象
//注意,这里没有创造一个对象,而是初始化
function Range(from,to) {
this.from = from;
this.to = to;
}
//所有的范围对象都继承自这个对象
//注意,属性的名字必须是prototype
Range.prototype = {
//在不在这个range当中
includes: function (x) { return this.from<=x &&x<=this.to;},

foreach: function (f) {
for(var x = Math.ceil(this.from);x<=this.to;x++) f(x);},
//range起点到终点
toString: function () {return "("+ this.from+"..."+this.to+")";}
};
//这是后一定要+new 关键词,是从range里出来的一个实例
var r =new Range(1,3);
console.log( r.includes(2));
r.foreach(console.log(r));
  • 工厂函数range()转化为构造函数时被重命名为Range()
  • Range()构造函数通过new 关键词调用,而range()工厂函数则不必使用new
  • 在第一段实例代码中的原型为range.methods,过于随意。在第二段示例代码中的原型时Range.prototype,这是一个强制的命名。对Range()构造函数的调用会自动使用
  • Range.prototype作为新Range对象的原型

9.2类和构造函数

构造函数和类的标识

  • 原型对象时类的唯一标识:当且仅当两个对象继承自同一个原型对象时,他们才是属于同一类的额实例

  • 构造函数时类的”外在表现,构造函数的名字通常用作类名

    • 使用instanceof运算符来检测对象是否属于某个类

    • ```javascript
      r instanceof Range // 如果r继承自Range.prototype 返回true

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15

      <img src="./JavaScript-9类和模块/1.png" style="zoom:67%;" />

      #### constructor属性

      + 每个JS函数都自动拥有一个prototype属性。这个属性的值时一个对象,这个对象包含唯一一个不可枚举属性constructor,constructor属性的值是一个函数对象,构造函数的原型中预先定义好的constructor属性,这意味着对象通常继承的constructor均指代他们的构造函数

      ```javascript
      var F = function(){};
      var p = F.prototype;
      var c = p.constructor;
      c===F //true 对于任意函数F.prototype.constructor===F

      var o = new F()
      o.constructor ===F//true constructor属性指代这个类
  • 例子

1
2
3
4
5
6
7
8
Range.prototype = {
//原来创建的这个重写了prototype,所以没有constructor的值
constructor:Range,//显示设置构造函数反向引用
includes: function (x) { return this.from<=x &&x<=this.to;},
foreach: function (f) {
for(var x = Math.ceil(this.from);x<=this.to;x++) f(x);},
toString: function () {return "("+ this.from+"..."+this.to+")";}
};
  • 尽量不用constructor属性

9.3Javascript中Java 式的类继承

  • 用JavaScript模拟出Java中的四种类成员类
  • 四种类型成员
    • 实例字段
      • 他们是基于实例的属性或者变量,用以保存独立对象的状态
    • 实例方法
      • 他们是类的所有实例所共享的方法,由每个独立的实例调用
    • 类字段
      • 这些属性或者变量是属于类的,而不是属于类的某个实例的
    • 类方法
      • 这些方法是属于类的,而不是属于某个实例的
  • Js和Java 的不同之处
  • Js的函数时以值得形式出现的,方法和字段没有太大的区别,如果属性值时函数,那么这个属性就定义了一个方法,否则仅仅时一个普通的属性或者字段,js中由三种不同的对象,如下所示
  • Javascript中类得三种不同的对象
    • 构造函数对象
      • 构造函数为JavaScript得类定义了名字。任何添加到这个构造函数对象中的属性都时类字段和类方法
    • 原型对象
      • 原型对象的属性被类得所有实例所继承’
    • 实例对象
      • 类的每个实例都是一个独立的对象,直接给这个实例定义的属性是不会为所有实例对象所共享的
  • Javascript中定义类
    • 第一步,先定义一个构造函数,并设置初始化新对象的实例属性
    • 第二部,给构造函数的prototype对象定义实例的方法
    • 第三步,给构造函数定义类字段和类属性
      Step1 先定义一个构造函数,并设置初始化新对象的实例属性
1
2
3
4
5
6
7
8
9
10
11
12
/*
这个构造函数为它所创建的每个实例定义了实例字段r和i
这两个字段分别保存附属的实部和虚部
它们是对象的状态
*/
function Complex(real,imaginary)
{
if(isNaN(real)||isNaN(imaginary))
throw new TypeError();
this.r = real;
this.i = imaginary;
}
Step 2给构造函数的prototype对象定义实例的方法
1
2
3
4
5
6
7
8
9
10
11
12
//当前复数对象加上另外一个复数,并返回一个新的计算和值后的附属对象
Complex.prototype.add = function(that){
return new Complex (this.r+that.r,this.i+that.i);
};
//负数的求负运算
Complex.prototyle.neg = function(){
return new complex (-this.r,-this.i);
};
//将复数对象转换为一个字符串
Complex.prototype.toString = function(){
return "{"+this.r +","+this.i+"}"
}
Step3 给构造函数定义类字段和类属性

1
2
3
4
5
6
var c = new Complex(2,3);
var d = new Complex(c.i,c.r);
c.add(d).toString(); //"{5,5}"使用了实例的方法
//这个稍微复杂的表达式用到了类方法和类字段
//complex表示一个类,先{2,3},再定义一个加上{-2,-3}
Complex.prase(c.toString()),add(c.neg()).equals(Complex.ZERO)//结果永远是0

9.4类的扩充

  • 具体可通过给原型对象添加新方法来扩充Javascript类

1
2
3
function printf(){return console.log(this)};
n=3;
n.times(printf,"hello world");

9.5类和类型

  • 三种用以检测任意对象的类的技术
    • instanceof运算符
    • constructor属性
    • 构造函数的名字
  • 鸭式辩型
    • 更关注对象可以完成什么工作(它包含什么方法)而不是对象属于哪个类
instanceof运算符
  • 左操作数是待检测类的对象,有操作时是定义类的构造函数
  • 如果o继承自c.prototype 则表达式o instanceof c的值为true。这里的继承可以不是直接继承
  • 构造函数是类的公共标识,但原型是唯一的标识

    • 尽管instanceof运算符的右操作数是构造函数,但计算过程实际上是检测了对象的继承关系,而不是检测创建对象的构造函数
  • 检测对象的原型链上是否有存在某特定的原型对象,而不用构造函数做中介的方法:isPrototypeOf()

1
range.methods.isPrototypeOf(r);//range.method是原型对象
  • instanceOf()和isPrototypeOf()方法的缺点
    • 无法通过对象获得类名,只能检测对象是否属于指定的类名
    • 客户端Javascript中,多窗口和多框架子页面的web应用中兼容性不佳,每个窗口和框架的子页面都具有单独的执行上下文,每个上下文都包含独有的全局变量和一组构造函数
constructor 属性
1
2
3
4
5
6
7
8
function typeAndValue(x){
if(x===null) return "";
switch(x.constructor){
case Number: return "Number: "+x;
cise String: return "String:'"+x+"'";
......
}
}
  • 不足之处
    • 再多个执行上下文的场景中无法正常工作(比如再浏览器窗口的多个框架子页面中)
    • 并非所有的对象都包含constructor属性。我们常常会忽略原型上的constructor属性。比如本章前面的示例代码中所定义的两个类,它们的实例都没有constructor属性
构造函数的名称
  • 使用instanceof元素安抚和constructor属性检测对象所属的类的主要的问题:
    • 多个执行上下文中的函数看起来是一摸一样的,但是他们是相互独立的对象,因此彼此不相等
  • 一种可能的解决方案,使用构造函数的名字而不是构造函数本身作为类标识符
    • 一些JS实现中,函数对象由非标准的属性name,用来标识函数的名称
    • 对没有name属性的Javascript实现,可将函数转换为字符串然后从中提取出函数名
  • 缺点:

    • 并不是所有的对象都具有constructor属性。此外并不是所有的函数都有名字。如果使用不带名字的函数定义表达式定义一个构造函数,getName()方法则会返回空字符串:
1
2
3
4
//这个构造函数没有名字
var Complex = function(x){this.r = x,this.i = y;}
//这个构造函数有名字
var Range = function Range(f,t){this.from = f;this.to = t;}

鸭式辩型

  • 不关注“对象的类是什么”关注“对象能做什么”
    • 上文所描述的检测对象的类的各种技术多少都会有些问题,至少在客户端Javascript中式如此。解决办法就是规避掉这些问题:不要关注“对象的类是什么”,而是关注“对象能做什么”。因此提出鸭式辩型
  • 像鸭子一样走路,有用并且嘎嘎叫的鸟就是鸭子
    • 哪怕并不是从鸭子类的原型对象继承而来,但认为这个对象是鸭子

9.6 JavaScript面向对象的技术(略去)

9.7子类

概述

  • 在JavaScript中创建子类的关键之处在于,采用合适的方法对原型对象进行初始化
  • 如果类B继承自类A,B.prototype 必须是 A.prototype的后嗣。B的实例继承自B.prototype,后者同样继承自A.prototype

  • 例子

step1 定义Teacher()构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//teacher类继承自Person类
//首先Person类定义如下
function Person(first,last,age,gender,interests){
this.name = {
first,
last
};
this.age = age;
this.gender = gender;
this.interests = interests;
}
//不要重新定义一边属性,否则就不是从Person()中继承的
//会需要更多的代码
function Teacher(first,last,age,gender,interests,subject) {
Person.call(this,first,last,age,gender,interests,subject);
this.subject = subject;
}
step 2 设置Teacher()的原型和构造器引用
  • Teacher.prototype=Object.create(Person.prototype)
  • create()在这个例子里,创建一个和Person.prototype 一样的新的原型属性值,这个属性指向一个包括属性和方法的对象,然后将它作为Teacher.prototype 的属性值,这意味着Teacher.prototype现在会继承Person.prototype的所有属性和方法
  • 现在Teacher()的prototype的constructor属性指向的是Person(),所以我们需要将其 正确设置
  • Teacher.prototype.constructor = Teacher;
step3 尝试向Teacher()添加新的greeeting()函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Teacher.prototype.greeting = function () {
var prefix;
if(this.gender==='male'||this.gender==='Male'||this.gender==='M'||this.gender==='m')
{
prefix = 'Mr.';
}else if(this.gender === 'female'||this.gender ==='Female'||this.gender ==='f'||this.gender ==='F')
{
prefix = 'Mrs.'
}else{
prefix = 'Mx.'
}
alert('Hello ,my name is '+ prefix+''+this.name.last +',and I teache '+ this.subject+'.')

};
step4创建一个Teacher()的实例
1
2
3
4
5
6
var teacher1 = new Teacher('Dave','Griffiths',31,'male',['football','cookery'],'mathematics');
teacher1.name,first;
teacher1.interests[0];

teacher1.subject;
teacher1.greeting();
  • 前面两个进入到Person()的构造器,继承的属性和方法
  • 后面两个则是只有Teacher()的构造器才有的属性和方法

9.8 ES6中的类

定义类-类声明

  • 使用戴悠class关键词的类名
1
2
3
4
5
6
class Rectangle{
constructor(height,width) {
this.height = height;
this.width = width;
}
}
  • 注意:函数声明和类声明之间的一个重要区别是函数声明会提升,但是类不会

定义类类表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//匿名类
let Rectangle = class {
constructor(height,width) {
this.height = height;
this.width = width;
}
};
console.log(Rectangle.name)
//输出实例名字,因为类是匿名的
//具名类
let Rectangle = class Rectangle2 {
constructor(height,width) {
this.height = height;
this.width = width;
}
};
console.log(Rectangle.name);
//后面会把类具体的名字输出,不输出实例名

类体和方法定义

  • 一个类 的类体式{}中的部分。这是定义类成员的位置,如方法或者构造函数
  • 严格模式
    • 类声明和类表达式的主题都执行在严格模式下,比如,构造函数,静态方法,原型方法,getter和setter都在严格模式下执行
  • 构造函数
    • 一个类只能拥有一个名为constructor的特殊方法。这个constructor方法是一个特殊的方法,这个方法用于创建和初始化一个由 class 创建的对象。一个构造函数可以使super关键词来调用一个父类的构造函数‘
原型方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Rectangle{
constructor(height,width) {
this.height = height;
this.width = width;
}
get area(){
return this.calcArea()
}
calcArea(){
return this.height*this.width;
}
}
const square = new Rectangle(10,10)
console.log(square.area)//100
静态方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Point{
constructor(x,y) {
this.x = x;
this.y = y;
}
static distance (a,b)
{
const dx = a.x-b.x;
const dy = a.y-b.y;
return Math.hypot(dx,dy);//返回所有参数的平方和的平方根
}
}
const p1 = new Point(5,5);//两个实例
const p2 = new Point(10,10);
console.log(Point.distance(p1,p2));
//静态方法直接写类名+方法,再传入参数
用原型和静态方法包装
  • 当一个对象调用静态或者原型方法时,如果该对象没有this值,那么this值再被调用的函数内部就是undefined,不会发生自动包装

  • ```javascript
    class Animal{

      speak() {
          return this;
      }
      static eat(){
    

    //这是对类来说的,不是对实例来说的,所以这样定义,this必为undefined

          return this;
      }
    

    }
    let obj = new Animal();
    obj.speak();// 这是一个Animal实例,有this对象,返回Animal{}
    let speak = obj.speak();
    speak();//undefined,找不到this的值

    Animal.eat();//class Animal;
    let eat = Animal.eat();
    eat();//用一个类来调用一个方法,肯定没有this

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    + 当我们使用传统的基于函数的类来编写上述代码,那么基于调用该函数的this值将发生自动装箱
    + 这时候

    ```javascript
    function Animal(){}
    Animal.prototype.speak = function(){
    return this;
    }
    Animal.eat = function(){
    return this;
    }
    let obj = new Animal();
    let speak = obj.speak();
    speak();//global object 逻辑会混乱,建议用新的方法

    let eat = Animal.eat();
    eat();//global object
实例属性
  • 实例的属性必须定义在类的方法里
1
2
3
4
5
6
class Rectangle{
constructor(height,width) {
this.height = height;
this.width = width;
}
}
  • 静态的或者原型的数据属性,必须定义在类定义的外面
1
2
3
4
5
6
//他和实例就完全没关系了,任何一个实例,要用这个静态属性的时候必然都是20
//对每个实例都是一样的,而且实例不能修改这个属性
Rectangle.staticWidth = 20;
//对于每个实例来说都继承了一个原型,那么他也有这个原型的数据属性
//但是可以对这个prototypeWidth修改
Rectangle.prototype.prototypeWidth = 25
字段声明
  • 共有字段声明
    • 通过预先声明字段,字段始终存在
    • 这个字段可以用也可以不用默认值来声明
  • 私有字段声明

    • 只能在类里面读取或者写入
    • 私有字段仅能在字段声明中预先定义
    • 私有字段不能通过在之后赋值来创建他们,这种方式只适合普通属性
  • 共有例子

1
2
3
4
5
6
7
8
class Rectangle{
height = 0;
width ;
constructor(height,width) {
this.height = height;
this.width = width;
}
}
  • 私有例子
1
2
3
4
5
6
7
8
9
class Rectangle{
# height = 0;
# width ;
constructor(height,width) {
this.height = height;
this.width = width;
}
}
//在外面是看不到的

用extends创建子类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Animal{
constructor(name) {
this.name = name;
}
speak(){
console.log(this.name+' makes a noise.');
}
}
class Dog extends Animal
{
//再原型方法的基础上修改speak(),打印....barks
speak() {
console.log(this.name+' barks.');
}
//不对原型方法进行任何修改,打印...makes a noise
speak() {
super.speak();
}
}
var d = new Dog('轲轲');
d.speak();
  • 如果子类中存在构造函数,则需要再使用this之前首先调用super()
  • 也可以继承传统的基于函数的类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
   function Animal(name) {
this.name = name;
}
Animal.prototype.speak= function () {
console.log(this.name+' makes a noise.');
};
//可以继承传统的基于函数的类
class Dog extends Animal
{
speak() {
// super.speak();
console.log(this.name+' barks.');
}
}
var d = new Dog('轲轲');
d.speak();
  • 需要注意的式,类不能继承常规对象。如果要继承常规对象,可以改成Object.setPrototypeOf();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//定义一个简单的对象
var Animal = {
speak(){
console.log(this.name+' makes a noise.');
}
}
//这是一个类
class Dog{
constructor(name) {
this.name = name;
}
}
//显示的指定Dog的prototype就是Animal
Object.setPrototypeOf(Dog.prototype,Animal);
var d = new Dog('轲轲');
d.speak();

模块

  • 任何javascript代码段就可以当作一个模块
  • 模块化的目标
    • 支持大规模的程序开发,处理分散源中代码的组装,并且能够让代码正确运行
    • 不同的模块必须避免修改全局执行上下文,因此后续模块应当在他们所期望运行的原始(或接近原始)上下文中执行

用作命名空间的对象

  • 在模块创建过程中避免污染全局变量的一种方法是使用一个对象作为命名空间
  • 用命名空间来的导入模块(往往将整个模块导入全局命名空间,而不是导入命名空间中的某个单独的类)
    • var sets = com.davidflanagan.collections.sets;
  • 按照约定,模块的文件名应当和命名空间匹配

作为私有命名空间的函数

  • 模块对外导出一些公用API,这些API是提供给其他程序员使用的,它包括函数类属性和方法
  • 例子:
  • 将模块函数当作构造函数使用,通过new来调用,通过将他们赋值来给this将其到处

模块化的几个规范

CommonJS
  • CommonJS的一个模块就是一个脚本文件,通过执行该文件来加载模块。CommonJS规范规定,每个模块内部,module变量代表当前模块,这个变量是一个对象,他的exports属性是对外的接口。加在某个模块其实是加载该模块的module.exports属性
  • 优点:解决了依赖,全局变量污染的问题
  • 缺点:CommonJS用同步的方式加载模块,在浏览器端,限于网络原因,CommonJS不适合浏览器端模块加载,更合理的是使用异步加载,比如AMD
AMD
  • 即异步模块定义
  • 采用这个异步方式加载模块,模块的加载不影响后面语句的运行
  • 所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行,除了和CommonJS同步加载方式不同之外,AMD在模块的定义与引用上也有所不同

  • 在AMD标准中,定义了下面三个API

    • require([module],callback)
      • 又是我们依旧通过require关键字,它包含两个参数,第一个数组为要加载的模块,第二个参数为回调函数
    • define(id,[depends],callback)

      • id:模块名称,或者模块加载器请求的指定脚本的名字
      • depends:是个定义模块所依赖的模块数组,默认为[“require”,”exports”,”module”]
      • callback:为模块初始化要执行的函数或对象。如果为函数,他应该只被执行一次。如果是对象,此对象应该为模块的输出值
    • require.config()

  • 优点:适合在浏览器环境中异步加载模块,并行加载多个模块
  • 不能按需加载
-------------本文结束,感谢您的阅读-------------