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语句
- 构造函数首字母大写而非构造函数首字母小写(以作区分)
调用构造函数创建实例的步骤:
- 创建一个新对象(new操作符)
- 把构造函数的作用域赋给这个对象(this就指向了这个新对象)
- 执行构造函数中的代码(为这个新对象添加属性)
- 返回新对象
自定义构造函数的优点:将实例标识为一种特定的类型(如上面的例子,实例的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. 原型对象的问题
- 省略了传递初始化参数的环节,使得所有实例默认情况下都将取得同样的属性值。
- 共享带来的问题:引用类型的属性,修改其中一个会造成所有实例都发生变化(基本类型可以通过覆盖的方式解决该问题)。
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高级程序设计 面向对象的程序设计的主要内容,如果未能解决你的问题,请参考以下文章