5、面向对象的程序设计1(JS高级笔记)

声明:此文集下的文章都是看了Javascript高级程序设计所做的笔记。此章对应书中的第六章。

一、理解对象

JS早期开发中经常使用以下两种方式创建对象:

var person = new Object();
person.name = "Tom";
person.age = 23;
person.job = "Software Engineer";
person.sayName = function(){
    console.log(this.name);
}
var person = {
    name : "Tom",
    age : 29,
    job : "Software Engineer",
    sayName : function(){
        console.log(this.name);
    }
};

说明:这里创建了一个对象,此对象有三个属性和一个方法。在第二种方式中,属性名可以是name、'name'或者"name",但是如果属性是保留字或者以数字开头则必须加引号。

1.1 属性类型

ECMAScript中有两种属性:数据属性和访问器属性。
1、数据属性

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

如果要修改属性默认的特性,必须使用ECMAScript5中的Object.defineProperty()方法。接收三个参数:属性所在对象、属性的名字和一个描述符对象,其中描述符对象必须是:configurable、enumberable、writable、value,比如:

        var person = {};
        Object.defineProperty(person, 'name', {
            writable : false,
            value : "Tom"
        });
        console.log(person.name);
        person.name = "Jerry";
        console.log(person.name);

说明:这里我们将writable属性设置为了false,标明此属性的值不能被修改了,可以看到使用此方法就能精确控制每个属性了。这里没有设置的属性则取false(这和之前定义时不同)。当然在后面的代码中我们还可以将此属性改为true,但是如果属性configurable被设置为了false则表示不能使用delete将某个属性删除掉了,同时也表示此属性变为不可配置了,即如果配置了writable,则在后面的代码中不能改变其值了。同时configurable本身也不能改变。如:

        var person = {};
        Object.defineProperty(person, 'name', {
            configurable : false,
            value : "Tom"
        });
        //抛出错误
        Object.defineProperty(person, 'name', {
            configurable : true,
            value : "Tome"
        });

2、访问器属性(存取器属性)
访问器属性不包括数据值,它们是一对儿gettersetter函数(不过,这两个函数都不是必须的),有以下四个属性:

  • [[Configurable]]:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性,对于直接在对象上定义的属性,默认为true,表示可以。
  • [[Enumerable]]:表示能否通过for-in循环返回每个属性,对于直接在对象上定义的属性,默认为true
  • [[Get]]:在读取属性时调用的函数。默认值为undefined
  • [[Set]]:在写入属性时调用的函数。默认值为undefined

访问器属性不能直接定义,必须使用Object.defineProperty()定义。比如:

        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
        book._year = 3000;
        alert(book._year);

说明:以上代码中我们给对象book定义了两个属性_year、edition,其中_year前面的下划线是一种标记,表示只能通过对象方法访问的属性,当然这里我们还是可以直接访问的。这里还定义了一个访问器属性year,包含getter、setter方法,修改year的值会导致_year值改变,而edition变为2,起始访问器属性就像一种标志,可以在方法中做一些操作来达到某个目的,比如这里在setter方法中我们就让edition的值随着year的值改变而改变。
定义gettersetter方法还可以这样:

        var person = {
            name: "Tom",
            get age() {
                return 12;
            }
        }
        console.log(person.age);

而此时age也是对象的一个属性。

在这两个方法之前一般使用的是两个非标准的方法:__defineGetter__()、__defineSetter__(),使用这两个方法实现之前的代码:

        var book = {
            _year: 2004,
            edition: 1
        };
          
        //legacy accessor support
        book.__defineGetter__("year", function(){
            return this._year;    
        });
        
        book.__defineSetter__("year", function(newValue){
            if (newValue > 2004) {
                this._year = newValue;
                this.edition += newValue - 2004;
            }    
        });
        book.year = 2005;
        alert(book.edition);   //2

1.2 定义多个属性

使用Object.defineProperty()方法定义属性太麻烦,我们可以使用Object.defineProperties()一次性定义多个属性,如下:

        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;
                    }                  
                }            
            }        
        });
           
        book.year = 2005;
        book._year = 100;//无效,因为这样定义的属性是不能修改的
        console.log(book._year);
        console.log(book.edition);//2

1.3 读取属性的特性

使用ECMAScript 5Object.getOwnPropertyDescriptor()方法,可以取得给定属性的描述符。接收两个参数:属性所在对象和要读取其描述符的属性名称。返回值是一个对象,如果是访问器属性,则这个对象的属性有configurable、enumberable、get、set;如果是数据属性,则这个对象的属性有configurable、enumberable、writable、value。如:

        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;
                    }                  
                }            
            }        
        });
           
        var descriptor = Object.getOwnPropertyDescriptor(book, "_year");
        alert(descriptor.value);          //2004
        alert(descriptor.configurable);   //false
        alert(typeof descriptor.get);     //"undefined"
        
        var descriptor = Object.getOwnPropertyDescriptor(book, "year");
        alert(descriptor.value);          //undefined
        alert(descriptor.enumerable);     //false
        alert(typeof descriptor.get);     //"function"

二、创建对象

虽然使用Object构造函数或对象字面量都可以用来创建单个对象,但这些方式使用同一个接口创建很多对象,会产生大量的重复代码。下面看几种其他的方式。

2.1 工厂模式

        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 person1 = createPerson("Nicholas", 29, "Software Engineer");
        var person2 = createPerson("Greg", 27, "Doctor");
        
        person1.sayName();   //"Nicholas"
        person2.sayName();   //"Greg"

