2020/06/06 JavaScript高级程序设计 面向对象的程序设计

Posted hermionepeng

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了2020/06/06 JavaScript高级程序设计 面向对象的程序设计相关的知识,希望对你有一定的参考价值。

ECMAScript虽然是一种面向对象的语言,但是他没有类的概念。所以他的对象也与其他语言中的对象有所不同。

ECMA-262定义对象:一组没有特定顺序的值。

6.1 理解对象

创建对象的方法:

1. 最简单直接的方式——Object构造函数

var person = new Object();
person.name = "Nicholas";
person. age = 29;
person.job = "Software Engineer";

person.sayName = function(){
    alert(this.name);
}

2. 对象字面量的方式

var person = {
    name = "Nicholas";
    age = 29;
    job = "Software Engineer";

    sayName = function(){
        alert(this.name);
    }
};     //注意这个分号

两种方法定义的对象是一样的,有着相同的属性和方法。这些属性创建时都带有一些特征值,JS通过这些值来定义他们的行为

6.1.1 属性的类型

特性:只有内部才用,描述了属性的各种特征。这些特性是为了实现JS引擎用的,因此不能直接访问他们。为表示特性是内部值,将他们放在两对方括号中

1. 数据属性

数据属性包含一个数据值的位置(引用类型)。在这个位置可以读取和写入值。有4个描述其行为的特征。

[[Configurable]] 能否通过delete删除或重定义属性/能否修改属性的特征/能否将属性修改为访问器属性 默认值为true
[[Enumerable]] 能否通过for-in循环返回属性 默认值为true
[[Writable]] 能否修改属性的值 默认值为true
[[Value]]

属性的数据值,这是读/写的位置 默认值为undefined

*这里的默认值针对的是对象字面量的定义方式,Object.defineProperties()方法定义方式默认为false。

修改属性默认的特性:Object.defineProperty()方法

该方法接收三个参数:属性所在的对象、属性的名字、一个描述符对象(属性是上面四个特征中的几个)

PS1:一旦把属性定义为不可配置,就不能再把它变为可配置了。此时再调用Object.defineProperty()方法除了修改writable外都会导致错误。

PS2:调用Object.defineProperty()方法时如果不指定,前三个特征的默认值都是false。

2. 访问器属性

访问器属性不包含数据值。他们包含一对getter和setter函数(不过都不是必须的)。

读取访问器属性时,会调用getter函数(负责返回有效的值);在写入访问器属性时,会调用setter函数(传入新值,负责决定如何处理数据)。

访问器属性有4个特征:

[[Configurable]] 能否通过delete删除或重定义属性/能否修改属性的特征/能否将属性修改为访问器属性 默认值为true
[[Enumerable]] 能否通过for-in循环返回属性 默认值为true
[[Get]] 读取属性时调用的函数 默认值为undefined
[[Set]] 写入属性时调用的函数 默认值为undefined

访问器属性的作用:用于设置一个属性会引起其他属性变化的情况。(get函数收到改变后的属性值,set针对这个值对其他属性值进行相应的改变)

PS1:_属性名(前面加下划线)是一种记号,表示只能通过对象方法访问的属性。

PS2:访问器属性不能直接定义,只能通过Object.defineProperty()方法来定义。

PS3:只设定getter意味着属性是不能写的,尝试写入属性会被忽略(严格模式下会抛出错误)。未设置getter意味着是不能读的(非严格模式返回undefined,严格模式下抛出错误)。

PS4:在Object.defineProperty()前使用_defineGetter_()和_defineSetter_()函数修改getter和setter特征(接收两个参数,修改的属性字符串形式和修改后的函数)。

PS5:在不支持Object.defineProperty()的浏览器中,无法修改[[Configurable]]和[[Enumerable]]。

6.1.2 定义多个属性

Object.defineProperties()方法,通过描述符一次定义多个属性(可以有数据属性也可以有访问器属性)。

接收两个对象属性,第一个对象是要添加/修改属性的对象;第二个对象是要添加/修改的属性。

PS:通过Object.defineProperties()定义的对象,凡是布尔值的特征默认值都是false。

6.1.3 读取属性的特征

Object.getOwnPropertyDescriptor()方法:取得给定属性的描述符。

接收两个参数,属性所在的对象,要读取的属性名称。返回值是一个对象。

