美文网首页HTML5
JavaScript 中面向对象的程序设计

JavaScript 中面向对象的程序设计

作者: Dimen_ | 来源:发表于2016-12-24 21:33 被阅读76次
    面向对象的程序设计

    面向对象程序设计(英语:Object-oriented programming,缩写:OOP)是种具有对象概念的程序编程范型,同时也是一种程序开发的方法。它可能包含数据、属性、代码与方法。对象则指的是类的实例。它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性,对象里的程序可以访问及经常修改对象相关连的数据。在面向对象程序编程里,计算机程序会被设计成彼此相关的对象。
    面向对象程序设计可以看作一种在程序中包含各种独立而又互相调用的对象的思想,这与传统的思想刚好相反:传统的程序设计主张将程序看作一系列函数的集合,或者直接就是一系列对电脑下达的指令。面向对象程序设计中的每一个对象都应该能够接受数据、处理数据并将数据传达给其它对象,因此它们都可以被看作一个小型的“机器”,即对象。目前已经被证实的是,面向对象程序设计推广了程序的灵活性和可维护性,并且在大型项目设计中广为应用。此外,支持者声称面向对象程序设计要比以往的做法更加便于学习,因为它能够让人们更简单地设计并维护程序,使得程序更加便于分析、设计、理解。反对者在某些领域对此予以否认。
    当我们提到面向对象的时候,它不仅指一种程序设计方法。它更多意义上是一种程序开发方式。在这一方面,我们必须了解更多关于面向对象系统分析和面向对象设计(Object Oriented Design,简称OOD)方面的知识。许多流行的编程语言是面向对象的,它们的风格就是会透由对象来创出实例。
    重要的面向对象编程语言包含 Common Lisp 、Python 、C++ 、Objective-C 、Smalltalk 、Delphi 、Java 、Swift 、C# 、Perl 、Ruby 与 PHP 等。
    来自 维基百科

    面向对象的实现方式

    编程语言对对面向对象的实现主流的有两种方式:基于类的面向对象和基于原型的面向对象。

    不管以什么方式实现,都具有面向对象的三大特征:

    封装性(Encapsulation)

    具备封装性的面向对象程序设计隐藏了某一方法的具体运行步骤,取而代之的是通过消息传递机制发送消息给它。封装是通过限制只有特定类的对象可以访问这一特定类的成员,而它们通常利用接口实现消息的传入传出。

    继承性(Inheritance)

    可以让某个类型的对象获得另一个类型的对象的属性的方法。

    多态性(Polymorphism)

    不同实例的相同方法在不同情形有不同表现形式。多态机制使具有不同内部结构的对象可以共享相同的外部接口。、

    JavaScript 中面向对象的程序设计

    JavaScript 中没有类的概念,因此它的对象也与基于类的语言中的对象有所不同。
    ECMA-262 把对象定义为:“无序属性的集合,其属性可以包含基本值、对象或者函数。”严格来讲,这就相当于说对象是一组没有特定顺序的值。对象的每个属性或方法都有一个名字,而每个名字都映射到一个值。正因为这样(以及其他将要讨论的原因),我们可以把ECMAScript 的对象想象成散列表:无非就是一组名值对,其中值可以是数据或函数。

    var person = new Object();
    person.name = "Nicholas";
    person.age = 29;
    person.job = "Software Engineer";
    person.sayName = function(){
        alert(this.name);
    };
    

    上面的例子创建了一个名为 person 的对象,并为它添加了三个属性(nameagejob)和一个方法(sayName())。其中,sayName() 方法用于显示 this.name(将被解析为 person.name )的值。

    我们可以直接通过 person.属性名 的方式去访问那个变量。

    甚至也可以通过 person.属性名 = 属性值; 的方式去给对象添加属性值。

    而如果该属性已存在,则会修改属性的值。对于对象,我们还可以使用for...in 遍历对象的属性。

    早期的 JavaScript 开发人员经常使用这个模式创建新对象。几年后,对象字面量成为创建这种对象的首选模式。前面的例子用对象字面量语法可以写成这样:

    var person = {
        name: "Nicholas",
        age: 29,
        job: "Software Engineer",
        sayName: function(){
            alert(this.name);
        }
    };
    ```
    这个例子中的 `person` 对象与前面例子中的 `person` 对象是一样的,都有相同的属性和方法。这些属性在创建时都带有一些**特征值(characteristic)**,JavaScript 通过这些特征值来定义它们的行为。
    
    ####访问器属性
    访问器属性不包含数据值。它们包含一对 `getter` 和 `setter` 函数(不过,这两个函数都不是必需的)。
    
    在读取访问器属性时,会调用 `getter` 函数,这个函数负责返回有效的值;在写入访问器属性时,会调用 `setter` 函数并传入新值,这个函数负责决定如何处理数据。访问器属性有如下4 个特性:
    
    *  [[Configurable]]:表示能否通过 `delete` 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为数据属性。对于直接在对象上定义的属性,这个特性的默认值为 `true` 。
    *  [[Enumerable]]:表示能否通过 `for-in` 循环返回属性。对于直接在对象上定义的属性,这个特性的默认值为 `true`。
    *  [[Get]]:在读取属性时调用的函数。默认值为 `undefined`。
    *  [[Set]]:在写入属性时调用的函数。默认值为 `undefined`。
    
    访问器属性不能直接定义,必须使用 `Object.defineProperty()` 来定义。
    
    ####创建对象的方式
    * 使用 new Object() 创建
    * 工厂模式创建
    * 构造函数模式创建
    
    ####原型模式
    
    我们创建的每个函数都有一个 `prototype`(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。如果按照字面意思来理解,那么 `prototype` 就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。
    
    ```
    function Person(){
    }
    Person.prototype.name = "Nicholas";
    Person.prototype.age = 29;
    Person.prototype.job = "Software Engineer";
    Person.prototype.sayName = function(){
        alert(this.name);
    };
    
    var person1 = new Person();
    person1.sayName();     //"Nicholas"
    
    var person2 = new Person();
    person2.sayName();    //"Nicholas"
    alert(person1.sayName == person2.sayName);  //true
    ```
    #####理解原型对象
    无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 `prototype` 属性,这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个 `constructor(构造函数)` 属性,这个属性包含一个指向 `prototype` 属性所在函数的指针。就拿前面的例子来说,`Person.prototype. constructor` 指向 `Person` 。而通过这个构造函数,我们还可继续为原型对象添加其他属性和方法。
    
    创建了自定义的构造函数之后,其原型对象默认只会取得 `constructor` 属性。至于其他方法,则都是从 `Object` 继承而来的。当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。ECMA-262 第 5 版中管这个指针叫 [[Prototype]]。虽然在脚本中
    没有标准的方式访问 [[Prototype]],但 Firefox、Safari 和Chrome 在每个对象上都支持一个属性 `__proto__` 。而在其他实现中,这个属性对脚本则是完全不可见的。不过,要明确的真正重要的一点就是,这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。
    
    ![展示了各个对象之间的关系](http:https://img.haomeiwen.com/i2280328/e48d0bae3f1031d6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
    
    #####判断方法
    
    大家知道,我们用去访问一个对象的属性的时候,这个属性既有可能来自对象本身,也有可能来自这个对象的 [[prototype]] 属性指向的原型。
    ​
    那么如何判断这个对象的来源呢?
    ​
    `hasOwnProperty()` 方法,可以判断一个属性是否来自对象本身。通过`hasOwnProperty()` 这个方法可以判断一个对象是否在对象本身添加的,但是不能判断是否存在于原型中,因为有可能这个属性不存在。也即是说,在原型中的属性和不存在的属性都会返回 `fasle` 。
    
    这个也是唯一的一个处理属性而不查找原型链的方法!
    
    如何判断一个属性是否存在于原型中呢?
    
    `in` 操作符用来判断一个属性是否存在于这个对象中。但是在查找这个属性时候,先在对象本身中找,如果对象找不到再去原型中找。换句话说,只要对象和原型中有一个地方存在这个属性,就返回 `true` 。
    
    回到前面的问题,如何判断一个属性是否存在于原型中:如果一个属性存在,但是没有在对象本身中,则一定存在于原型中。
    ```
    function Person () {
    }
    Person.prototype.name = "志玲";
    var p1 = new Person();
    p1.sex = "女";
    //定义一个函数去判断原型所在的位置
    function propertyLocation(obj, prop){
        if(!(prop in obj)){
            alert(prop + "属性不存在");
        }else if(obj.hasOwnProperty(prop)){
            alert(prop + "属性存在于对象中");
        }else {
            alert(prop + "对象存在于原型中");
        }
    }
    propertyLocation(p1, "age");
    propertyLocation(p1, "name");
    propertyLocation(p1, "sex");
    ```
    
    ###继承
    
    其实笔者有整理一篇比较完整的继承的文章,只不过由于最近比较忙没有整理好。这里先做简略整理。
    
    继承是所有的面向对象的语言最重要的特征之一。大部分的 oop 语言的都支持两种继承:接口继承和实现继承。比如基于类的编程语言 Java,对这两种继承都支持。从接口继承抽象方法 (只有方法签名),从类中继承实例方法。
    ​但是对 JavaScript 来说,没有类和接口的概念( ES6 之前),所以只支持实现继承,而且继承在 **原型链** 的基础上实现的。等了解过原型链的概念之后,你会发现继承其实是发生在对象与对象之间。这是与其他编程语言很大的不同。
    
    ####原型链
    在 JavaScript 中,将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
    
    简单回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么,假如我们让原型对象等于另一个类型的实例,结果会怎么样呢?显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念。
    
    ![示意图](http:https://img.haomeiwen.com/i2280328/01a0d7efd0e420a5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
    
    #####测试数据类型
    1. `typeof`:一般用来测试简单数据类型和函数的类型。如果用来测试对象,则会一直返回 object,没有太大意义。
    2. `instanceof` :  用来测试一个对象是不是属于某个类型。结果为 `boolean`值。
    3. `isPrototypeOf( 对象 )` : 这是个原型的方法,参数传入一个对象,判断参数对象是不是由这个原型派生出来的。 也就是判断这个原型是不是参数对象原型链中的一环。
    
    ####借用构造函数调用继承
    借用构造函数调用继承,又叫伪装调用继承或冒充调用继承。虽然有了继承两个字,但是这种方法从本质上并没实现继承,只是完成了构造方法的调用而已。
    
    使用 `call` 或 `apply` 这两个方法完成函数借调。这两个方法的功能是一样的,只有少许的区别。功能都是更改一个构造方法内部的 `this` 指向到指定的对象上。
    
    代码示例:
    ```
    function Father (name,age) {
        this.name = name;
        this.age = age;
    }
    //如果这样直接调用,那么 father 中的 `this` 只的是 `window` 。 因为其实这样调用的: window.father("李四", 20)
    // `name` 和 `age` 属性就添加到了 `window` 属性上
    Father("李四", 20);
    alert("name:" + window.name + "\nage:" + window.age);  //可以正确的输出
    
    //使用call方法调用,则可以改变this的指向
    function Son (name, age, sex) {
        this.sex = sex;
        //调用Father方法(看成普通方法),第一个参数传入一个对象 `this`,则this(Son类型的对象)就成为了 Father 中的 `this`
        Father.call(this, name, age);
    }
        var son = new Son("张三", 30, "男");
        alert("name:" + son.name + "\nage:" + son.age + "\nsex:" + son.sex);
        alert(son instanceof Father); //false
    ```
    
    ####组合继承
    ```
    //定义父类型的构造函数
    function Father (name,age) {
        // 属性放在构造函数内部
        this.name = name;
        this.age = age;
        // 方法定义在原型中
        if((typeof Father.prototype.eat) != "function"){
            Father.prototype.eat = function () {
                alert(this.name + " 在吃东西");
            }
        }  
    }
    // 定义子类类型的构造函数
    function Son(name, age, sex){
    //借调父类型的构造函数,相当于把父类型中的属性添加到了未来的子类型的对象中
        Father.call(this, name, age);
        this.sex = sex;
    }
    //修改子类型的原型为父类型的对象。这样就可以继承父类型中的方法了。
    Son.prototype = new Father( );
    var son1 = new Son("志玲", 30, "女");
    alert(son1.name);
    alert(son1.sex);
    alert(son1.age);
    son1.eat();
    ```
    组合函数利用了原型继承和构造函数借调继承的优点,组合在一起。成为了使用最广泛的一种继承方式。
    说明:
    1. 组合继承是我们实际使用中最常用的一种继承方式。
    2. 可能有个地方有些人会有疑问:Son.prototype = new Father( );这不照样把父类型的属性给放在子类型的原型中了吗,还是会有共享问题呀。但是不要忘记了,我们在子类型的构造函数中借调了父类型的构造函数,也就是说,子类型的原型中有的属性,都会被子类对象中的属性给覆盖掉。就是这样的。
    
    ###作用域和闭包
    JavaScript 中的作用域和闭包已经发布,可以移步查阅,这里只做总结。
    
    变量的作用域指的是,变量起作用的范围。也就是能访问到变量的有效范围。
    
    **JavaScript 的变量依据作用域的范围可以分为:**
    ######全局变量
    定义在函数外部的变量都是全局变量。
    
    全局变量的作用域是**当前文档**,也就是当前文档所有的 JavaScript 脚本都可以访问到这个变量。
    
    ######局部变量
    在函数内声明的变量,叫局部变量!表示形参的变量也是局部变量!
    
    局部变量的作用域是局部变量所在的整个函数的内部。 在函数的外部不能访问局部变量。
    
    ####执行环境
    执行环境( execution context )是 JavaScript 中最为重要的一个概念。执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的 **变量对象**(variable object),环境中定义的所有变量和函数都保存在这个对象中。虽然我们编写的代码无法访问这个对象,但解析器在处理数据时会在后台使用它。
    
    ​全局执行环境是最外围的一个执行环境。在 Web 浏览器中,全局执行环境被认为是 window 对象,因此所有全局变量和函数都是作为 window 对象的属性和方法创建的。对全局执行环境变量来说,变量对象就是 window 对象,对函数来说,变量对象就是这个函数的 **活动对象** ,活动对象是在函数调用时创建的一个内部变量。
    
    ​每个函数都有自己的执行环境,当执行流进入一个函数时,函数的执行环境就会被推入一个执行环境栈中。而在函数执行之后,栈将执行结束的函数的执行环境弹出,把控制权返回给之前的执行环境。
    
    #####作用域链
    **作用域链与一个执行环境相关,作用域链用于在标示符解析中变量查找。**
    
    ​在 JavaScript 中,函数也是对象,实际上,JavaScript 里一切都是对象。函数对象和其它对象一样,拥有可以通过代码访问的属性和一系列仅供 JavaScript 引擎访问的内部属性。其中一个内部属性是 [[Scope]],由 ECMA-262 标准第三版定义,他就指向了这个函数的作用域链。作用域链中存储的是与每个执行环境相关 **变量对象 **(函数内部也是活动对象)。
    ​
    当创建一个函数( 声明一个函数 )后,那么会创建这个函数的作用域链。这个函数的作用域链在这个时候只包含一个变量对象( `window` )
    
    ####闭包
    一句话总结:​ **闭包是指有权访问另一个函数作用域中的变量的函数。**
    
    详细请查阅我的文章。
    
    ###其他相关
    
    想到什么就写什么。
    
    1. 在 JavaScript 中, `this` 的指向是动态改变的,不同的调用方式,`this` 的执行是不同的。调用一个方法或者函数的时候,如果是直接调用 **方法名()** 则这函数或方法中的 `this` 指代的就是 `window`。调用一个方法或者函数的时候,使用的是 **对象.方法名()** 则这个函数或方法中的this指代的就是 **这个对象**当做构造方法来用,使用 `new` 的时候,则 `this` 指代的是将要创建的对象。
    
    2. ​永远记住:只要是在全局作用域声明的任何变量和函数默认都是作为 `window` 对象的属性而存在的.
    
    3. 在 JavaScript 中构造函数和非构造函数没有本质的区别。**唯一的区别只是调用方式的区别。
        * 使用 `new` 就是构造函数。
        * 直接调用就是非构造函数。
    
    ---
    >参考书籍:《JavaScript 高级程序设计(第 3 版)》

    相关文章

      网友评论

        本文标题:JavaScript 中面向对象的程序设计

        本文链接:https://www.haomeiwen.com/subject/edtrvttx.html