《JavaScript高级程序设计》学习笔记(5)——面向对象编程

欢迎关注本人的微信公众号“前端小填填”,专注前端技术的基础和项目开发的学习。

  本节内容对应《JavaScript高级程序设计》的第六章内容。

1、面向对象(Object-Oriented, OO)的语言有一个标志,那就是它们都有类的概念,而通过类可以创建任意多个具有相同属性和方法的对象。前面提到过,ECMAScript中没有类的概念,因此它的对象也与基于类的语言中的对象有所不同。

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

每个对象都是基于一个引用类型创建的,这个引用类型可以是前面讨论的原生类型,也可以是开发人员定义的类型。

2、属性类型:ECMA-262第5版在定义只有内部才用的特性时,描述了属性的各种特征。ECMA-262定义这些特性是为了实现JavaScript引擎用的,因此在JavaScript中不能直接访问它们。为了表示特性是内部值,该规范把它们放在了两对方括号中,例如[[Enumerable]]

  • 数据属性:包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有四个描述其行为的特性。

    • [[Configurable]]:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为true
    • [[Enumerable]]:表示能否通过for-in循环返回属性。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为true。
    • [[Writable]]:表示能否修改属性的值。像前面例子中那样直接在对象上定义的属性,它们的这个特性默认值为true。
    • [[Value]]:包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。这个特性的默认值为undefined。

要修改属性默认的特性,必须使用ECMAScript5的Object.defineProperty()方法。这个方法接收三个参数:属性所在的对象、属性的名字和一个描述符对象。其中,描述符对象的属性必须是:configurable、enumerable、writable和value。设置其中的一或多个值,可以修改对应的特性

var person = {
        name:"Nicholas",
        age:29,
        toString:function(){
            return "[name=" + this.name + "; age=" + this.age + "]";
        }
};
Object.defineProperty(person , "name",{
    writable:false
});
person.name="goskalrie";//修改无效
alert(person);//[name=Nicholas; age=29]
  • 访问器属性:不包含数据值;它们包含一对getter和setter函数(不过,这两个函数都不是必需的)。在读取访问器属性时,会调用getter函数,这个函数负责返回有效的值;在写入访问器属性时,会调用setter函数并传入新值。这个函数负责决定如何处理数据。访问器属性有如下4个特性。访问器属性不能直接定义,必须使用Object.defineProperty()来定义

    • [[Configurable]]:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为数据属性。对于直接在对象上定义的属性,它们的这个特性默认值为true
    • [[Enumerable]]:表示能否通过for-in循环返回属性。对于直接在对象上定义的属性,它们的这个特性默认值为true。
    • [[Get]]:在读取属性时调用的函数。默认值为undefined。
    • [[Set]]:在写入属性时调用的函数。默认值为undefined。
          var book = {
                  _year:2004,
                  edition:1
          };
          Object.defineProperty(book , "year" , {
              get:function(){
                  return this._year;
              },
              set:function(newValue){
                  if(newValue > 2004){
                      this._year = newValue;
                      this.edition += newValue - 2004;
                  }
              }
          });
          book.year = 2005;
          alert(book.edition);//2  
  • 由于为对象定义多个属性的可能性很大,ECMAScript5又定义了一个Object.defineProperties()方法。利用这个方法可以通过描述一次定义多个属性。这个方法接收两个对象参数:第一个对象是要添加和修改其属性的对象,第二个对象的属性与第一个对象中要添加或修改的属性一一对应。以下代码在book对外上定义了两个数据属性(_year和edition)和一个访问器属性(year)。

    var book = {};
    Object.defineProperties(book , {
        _year:{
           value:2004
        },
        edition:{
            value:1
        },
        year:{
            get:function(){
                return this._year;
            },
            set:function(newValue){
                if(newValue > 2004){
                    this._year = newValue;
                    this.edition += newValue - 2004;
                }
            }
        }
    }); 
  • 读取属性的特性:使用ECMAScript5的Object.getOwnPropertyDescriptor()方法,可以取得给定属性的描述符。这个方法接收两个参数:属性所在的对象和要读取其描述符的属性名称。返回值是一个对象,如果是访问器属性,这个对象的属性有configurable、enumerable、get和set;如果是数据属性,这个对像的属性有configurable、enumerable、writable和value。
    var descriptor =Object.getOwnPropertyDescriptor(book , "_year");
    alert(descriptor.value);//2004
    alert(descriptor.configurable);//false
    alert(typeof descriptor.get);//"undefined"