6.2 创建对象

Object构造函数和对象字面量创建对象的缺点:使用一个接口创建很多对象时,会产生大量的重复代码。

6.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("Grey", 27, "Doctor");

缺点:未解决对象识别的问题(即无法识别出一个对象的类型)。

6.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");   //注意这里要使用new操作符
var person2 = new Person("Grey", 27, "Doctor");

构造函数模式和工厂模式的区别:

  • 没有显式的创建对象
  • 直接将属性和方法赋给了this对象
  • 没有return语句
  • 构造函数首字母大写而非构造函数首字母小写(以作区分)

调用构造函数创建实例的步骤:

  1. 创建一个新对象(new操作符)
  2. 把构造函数的作用域赋给这个对象(this就指向了这个新对象)
  3. 执行构造函数中的代码(为这个新对象添加属性)
  4. 返回新对象

自定义构造函数的优点:将实例标识为一种特定的类型(如上面的例子,实例的constructor属性指向Person),并且通过instance of操作符可知,创建的对象既是Object的实例也是Person的实例。

1. 将构造函数当作函数

构造函数和非构造函数唯一的区别就是调用方法不同。

下面展示不使用new操作符调用的情况(和普通函数没有区别)。

//作为构造函数使用
var person = new Person("Nicholas", 29, "software engineer"); 
person.sayName();  //"Nicholas"

//作为普通函数调用
Person("Grey", 27, "Doctor");
window.sayName();  //"Grey"

//在另一个对象的作用域中调用
var o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName();  //"Kristen" 

当在全局作用域调用一个函数时,this对象总指向Global对象(在浏览器中是window对象)。因此在函数调用后可以通过window对象调用sayName()方法。

也可以使用call()或者apply()在某个特殊对象的作用域中调用Person()函数。

2. 构造函数的问题

主要问题:每个方法都要在每个实例上重新创建一遍。(不同实例上的同名函数是不相等的)

alert(person1.sayName == person2.sayName);  //false

这样做没有必要,在有this的情况下,不用在执行代码前就把函数绑定到特定的对象上。

=>通过把函数定义转移到构造函数外来解决这个问题?

将函数变为全局函数,在构造函数内设置指针(this.sayName = sayName)指向同一个函数。看似问题得到了解决,但这样做使得“全局”名不副实,并且自定义类型没有封装性可言。

6.2.3 原型模式

函数的prototype(原型)属性:指向包含特定类型所有实例共享的属性和方法的对象的指针。是调用构造函数创建的实例的原型对象。 

优点:让所有实例共享它所包含的属性和方法。

function Person(){
}

Person.ptototype.name = name;
Person.ptototype.age = age;
Person.ptototype.job = job;
Person.ptototype.sayName = function(){
        alert(this.name);
};

var person1 = new Person("Nicholas", 29, "software engineer");  
var person2 = new Person("Grey", 27, "Doctor");

alert(person1.sayName == person2.sayName);  //true

仍然可以通过调用构造函数来创建实例,但新对象的属性和方法是所有实例共享的。

1. 理解原型对象

 prototype属性是连接构造函数和原型对象的。  函数 => (prototype) => 原型对象 => (constructor) => 函数(闭环)

Person.prototype.constructor;  //Person

原型对象默认只会取得constructor属性,其余方法都继承自Object。

连接实例和原型对象的指针:[[Prototype]](指向函数的原型对象)

PS:实例和构造函数并没有直接的联系。

实例只有一个属性[[Prototype]],通过这个指向实例对象的指针来获得属性和方法。

两个重要的方法:

  • isPrototypeOf():检验原型对象是否属于该实例
alert(Person.prototype.isPrototypeOf(person1));  //true
  • Object.getPrototypeOf():返回实例的原型(常用于继承)
alert(Object.getPrototypeOf(person1) == Person.prototype);  //true
alert(Object.getPrototypeOf(person1).name);  //"Nicholas"

PS1:在对象中搜索属性的顺序是:先在实例中搜索,再在原型对象中搜索。

PS2:在实例中定义原型中的同名属性并不能修改原型,只是覆盖了原型中的属性(因为搜索时先找的是实例的属性)。并且通过设置这个同名属性值为null也无法恢复指向原型的连接。只能使用delete操作符完全删除属性。

PS3:可以通过hasOwnProperty()方法检验一个熟悉是否在实例中(在原型的属性会返回false)。

