01|理解对象
01|属性类型
对应的ECMAScript中有两种属性,分别为数据属性和访问器属性
- 数据属性
- Configurable:表示是否可以配置的! 能否修改属性的特性,或者说能否把属性修改为访问器属性!
- Enumerable:表示是否可以通过 for-in循环返回属性
- Writable:表示能否修改属性的值
- Value:表示该属性的属性值!
- 直接在对象上面定义的属性的特性默认值为true,对应的value属性的特性值为undefined!
对应的配置都可以通过 Object.defineProperty
来进行定义!
let person = {};
Object.defineProperty(person,'name',{
writable:false,
Value:'ProbeDream'
})
其实都是比较好理解的:
-
writable为false的话,那么对应值是不能被写入的!
-
configurable为false的话表示不能够从对象中删除属性! 该操作是不可逆的!
-
在调用Object.defineProperty的时候如果说不指定对应的几个特性的话,对应的默认值都是为false!
-
IE8是第一个实现Object.defineProperty的浏览器版本,该版本实现存在诸多限制,只能在DOM对象上面使用该方法,而且只能创建访问器的属性!
- 访问器属性
对应的访问器属性不包含数据值,它们包含一对儿 setter和getter函数!
- getter:读取访问器属性的时候,会调用getter并且返回该值
- setter:写入访问器属性的事后,会调用setter函数并且传入新值
对应的访问器有如下四个特性:
- Configurable:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性或者能否把属性修改为数据属性!
- Enumerable:表示能否通过for-in循环返回属性!
- Get:在读取属性时调用的函数! 默认值为undefined
- Set:在写入属性时候调用的函数! 默认值为undefined
我们通过对应的例子讲述:
let book = {
_year:2004,
edition:1
}
Object.defineProperty(book,'year',{
get(){
return this._year;
},set(newvalue){
if(newvalue>2004){
this._year = newvalue;
this.edition += newvalue - 2004;
}
}
});
book.year = 2005;
console.log(book.edition);//2
其中对应的下划线表示一种常用的记号,表示只能通过对象方法访问的属性!
02|定义多个属性
其中在ES5中又提出了一个新的API Object.defineProperties
方法,用该方法可以通过描述符一次定义多个属性,这个方法接收两个对象参数:
- 第一个对象是要添加和修改其属性的对象
- 第二个对象的属性与第一个对象重要添加或修改的属性一一对应!
let book = {};
Object.defineProperties(book,{
_year:{value:2004},
edition:{value:1},
year:{
get(){
return this._year;
},set(newvalue){
if(newvalue>2004){
this._year = newvalue;
this.edition += newvalue - 2004;
}
}
}
})
03|读取属性的特性
我们需要通过一个API 出自于 ES5中的Object.getOwnPropertyDescriptor
,可以取得给定属性的描述符,其中传入两个参数:
- 属性所在的对象
- 读取描述符的属性名称
let descriptor = Object.getOwnPropertyDescriptor(book,"_year");
console.log(descriptor.value,descriptor.configurable);
02|创建对象
其中使用Object构造函数或者对象字面量,确实能够很快的创建对象,但是会出现很多重复的代码,我们推荐使用工厂模式:
01|工厂模式
function createPersonFactory(name,age,job){
let p = new Object();
p.name = name;
p.age = age;
p.job = job;
p.sayName = function(){
console.log(this.name);
};
return p;
}
const p1 = createPersonFactory("p1",19,"Software Engineer");
const p2 = createPersonFactory("p2",20,"Software Engineer");
- 特性: 虽然解决了创建多个重复对象的问题,但是没有解决对象识别的问题(对象类型如何识别?)
于是有了构造器模式!
02|构造器模式
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
console.log(this.name);
};
}
const p1 = new Person("p1",19,"Software Engineer");
const p2 = new Person("p2",20,"Software Engineer");
-
该例子中使用 createPerson 取代了对应的 createPersonFactory函数,但是对应的内部实现除了相同的部分之外,还存在以下不同之处:
- 没有显式的创建对象
- 直接将属性和方法赋值给了this对象
- 没有return语句
-
使用大写的函数名是因为如下原因:
- 按照惯例:构造函数函数名首字母大写
- 为了更好的区分普通函数,构造器函数也是函数,只不过可以用来创建对象
-
要想创建Person的新实例必须的使用关键字: new 以这种方式调用构造函数实际上会经历如下四个步骤:
- 创建一个对象
- 将构造函数的作用域赋值给新对象 this指向了这个新对象
- 执行构造函数中的代码 为新对象添加属性
- 返回新对象
最后的p1和p2都保存着不同的Person实例,这两个实例都有一个constructor(构造器/函数)指向Person
console.log(p1.constructor === Person,p2.constructor === Person);//true true
对应的使用构造函数属性最初是用来表示对象类型的,但是用来监测对象类型最靠谱的还是使用 instanceof 操作符来得更加可靠一点!
console.log(p1 instanceof Person,p2 instanceof Person);//true true
- 将构造函数当作函数
构造函数与其他函数的唯一区别,就在于调用它的方式不同,对应的构造函数也是函数,不存在定义构造函数的特殊语法!
- 任何函数,只要通过new操作符来调用,那么就可以作为构造函数!
- 任何函数如果不通过new操作符调用,那么对应的和普通函数不会有什么两样
//通过构造函数来使用
let Person = new Person("ProbeDream",22,'softWare Engineer');
Person.sayName();//ProbeDream
//作为普通函数的使用
Person('Probe',19,'Frontend Engineer');//添加到window上
window.sayName();//Probe
//在另一个对象的作用域中调用
let o = new Object();
Person.call(o,'Julia',18,'Hr');
o.sayName();//Julia
- 在不使用new操作符的情况下,对应的对象和属性都添加给了全局对象了!
- 对应的因为如此,我们可以通过window.sayName()调用方法了!
- 但是可以通过call方法在某个对象的作用域中调用Person方法!
- 构造函数的问题
虽然说这样创建没有什么问题,但是比较麻烦的就是,每个方法都需要在不同的实力上重新创建一遍,对应的以上的代码中,都有对应的一个sayName的方法,其实对应的方法都是 Function不同的实例对象! 因此定义的函数其实也就是实例化的Function对象,因此可以这样定义构造函数!
function Person(name,age,obj){
this.name = name;
this.age = age;
this.obj = obj;
this.sayName = new Function('console.log(this.name)');//与声明函数在逻辑上是等价的!
}
- 这种方式创建的函数,会导致不同的作用域链和标识符解析,但是创建Function新势力的机制仍然是相同的!
- 不同实例上的同名函数是不相等的!
console.log(p1.sayname === p2.sayName);//false
这样一来对应的创建两个完成同样任务的Function实例的确没有必要,况且this对象在,根本不用在执行代码前就把函数绑定到特定对象上面! 可以通过把函数定义转移到构造函数外部解决这个问题
function Person(name,age,obj){
this.name = name;
this.age = age;
this.obj = obj;
this.sayName = sayName;
}
function sayName(){
console.log(this.name);
}
虽然这样一来对应的不同的Person的实例共享了在全局作用域定义的函数,但是全局作用域内定义的函数,只能够被某个对象调用,让全局作用域名不副实,如果说对想要定义多个方法的话,就需要在全局作用域内定义很多全局函数 自定义的引用类型没有任何的封装性可言了! 因此我们介绍到了原型模式!
03|原型模式
我们创建的函数都有一个prototype(原型)属性,该属性是一个指针,指向一个对象,该对象的用途是包含可以由特定类型所有实例共享的属性和方法!
- prototype通过调用构造函数而创建的那个对象实例的原型对象!
- 原型对象可以让所有对象实例共享它所包含的属性和方法!
于是,我们不必在构造函数中定义对象实例的信息,而是将该信息直接添加到圆形对象中
function Person(){}
Person.prototype.name = 'ProbeDream';
Person.prototype.age = 22;
Person.prototype.job = 'software Engineer';
Person.prototype.sayName = function(){console.log(this.name);}
let p1 = new Person();
p1.sayName();//ProbeDream
let p2 = new Person();
p2.sayName();//ProbeDream
console.log(p1.sayName === p2.sayName);//true
之所以对应的不同Person实例的sayName相等,是因为都指向同一个原型对象!
- 理解原型对象
我们创建了一个新的函数,都会根据一组特定的规则为该函数创建一个prototype的属性,该属性指向函数的原型对象! 所有的原型对象都会自动获得一个constructor属性,该属性包含一个prototype属性所在的函数的指针,例如说Person.prototype.constructor 指向Person,我们可以通过构造函数,继续为原型对象添加其他属性和方法!
对应的一个属性__proto__
是存在于实例和构造函数的原型对象之间,并非存在于实例和构造函数之间
对应的p1,p2都包含了一个内部属性,该属性仅仅指向了Person.constructor,他们与构造函数没有直接的关系,但是他们可以调用对应的sayName是因为通过查找对象属性的过程实现的!
虽然所有的视线中都无法访问到对应的prototype,但是可以通过isPrototypeOf方法来确定对象间是否存在这种关系,我们使用Prototype指向调用了isPrototypeOf方法的对象Person.prototype那么该方法就返回true!
console.log(Person.prototype.isPrototypeOf(p1),Person.prototype.isPrototypeOf(p2));//true true
- 对象原型的获取 Object.getPrototypeOf()
console.log(Object.getPrototypeOf(p1));//Person.prototype
对应的创建出来的实例,首先是看实例对象本身是否由该属性,如果没有的话再去原型链上查找!
通过以下例子就可以理解:
function Person(){}
Person.prototype.name = 'probedream';
Person.prototype.age = 29;
Person.prototype.job = 'Frontend Engineer';
Person.prototype.sayName = function(){
console.log(this.name);
}
let p1 = new Person();
p1.name == "Julia";
let p2 = new Person();
console.log(p1.name,p2.name);//Julia probedream
delete p1.name;
console.log(p1.name);//probedream
- 为了判断对应的属性是来自于实例中还是原型中可以通过
hasOwnProperty
进行查询验证 只有对应的该属性来自于实例对象本身的时候才会返回为true!
- 原型与in操作符
对应的in操作符一般分为两种情况下使用:
- for-in循环中使用
- in通过对象能够访问给定属性时返回true 无论是存在原型或者示例中!
对应的判断属性只存在于原型中应该如何编写代码?
function hasPrototypeProperty(object,name){
//先判断不在实例中
return !object.hasOwnProperty(name) && (name in object);
}
如果想获得所有实例属性,可以通过Object.getOwnPropertyNames() 方法!
- 更简单的原型语法
function Person(){}
Person.prototype = {
//可以理解为 constructor:Object
name:"ProbeDream",
age:22,
job:"FrontEnd Engineer",
sayName(){console.log(this.name);}
}
let p1 = new Person();
console.log(p1 instanceof Person,p1.constructor === Object);//true true
虽然这样通过new操作符创建出来的对象的结果相同,但是有问题的是 constructor属性不再指向Person了! ,每创建一个函数都会同事创建它的prototype对象,并且该对象会自动获得constructor属性! 而我们这里的语法,本质上完全重写了默认的prototype对象,因此constructor属性也就变成了新的对象的constructor属性(Object) 以上的代码虽然通过instanceof可以返回为true的结果值,但是对应的Constructor却无法确定对应的对象的类型了!
但是如果说想要Constructor对应的话,可以在 Person.prototype赋值的时候加上 constructor:Person
但是会导致Enumerable为true,原生的Constructor属性是不可以枚举的,因此我们可以通过Object.defineProperty设置Enumerable为false!
- 原型的动态性
let p1 = new Person();
Person.prototype.sayHi = function(){console.log('Hi!')}
p1.sayHi();//Hi!
以上代码运行没有问题,虽然说是新方法的添加是在实例化之后添加的,但是因为 实力与原型之间的松散连接的关系 我们调用sayHi方法的时候惠贤从实例上搜索,找不到的话再从原型上去查找!
但是有些时候,比如说重写整个原型对象的话,构造函数会为实例添加一个指向原型之间的[[Prototype]]指针,而把原型修改为另外一个对象等于切断了构造函数与最初原型之间的联系!
function Person(){}
let p1 = new Person();
Person.prototype = {
constructor:Person,
name:"ProbeDream",
age:22,
job:"FrontEnd Engineer",
sayName(){console.log(this.name);}
}
p1.sayName();//error
- 原生对象的原型
原型模式不仅仅仅体现在创建自定义类型方面,就连所有原生的引用类型,都是采用这种模式创建的!
对应的所有的原生引用类型,都在其构造类型的原型上定义了方法,例如说 Array.prototype可以找到sort方法! 对应的String.prototype可以找到对应的substring方法!
- 原型对象的问题
其中原型对象中最大的问题就是,共享本性所导致的,所有的属性都是被很多实例所共享的 如果说对于引用类型的值来说,问题就比较突出了!
function Person(){}
Person.prototype = {
name:"ProbeDream",
age:22,
job:'Front End Engineer',
friends:['Julia','Jerry'],
sayName(){
console.log(this.name);
}
}
let p1 = new Person();
let p2 = new Person();
p1.friends.push('tom');
console.log(p1.friends === p2.friends);//不同的实例共享同一个数组
但是其中一点比较总要的是:实力要有属于自己全部的属性的! 而这种所谓的原型模式恰恰违反了这一原则!
04|组合使用构造函数模式和原型模式
这种模式也是我们常常见到的:
- 构造函数:用于定义实例属性
- 原型模式:用于定义方法和共享的属性
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ['julia','jerry'];
}
Person.prototype = {
constructor:Person,
sayName(){console.log(this.name);}
}
//关闭构造器可遍历的特性!
Object.defineProperty(Person.prototype,'constructor',{
enumerable:false,value:Person
})
这种方式可以说是认同度最高的一种创建定义类型的方法!
05|动态原型模式
动态原型就是致力于解决独立的构造函数和原型的问题! 将所有的信息都封装在了构造函数中,而通过在构造函数中初始化对应的原型!(在必要的情况下)
function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
if(typeof this.sayName !== 'function'){
Person.prototype.sayName = function(){
console.log(this.name);
}
}
}
这段代码总的情况下来看还是比较容易理解的,只有在对应的sayName不存在的情况下才会添加到原型中!
对应的对于原型做的修改在对应的实例中完全可以得到体现!
但是需要注意的点就是:不能够使用对象字面量的形式重写原型,会导致切断现有实例和新原型之间的联系!
06|寄生构造函数模式
主要的原则就是:使用一个函数封装创建对象的代码,之后返回该对象!
function Person(name,age,job){
let p = new Object();
p.name = name;
p.age = age;
p.job = job;
p.sayName = function(){
console.log(this.name);
}
reutrn p;
}
- 这种模式创建出来的对象,与构造函数或者构造函数原型之间并没有什么关系!
- 不能够依赖 instanceof 操作符确定对象类型!
不推荐这种模式创建对象!
07|稳妥的构造模式
- 没有公共属性
- 方法不引用this的对象
- 适合在安全的环境中(该环境中禁止使用this和new)
function Person(name,age,job){
let p = new Object();
//私有变量和函数的定义!
p.sayName = function(){
console.log(name);
}
reutrn p;
}
对应的与寄生模式相似,但是与构造器和构造器原型之间并没有关系,使用instanceof操作符并没有意义!
03|继承
继承是OO语言(面向对象语言)中为人津津乐道的概念,很多OO语言都支持两种方式实现继承,接口继承和实现继承
- 接口继承:继承方法签名
- 实现继承:继承实际的方法
ECMAScript中没有对应的函数签名因此无法实现接口继承!
01|原型链
- ES中描述了圆形脸的概念,并且将原型链作为实现继承的主要方法对应的思想也是利用原型让一个引用类型继承另外一个引用类型的属性和方法!
对应的构造函数,原型,实例之间的关系
- 构造函数:都有对应的原型对象
- 原型对象:包含一个指向构造函数的指针
- 实例:包含一个指向原型对象的内部指针
如果说让原型对象等于另一个类型的实例,结果会如何呢?
原型对象包含指向另外一个原型对象的指针,另一个原型中包含另外一个指向构造函数的指针! 这种层层递进的关系构成了实力与原型之间的链条,这就是所谓的原型链!
对应的实现原型链有一种基本模式,代码实现如下所示:
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
}
function SubType(){
this.subproperty = false;
}
SubType.property = new SuperType();//实现继承
SubType.prototype.getSubValue = function(){
return this.subproperty;
}
let p = new SubType();
console.log(p.getSuperValue());
其中对应的继承是通过,SuperType创建的实例赋值给SubType.prototype实现的! 实质上是重写原型对象,代之以一个新类型的实例
其实代码中比较好理解的是:
instance指向SubType原型对应的SubType指向SuperType原型!
- getSuperValue存在于SuperType.prototype中
- property是一个实例属性,SubType.prototype是对应的SuperType的实例,因此property是存在于实例中!
- instance.constructor指向的是SuperType! 因为对应的构造器被重写了!
而我们通过 instance.getSuperType
调用方法其实是经历了三个过程:
- 搜索实例
- 搜索SubType.prototype
- 搜索SUperType.prototype 找到该方法!
- 确定原型和实例的关系
第一种方式是通过 instanceof 操作符,只要用该操作符就可以测试实例与原型链中出现过的构造函数! 结果会返回true
console.log(p instanceof Object );
console.log(p instanceof SuperType);
console.log(p instanceof SubType);
第二种方式是通过isPrototypeOf方法,只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型! 因此对应的方法也会返回true!
console.log(Object.prototype.isPrototypeOf(p));
console.log(SuperType.prototype.isPrototypeOf(p));
console.log(SubType.prototype.isPrototypeOf(p));
- 谨慎的定义方法
子类型有些时候需要重写炒类中的某个方法,但是需要注意的是,给原型添加方法一定是在替换原型语句(继承)之后
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
}
function SubType(){
this.subproperty = false;
}
SubType.property = new SuperType();//实现继承
SubType.prototype.getSubValue = function(){
return this.subproperty;
}
//重写超类型中的方法!
SubType.prototype.getSuperValue = function(){
return false;
}
let p = new SubType();
console.log(p.getSuperValue());
如果说使用对象字面量的形式添加方法,会导致替换原型(继承)的代码无效! 因此不推荐使用 对象字面量的形式重写超类型的方法或者原型的方法!
- 原型链所出现的问题
原型链虽然说比较强大,可以用来实现继承,但是如果说原型链包含应用类型值的原型的话,会被所有实例所共享! 在通过原型实现继承的时候,原型实际上会变成另外一个类型的实例,对应的实例的属性也会变成现在的原型属性了!
function SuperType(){
this.colors = ['red','blue','green'];
}
function SubType(){}
SubType.prototype = new SuperType();//实现继承
let p1 = new SubType();
p1.colors.push('grey');
console.log(p1.colors);//["red", "blue", "green", "grey"]
let p2 = new SubType();
console.log(p2.colors);//["red", "blue", "green", "grey"]
02|借用构造函数
为了解决原型中包含引用类型值所带来的问题的过程中,开发人员使用一种叫做 借用构造函数的技术 其实总的来讲还是比较好理解的,就是 子类型构造函数内部调用父类型构造函数!
function SuperType(){
this.colors = ['blue','red','yellow'];
}
function SubType(){
//继承了SuperType
SuperType.call(this);
}
let p1 = new SubType();
p1.colors.push('grey');
console.log(p1.colors);/["blue", "red", "yellow", "grey"]
let p2 = new SubType();
p2.colors.push('black');
console.log(p2.colors);//["blue", "red", "yellow", "black"]
其实通过这种方式就已经很好的解决了 圆形中包含引用类型带来的共享问题了! 每个实例中都只包含自己特有的colors属性副本!
与此在借用构造函数的时候还可以 传递参数!
- 子类型构造函数中向超类型构造函数传递参数
借用构造函数出现的问题,方法都造构造函数中定义的话,函数的复用就无从谈起!
超类型中定义的方法对于子类而言的话是不可见的,结果所有的类型都只能够使用构造函数模式! 借用构造函数的模式也是很少使用的!
03|组合继承
对应的组合继承指的就是说,将原型链和借用构造函数的奇数组合到一块!
- 原型链实现对原型属性和方法的继承
- 构造函数实现对实例属性的继承
function SuperType(name){
this.name = name;
this.colors = ['red','blue','yellow'];
}
SuperType.prototype.sayName = function(){
console.log(this.name);
}
function SubType(name,age){
SuperType.call(this,name);
this.age = age;
}
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
console.log(this.age);
}
其实对应的组合继承避免了原型联合借用构造函数的缺陷!融合他们的优点,与此同时instanceof和isPrototypeOf能够用于识别基于组合继承创建的对象!
之后又对原型式继承和寄生式继承以及寄生组合式继承进行了简要的讲解! 因为也是不常用的例子,这里就不进行阐述了!
网友评论