3、创建对象:虽然Object构造函数或对象字面量都可以用来创建单个对象,但这些方式有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。下面介绍多找种创建对象的方法:

  • 工厂模式:工厂模式是软件工程领域一种广为人知的设计模式,这种模式抽象了创建具体对象的过程。考虑到在ECMAScript中无法创建类,开发人员就发明了一种函数,用函数来封装以特定接口创建对象的细节。工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)

    function createPerson(name , age){
        var obj = new Object();
        obj.name = name;
        obj.age = age;
        obj.sayName = function(){
            alert(this.name);
        };
        return obj;
    }
    var person1 = createPerson("Nicholas" , 29);
    var person2 = createPerson("Greg" , 21);
  • 构造函数模式:前面介绍过,ECMAScript中的构造函数可用来创建特定类型的对象。像Object和Array这样的原生构造函数,在运行时会自动出现在执行环境中。此外,也可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。利用构造函数创建实例,必须使用new操作符注意和工厂模式进行比较其不同点。

    function Person(name , age){
        this.name = name;
        this.age = age;
        this.sayName = function(){
            alert(this.name);
        };
    }
    var person1 = new Person("Nicholas" , 29);
    var person2 = new Person("Greg" , 21); 
  • 构造函数与其他函数的唯一区别,就在于调用它们的方式不同。不过,构造函数毕竟也是函数,不存在定义构造函数的特殊语法。任何函数,只要通过new操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过new操作符来调用,那它跟普通函数也不会有什么两样。

        //当作构造函数使用
        var person = new Person("Nicholas" , 29);
        person.sayName();//"Nicholas"
        //作为普通函数调用
        Person("Greg" , 21);//添加到window
        window.sayName();//"Greg"
        var o = new Object();
        Person.call(o , "goser" , 24);
        o.sayName();//"goser"  

    构造函数虽然好用,但也并非没有缺点。使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍。在前面的例子中,person1和person2都有一个名为sayName()的方法,但那两个方法不是同一个Function的实例。不要忘了——ECMAScript中的函数是对象,因此每定义一个函数,也就是实例化了一个对象。从这个角度上来看构造函数,更容易明白每个Person实例都包含一个不同的Function实例的本质。说明白些,以这种方式创建函数,会导致不同的作用域链和标识符解析,但创建Function新实例的机制仍然是相同的。因此,不同实例上的同名函数是不相等的,以下代码可以证明这一点  alert(person.sayName ==person2.sayName);//false  ,创建两个完成同样任务的Function实例的确没有必要;况且this对象在,根本不用在执行代码前就把函数绑定到特定对象上面。因此,通过把函数定义到构造函数外部来解决这个问题,这样做确实解决了两个函数做同一件事的问题,可是新问题又来了:在全局作用域中定义的函数实际上只能对某个对象调用,这让全局作用域有点名不副实。而更让人无法接受的是:如果对象需要定义很多方法,那么就要定义很多个全局函数,于是这个自定义的引用类型就丝毫没有封装性可言了。好在,这些问题可以通过使用原型模式来解决。

    function sayName(){
        alert(this.name);
    }
    function Person(name , age){
        this.name = name;
        this.age = age;
        this.sayName = sayName;
    } 
  • 原型模式:我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。如果按照字面意思来理解,那么prototype就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。换句换说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。
    function Person(){}
    Person.prototype.name = "Nicholas";
    Person.prototype.age = 29;
    Person.prototype.sayName = function(){alert(this.name);};
    var person1 = new Person();
    person1.sayName();//"Nicholas"
    var person2 = new Person();
    person2.sayName();//"Nicholas"
    person1.name = "Greg";
    alert(person1.name);//"Greg"——来自实例
    delete person1.name;
    alert(person1.name);//"Nicholas"——来自原型
    alert(person1.sayName == person2.sayName);//true 

    在这个例子中,person1的name被一个新值给屏蔽了。但无论访问person1.name还是访问person2.name都能正常地返回值,即分别是”Greg”和”Nicholas”。当在alert()中访问person1.name时,需要读取它的值,因此就会在这个实例上搜索一个名为name的属性,这个属性确实存在,于是就返回它的值,不必再搜索原型了。当以相同的方式访问person2.name时,并没有在实例上发现该属性,因此就会继续搜索原型,结果在那里找到了name属性。使用delete操作符删除了person1.name,之前它保存的”Greg”值屏蔽了同名的原型属性。把它删除以后,就恢复了对原型中name属性的连接。因此,接下来再调用person1.name时,返回的就是原型中name属性的值了。

    //更简单的原型模式的写法
    function Person(){}
    Person.prototype = {
            name : "Nicholas",
            age : 29,
            sayName : function(){alert(this.name);}
    }; 
  • 组合使用构造函数模式和原型模式:创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。另外,这种混成模式还支持向构造函数传递参数;可谓是集两种模式之长
     function Person(name , age){
         this.name = name;
         this.age = age;
         this.friends = ["goser" , "greg"];
     }
     Person.prototype = {
             constructor : Person,
             sayName : function(){alert(this.name);},
     };
     var person1 = new Person("Nicholas" , 29);
     var person2 = new Person("Greg" , 21);
     person1.friends.push("gat");
     alert(person1.friends);//"goser,greg,gat"
     alert(person2.friends);//"goser,greg"
     alert(person1.friends == person2.friends);//false
     alert(person1.sayName == person2.sayName);//true
  • 动态原型模式:有其他OO语言经验的开发人员在看到独立的构造函数和原型时,很可能会感到非常困惑。动态原型模式正式致力于解决这个问题的一个方案。它把所有信息都封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。换句话说,可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型
    function Person(name , age){
        this.name = name;
        this.age = age;
    -----------------------------------------//
        if(typeof this.sayName != "function"){
            Person.prototype.sayName = function(){
               alert(this.name);
            };
        }
    -----------------------------------------//
    }
    var p = new Person("Nicholas" , 29);
    p.sayName();
  • 寄生构造函数模式:通常,在前述的几种模式都不适用的情况下,可以使用寄生构造函数模式。这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;但从表面上看,这个函数又很像是经典的构造函数。实际上就是设计模式 中的组合模式的应用吧
    function Person(name , age){
         var obj = new Object();
         obj.name = name;
         obj.age = age;
         obj.sayName = function(){
         alert(this.name);
         };
         return obj;
    }
    var p = new Person("Nicholas" , 29);
    p.sayName();  