PS4:Object.getOwnPropertyDescriptor()方法只能用于实例属性。要取得原型属性的描述符必须直接在原型对象上调用。

2. 原型与in操作符

in操作符:在通过对象能够访问属性时返回true。(无论是在实例中还是在原型中)

hasOwnProperty()方法:检验属性是否在实例中。

结合in和hasOwnProperty()可以确定属性究竟在原型中还是在实例中。

//检验属性是否在原型中
function hasPrototypeProperty(object, name){    
    return !object.hasOwnProperty(name) && (name in object);
}

如果是实例中覆盖了原型的,也会返回false。

for-in循环:返回能通过对象访问的、可枚举([[Enumerable]])的属性。(实例中&原型中的)

Object.keys()方法:接受一个对象作为参数,返回一个包含可枚举属性的字符串数组。

Object.getOwnPropertyNames()方法:返回所有实例属性,无论是否可枚举(包括constructor)。

3. 更简单的原型语法

原型模式的缺点:每定义一个属性/方法就要写一遍prototype,比较麻烦。也为了视觉上的封装性。  => 使用对象字面量的方式来重写整个原型对象。

function Person(){
}

Person.prototype = {
    name: "Nicholas";
    age: 29;
    job: "Software Engineer";
    sayName: function(){
        alert(this.name);
    }
};

但是这种方式有一个缺点:由于重写了原型对象,constructor不再指向他的构造函数,而是指向Object。即使instanceof仍能返回正确的结果

var friend = new Person();

alert(friend instanceof Object);  //true
alert(friend instanceof Person);  //true

alert(friend.constructor == Person);  //false
alert(friend.constructor == Object);  //true

如果直接设置constructor属性指向对应的构造函数,[[Enumerable]]属性会被默认为true(即可枚举)。此时可以使用Object.defineProperty()来更改[[Enumerable]]的值。

4. 原型的动态性

对原型对象的任何修改都能够立刻从实例中反映出来(即使是先创建了实例再修改原型)。

var friend = new Person();
Person.prototype.sayHi = function(){
    alert("hi");
};
friend.sayHi();  //"hi"

可以看到即使在创建实例之后创建的原型方法,在实例中依旧可以访问。这是由于原型和实例之间的松散连接方法

在调用方法时先在实例中搜索,搜索不到再在原型中搜索(这里使用的是指向原型的指针而不是一个副本,所以具有动态性)。

 

但是,重写原型对象是不一样的情况。由于在调用构造函数时会给实例添加一个指向最初原型的指针([[Prototype]]),把原型直接改为另一个对象会切断他和实例的联系。

function Person(){
}

var friend = new Person();

Person.prototype = {
    constructor: Person,
    name: "Nicholas",
    age: 29,
    job: "Software Engineer",
    sayName: function(){
        alert(this.name);
    }
};

friend.sayName();   //error

重写原型对象后相当于创建了一个新的对象,这个对象的constructor虽然指向了Person,但是之前创建的实例的原型仍是最初的原型对象。所以这里是调用不了sayName方法的。

5. 原生对象的原型

原生的引用类型都是采用原型模式创建的。

6. 原型对象的问题

  1. 省略了传递初始化参数的环节,使得所有实例默认情况下都将取得同样的属性值。
  2. 共享带来的问题:引用类型的属性,修改其中一个会造成所有实例都发生变化(基本类型可以通过覆盖的方式解决该问题)。

6.2.4 组合使用构造函数模式和原型模式

构造函数模式用于定义实例属性,原型模式用于定义方法和共享的属性

优点:最大限度节省内存,还支持向构造函数传递参数,集两种模式之长。这是目前使用最广泛、认同度最高的一种创建自定义类型的方式。

function Person(name, age, job){
    this.name = name;
    this.age = age;
    this.job = job;
    this.friend = ["Shelby", "Court"];
}

Person.ptototype = {
    constructor: Person,
    sayName = function(){
           alert(this.name); 
    }
};

var person1 = new Person("Nicholas", 29, "software engineer");  
var person2 = new Person("Grey", 27, "Doctor");

person1.friends.push("Van");
alert(person1.friends);  //"Shelby", "Court", "Van"
alert(person2.friends);  //"Shelby", "Court"