说明:这里使用函数来封装以特定接口创建对象的细节,这中方式虽然解决了相似对象的问题,但是却没有解决对象识别的问题(即怎样知道一个对象的类型)。

2.2 构造函数模式

可以使用构造函数将前面例子重写:

        function Person(name, age, job){
            this.name = name;
            this.age = age;
            this.job = job;
            this.sayName = function(){
                alert(this.name);
            };    
        }
        
        var person1 = new Person("Nicholas", 29, "Software Engineer");
        var person2 = new Person("Greg", 27, "Doctor");

说明:这里使用Person()函数取代了createPerson()函数,有几点不同的地方:

  • 没有显式地创建对象
  • 直接将属性和方法赋给this对象
  • 没有return语句

这里要创建Person新实例,必须使用new操作符,上述代码创建了两个Person实例,都有一个相同的construtor(构造函数)属性:

console.log(person1.construtor == Person);//true
console.log(person2.construtor == Person);//true

对象的construtor属性最初是来标识对象类型的,但是,在检测对象类型时,还是使用instanceof操作符更可靠一些。

console.log(person1 instanceof Object);//true
console.log(person1 instanceof Person);//true
console.log(person2 instanceof Object);//true
console.log(person2 instanceof Person);//true

这里所有对象均继承自Object

2.2.1 将构造函数当作函数

任何函数,只要通过new操作符来调用,那它就可以作为构造函数,而任何函数,如果不通过new操作符来调用,那它跟普通的函数也一样。如前面定义的Person构造函数:

var person = new Person("Nicholas", 29, "Software Engineer");
person.sayName();   //"Nicholas"
        
Person("Greg", 27, "Doctor");  //adds to window
window.sayName();   //"Greg"
        
var o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName();    //"Kristen"

说明:当在全局作用域中调用一个函数时,this对象总是指向Global对象(在浏览器中就是window对象)。也可以使用call()apply()调用。

2.2.2 构造函数的问题

构造函数模式的主要问题就是每个方法都要在每个实例上重新创建一遍。因为函数就是对象,因此没定义一个函数,也就是实例化了一个对象,于是构造函数可以定义为这样:

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = new Function("alert(this.name)");
}

说明:不同实例上的同名函数sayName不是相等的。这里我们可以将函数定义在构造函数之外:

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.sayName = sayName;
}
function sayName(){
  alert(this.name);
}
var person1 = new Person("Tom", 29, "Software Engineer");
var person2 = new Person("Jerry", 28, "Doctor");

说明:此时两个实例中的同名函数sayName就是同一个了,但是新问题又来了:在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实,而且,如果要定义很多方法,那就得定义多个全局函数,那自定义的引用类型就没有封装性了。这需要使用原型模式解决。

2.2.3 原型模式

我们创建了每个函数都有一个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

说明:我们将sayName()方法和所有属性都直接添加到了Personprototype属性中,构造函数变成了空函数,当然还是可以通过构造函数来创建新对象,而且新对象还会具有相同的属性和方法。

1、理解原型对象

1

说明:无论什么时候,只要创建一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象,如图中的构造函数Person、实例person1、person2其中的prototype属性都指向原型对象。创建了自定义的构造函数之后,其原型对象默认只会取得construtor属性,至于其他方法,都是从Object继承或我们自己添加的。一般使用属性__proto__访问原型对象。
虽然在所有实现中都无法访问到[[Prototype]],但是可以通过isPrototypeOf()方法来确定对象之间是否存在这种关系,本质上讲,如果[[Prototype]]指向调用isPrototypeOf()方法的对象(Person.prototype),那么这个方法就返回true

alert(Person.prototype.isPrototypeOf(person1));//true
alert(Person.prototype.isPrototypeOf(person2));//true

ECMAScript 5中增加了一个新方法,叫Object.getPrototypeOf(),在所有支持的实现中,这个方法返回[[Prototype]]的值,如:

alert(Object.getPrototypeOf(person1) == Person.prototype);//true
alert(Object.getPrototypeOf(person1).name);//"Tom"

说明:每当代码读取某个对象的某个属性时,都会执行搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始。如果在实例中找了具有给定名字的属性,则返回该属性的值;如果没有,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性,如果找到则返回相关的值。
虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。如果我们在实例中添加了一个属性,而该属性与实例原型中的一个属性同名,那我们就在实例中创建该属性,该属性将会屏蔽原型中的那个属性。看如下例子:

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();
var person2 = new Person();

person1.name = "Greg";
alert(person1.name);   //"Greg"来自实例
alert(person2.name);   //"Nicholas"来自原型

说明:在实例中增加一个和原型对象中同名属性,只会屏蔽原型中的属性,并不会覆盖。当在搜素时,如果在实例中搜索到了相关属性,则不会继续向原型中搜索了。当然使用delete方法可以完全删除实例中的某个属性,从而放我们能够重新访问原型中的属性。

delete person1.name;
alert(person1.name);  //"Nicholas"来自原型

使用hasOwnProperty()方法可以检测一个属性是存在于实例中,还是存在于原型中,这个方法是从Object继承过来的。只在给定属性存在于对象实例中,才会返回true

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();
var person2 = new Person();
alert(person1.hasOwnProperty("name"));//false

person1.name = "Greg";
alert(person1.name);   //"Greg"来自实例
alert(person1.hasOwnProperty("name"));//true

alert(person2.name);   //"Nicholas"来自原型
alert(person2.hasOwnProperty("name"));//false

delete person1.name;
alert(person1.name); //"Nicholas"来自原型
alert(person1.hasOwnProperty("name"));//false
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容