时间: 2024-10-18 03:00:54

《JavaScript高级程序设计》学习笔记(5)——面向对象编程的相关文章

javascript 高级程序设计学习笔记(面向对象的程序设计) 2

在调用构造函数时会为实例添加一个指向最初原型的指针,我们可以随时为原型添加属性和方法,并且能在实例中体现出来,但如果是重新了原型对象,那就会切断构造函数与最初原型的联系. function Dog () { } var friend = new Dog(); Dog.prototype ={ constructor : Dog, name : "Bob", age : 11, jump : function () { alert("跳一下"); } }; frien

JavaScript高级程序设计学习笔记--面向对象程序设计

工厂模式 虽然Object构造函数或对象字面量都可以用来创建单个对象,但这些方式有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码.为解决这个问题,人们开始使用 工厂模式的一种变体. function createPerson(name,age,job){ var o=new Object(); o.name=name; o.age=age; o.job=job; o.sayName=function(){ alert(this.name); }; return o; } var

JavaScript高级程序设计学习笔记--基本概念

1.语句 ECMAScript中的语句以一个分号结尾:如果省略分号,则由解析器确定语句的结尾,如下例所示: var sum=a+b //即使没有分号也是有效的语句--推荐 var diff=a-b; //有效的语句--推荐 虽然语句结尾的分号不是必需的,但我们建议任何时候都不要省略它.两个原因:1.加上分号可以避免很多错误 2.加上分号也会在某些情况下增进代码的性能,因为这样解析器就不必再花时间 推测应该在哪里插入分号了. 2.变量 var message="hi"; 像这样初始化变量