alert(person1.friends === person2.friends);  //false
alert(person1.sayName === person2.sayName);  //true

6.2.5 动态原型模式

把所有信息都封装在构造函数中,仅在必要的情况下在构造函数中初始化原型。即通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。

在初次调用构造函数时执行if语句中的部分,此后原型就完成了初始化。这里做出的修改会立即在所有实例中得到反映。

PS:if检查的可以是初始化后应该存在的任何属性和方法,无需对每一个属性和方法都进行检查。

function Person(name, age, job){
    //属性,构造函数模式
    this.name = name;
    this.age = age;
    this.job = job;
    
    //方法,原型模式
    if (typeof this.sayName != function){  //也可以使用instanceof操作符
        Person.prototype.sayName = function(){
            alert(this.name);
        }; 
    }
}

使用动态原型模式时,不能用对象字面量重写原型。

6.2.6 寄生构造函数模式

基本思想:创建一个封装对象代码,并返回新创建的对象的函数。

除了使用new操作符外,这个模式和工厂模式是一模一样的。这个模式可以在特殊情况下为对象创建构造函数。比如想创建一个拥有特殊方法的数组(因为不能直接修改Array)。

PS:返回的对象和构造函数或者构造函数的原型属性之间没有关系。即构造函数返回的对象和在构造函数外部创建没有什么不同。所以不能依赖instanceof操作符来确定对象类型。

6.2.7 稳妥构造函数模式

稳妥对象:没有公共属性,其方法也不引用this的对象。

稳妥对象适合用于一些安全的环境里(在这种环境中会禁止使用this和new)或者防止数据被其他应用程序改动时使用。

稳妥函数与寄生构造函数的区别:

  • 创建对象不引用this;
  • 不使用new操作符调用构造函数

除了构造函数中的方法外,不能通过其他方法访问构造函数中的原始数据(这些都是私有变量和函数)。同样,稳妥构造函数创建的对象与构造函数之间也没有什么关系。

6.3 继承

  • 接口继承:只继承方法签名。由于函数没有签名,故ECMAScript无法实现接口继承。
  • 实现继承:主要依靠原型链来实现。

6.3.1 原型链

原型链是实现继承的主要方法。

原型对象成为另一个类型的实例,此时的原型对象(1号原型)将包含一个指向另一个原型(2号原型)的指针([[Prototype]]),另一个原型(2)也将包含一个指向另一个构造函数的指针(constructor)。这样就构成了原型链(1继承自2)。

function SuperType(){
    this.property = true;
}

SuperType.prototype.getSuperValue = function(){
    return this.property;
};

function SubType(){
    this.subproperty = false;
}

//继承SuperType
SubType.prototype = new SuperType();  //SubType的原型改写为SuperType的实例

SubType.prototype.getSubValue = function(){   
    return this.subproperty;
};

var instance = new SubType();
alert(instance.getSuperValue());  //true

这里的SuperType继承了SubType,继承的方法是将SuperType的实例作为SubType的原型(本质是重写原型对象)。现在SuperType中的所有属性和方法都存在在SubType的原型中了。在确认继承关系后,给SubType的原型又添加了一个方法(getSubValue),这样就在继承的基础上添加了新的方法。

技术图片

 

  • 可以看到instance指向SubType的原型,而SubType的原型又指向SuperType的原型。getSuperValue方法虽然在SuperType的原型中,通过instance仍能够进行调用(这表示通过原型链实现了继承)。
  • SubType的原型作为SuperType的实例,拥有了property属性。
  • instance.constructor现在指向SuperType。因为SubType.prototype中的constructor被重写了(实际上是指向了另一个对象——SuperType的原型,而这个原型对象的constructor。属性指向的是SuperType)。

1. 别忘记默认的原型

2. 确定原型和实例的关系

3. 谨慎地定义方法

4. 原型链的问题

6.3.2 借用构造函数

 

以上是关于2020/06/06 JavaScript高级程序设计 面向对象的程序设计的主要内容,如果未能解决你的问题,请参考以下文章

《JavaScript高级程序设计》啥时候出版

《Javascript高级程序设计》阅读记录:第五章 上

《JavaScript高级程序设计》啥时候出版

JavaScript高级 引用类型《JavaScript高级程序设计(第三版)》

JavaScript高级程序设计(读书笔记)

赠书《JavaScript高级程序设计》5本