JavaScript高级程序设计学习笔记--错误处理与调试

try-catch语句 只要代码中包含finally子句,则无论try或catch语句块中包含什么代码--甚至return语句,都不会阻止finally子句的执行,来看下面这个函数: function testFinally(){ try{ return 2; }catch(error){ return 1; }finally{ return 0; } } 调用这个函数会返回0(PS:但我实际执行的时候会先返回0,再返回2) 抛出错误 与try-catch语句相配的还有一个throw操作符,用于

Javascript高级程序设计学习笔记

3. 基本概念 基本数据类型:Undefined,Null,Boolean,Number,String. 复杂数据类型:Object. 3.6 语句 switch比较值时用的是全等运算符 “===” ,因此不会进行类型转换.例如 “10” 不等于10. 3.7 函数 ECMAScript函数不介意传递进来多少个参数,也不在乎参数的类型.即使定义的函数只接受两个参数,在调用的时候也可以传递任意多个或者0个.因为ECMAScript的参数在内部是用一个数组表示的,在函数体内部可以通过argument

JavaScript高级程序设计学习笔记--高级技巧

惰性载入函数 因为浏览器之间行为的差异,多数JavaScript代码包含了大量的if语句,将执行引导到正确的代码中,看看下面来自上一章的createXHR()函数. function createXHR(){ if (typeof XMLHttpRequest != "undefined"){ return new XMLHttpRequest(); } else if (typeof ActiveXObject != "undefined"){ if (typeo

JavaScript高级程序设计学习笔记--DOM

DOM(文档对象模型)是针对HTML和XML文档的一个API(应用程序接口). Document类型 文档的子节点 虽然DOM标准规定Document节点的子节点可以是DocumentType,Element,ProcessingInstruction或Comment,但还有两个内置的访问其子节点的快捷方式.第一个就是documnetElement属性,该属性始终指向HTML页面中的<html>元素.另一个就是通过childNodes列表访问文档元素,但通过documentElement属性则

JavaScript高级程序设计学习笔记--BOM

window对象 BOM的核心对象是window,它表示浏览器的一个实例.在浏览器中,window对象有双重角色,它既是通过JavaScript访问浏览器窗口的一个接口,又是ECMScript规定的Global对象. 全局作用域 由于window对象同时扮演着ECMAScript中Global对象的角色,因此所有在全局作用域中声明的变量.函数都会变成window对象的属性和方法.来看下面的例子: var age=29; function sayAge(){ alert(this.age); }

JavaScript高级程序设计学习笔记--引用类型

Object类型 对象字面量表示法: var person={ name:"Nicholas", age:29, 5:true }; 这人例子会创建一个对象,包含三个属性:name,age和5.但这里的数值属性名会自动转换为字符串. 对象属性的访问:点表示法和方括号语法 alert(person["name"]); // "Nicholas" alert(person.name); // "Nicholas" 从功能上看,这两

javascript 高级程序设计 学习笔记01章 javascript的认知

hello world: 大家早上好 ,所谓一年之计在于春,一日之计在于晨.今天开始学习 高级程序设计第三版 第二遍 以前以迅雷不及掩耳之势草草看过一遍 ,但是什么也没记住 已经忘得差不多了.哈哈,原来我不是黄蓉,也不是天才,还是那句话 好记性不如烂笔头.这次学习高级程序设计第三版 是有目标的,有目的性的.所以我会把 每一章学习的心得和笔记 都记录在此.于己于人都方便.废话不多说 ,开始吧 今天的第一张 ,javascript 的实现.张鑫旭大神说过 非it(计算机专业的)的前端从业人员 学习j