1.在js中,类是最重要的数据结构,甚至可以说“在js中一切皆对象”。对象中的属性有两种属性:数据属性和访问器属性。
数据属性包含一个数据值的位置,通过这个位置可以读取和写入数据,其中数据属性拥有4个描述其行为的特性:
在ES5中要修改属性的特性,必须使用Object.defineProperty()方法来修改,该方法接受三个参数:属性所在的对象、属性名、描述属性特性的对象,使用方法如下:
person.name = "daydaywen";
console.log(person.name) // daydaywen
Object.defineProperty(person,"name",{
writable: false
})
person.name = "huahua";
console.log(person.name) // daydaywen
上面代码通过Object.defineProperty()方法对person对象的name属性进行了配置,让name属性变得不可修改。
2.访问器属性:访问器属性不包含数据值,而是包含一对setter、getter方法(这两个方法不是必须的),在读取访问器属性时,会调用getter方法,此方法会返回相应的值。在写入访问器属性时,会调用getter方法,此方法决定如何处理传入的数据。访问器属性包含以下四个特性:
= birthday) {
this.age = thisYear - birthday;
}
}
})
console.log(person.birthday) // 1998
person.birthday = 2008;
console.log(person.age) // 10
上面代码中为birthday属性设置了访问器属性,即在访问birthday属性时会自动调用这两个方法。比如在获取person.birthday属性时,自动调用了set方法,此方法放回当前时间与年龄的差值。在设置person.birthday属性为2008时,自动调用了set方法,并更改age属性为当前时间与2008的差值。
上面例子就是使用访问器属性最常见的方式,即设置一个属性的值会导致其他属性随之改变。
3.在ES5中新增了一个新的方法Object.defineProperties()方法,此方法可以一次性定义多个属性值,使用方法和defineProperty类似。除了设置属性的特性外,也可以使用getOwnPropertyDescriptor方法读取某个属性的特性,该方法接受两个参数,属性所在的对象和属性的名称。
Object.defineProperties(person,{
name: {
value: "dayday",writable: false
},age: {
value: 22,birthday: {
get: function() {
// ...
},set: function(birthday) {
// ...
}
}
})
let descriptor = Object.getOwnPropertyDescriptor(person,"name");
console.log(descriptor) // {value: "dayday",writable: false,enumerable: false,configurable: false}
4.原型链机制:在js中原型链是极其重要的知识,想要了解js面向对象的原理就必须掌握原型链机制。
首先,我们创建的每一个函数都会有一个prototype属性,这个属性的值是一个指向其原型对象的指针。什么是原型对象呢?原型对象是一个包含了函数所有实例共享方法和属性的的对象,简单的来说函数所有的实例都可以共享原型对象中的属性和方法。
function Animal(species) {
this.species = species
}
Animal.prototype.name = "animal";
let dog = new Animal("dog");
let cat = new Animal("cat");
console.log(dog.species); // dog
console.log(dog.name); // animal
console.log(cat.species); // cat
console.log(cat.name); // animal
上面代码中我们先创建了构造函数Animal,并在构造函数中指定species属性为传入的参数,然后再指定构造函数原型对象中name属性为"animal"。当我们在实例化一个对象(调用new操作符)时,species属性是所有实例私有的,而name属性则是所有实例共享的。当我们修改原型对象中的属性值时,所有的实例都会随之改变。
原型对象和构造函数是怎么关联起来的呢?首先要明确的一点是,原型对象也是普通的对象,原型对象中有一个constructor属性,此属性同样是一个指针,指向它的构造函数,在上面的例子中Animal的原型对象Animal.prototype的constructor属性就指向Animal构造函数,即Animal.prototype.construtor == Animal。
所有实例共享原型对象中的属性,实例和原型对象又是怎么关联的呢?在所有实例中,都会有一个proto属性,此属性也是一个指针,指向它的构造函数的原型对象,上面例子中dog是Animal的实例,因此dog._proto_指向Animal.prototype,即dog.proto == Animal.prototype,我们还有可以通过方法Object.getPrototypeOf()得到实例的原型对象
console.log(Animal.prototype.constructor == Animal) // true
console.log(Animal.prototype == dog.__proto__) // true
console.log(Object.getPrototypeOf(dog) == Animal.prototype) // true
5.访问实例的某个属性时的查询顺序:在访问对象的某个属性时,js解析引擎会在首先在实例对象内部查询,查找到则返回,没有查找到则沿着原型链继续向上查找,查到则返回,没有查到则返回undefined。
function Animal(species) {
this.species = species
}
Animal.prototype.name = "animal";
let dog = new Animal("dog");
let cat = new Animal("cat");
dog.name = "二哈";
console.log(dog.name); // 二哈
console.log(cat.name); // animal
上面代码中,给dog实例的name属性赋值"二哈",可以看成是sog实例的name属性覆盖了原型对象中的name属性,实际上是dog属性的name属性优先级高于原型对象的name属性,因此查询时会就近返回dog实例的name属性。为了区分属性时原型对象中的属性还是实例中的属性,我们可以使用hasOwnProperty()方法来区分,如果是原型属性则返回false,如果是实例属性则返回true。
``javascript
function Animal(species) {
this.species = species
}
Animal.prototype.name = "animal";
let dog = new Animal("dog");
let cat = new Animal("cat");
dog.name = "二哈";
console.log(dog.hasOwnProperty("name")); // true
console.log(cat.hasOwnProperty("name")); // false
遍历对象的属性:在遍历对象的属性时,可以使用for-in循环进行遍历,注意for-in循环只能遍历对象的可枚举属性(包含实例上的和原型上的可枚举属性)。要获取对象的所有可枚举实例属性(注意是实例属性)还可以使用Object.keys()方法,该方法接受一个对象作为参数,返回包含此对象所有可枚举属性的字符串数组。
let dog = new Animal("dog");
for(i in dog) {
console.log(i) // species name(原型和实例上的所有可枚举属性)
}
console.log(Object.keys(dog)) // ["species"] (实例上的可枚举属性)
person.prototype.constructor = person; // 动态修改constructor属性
console.log(person.prototype.constructor == person) // true
console.log(Object.keys(person.prototype)); // ["county","city","constructor"]
// 使用Object.defineProperty()方法来修改constructor属性
Object.defineProperty(person.prototype,"constructor",{
enumerable: false,value: person
})
console.log(Object.keys(person.prototype));
上面代码中,使用字面量方法给person函数动态的赋值了一个原型对象,此时新的原型对象中没有constructor属性,我们需要手动修改原型对象的constructor属性,因此person.prototype.constructor == person返回false。
当我们加上person.prototype.constructor = person;语句后就修改原型对象的constructor属性指向person函数对象。但是有一点需要注意的是:原来的constructor属性是不可枚举的,当我们使用这种方法修改constructor属性后,constructor属性就会变为可枚举属性了。如果想要保持constructor属性的可枚举特性不变,必须使用Object.defineProperty()方法。
原型的动态性:即我们对原型对象做的任何修改都能立即从实例中反映出来(即使是先创建实例,再对原型进行修改)。但是请注意如果是采用字面量对原型对象进行重新赋值则需要实例化对象操作(new操作)在赋值操作之后。
let person1 = new person("dayday",22);
person.prototype.county = "China";
person.prototype.city = "成都";
person.prototype.sayHello = function() {
console.log(hello ${this.name}
)
};
person1.sayHello() // hello dayday
上面代码中先是实例化了一个person1实例,接着再为原型对象添加了一个sayHello()方法,此时调用再通过person.sayHello依然可以访问到原型对象的sayHello()方法。
person.prototype = {
sayHello: function() {
console.log(hello ${this.name}
)
}
}
person1.sayHello(); // 报错
// 修改person1的proto属性,让其指向新的原型对象
person1.proto = person.prototype;
person1.sayHello(); // hello dayday
上面代码中采用字面量赋值的方法为person重新赋值了一个原型对象,由于实例化操作在赋值操作之前,因此person的__proto__属性依旧指向原来的原型对象,因此无法访问到sayHello()方法。
如果要让person1实例可以访问新的原型对象,有两种方法:1、将new person()操作放在原型对象赋值操作之后 2、动态的修改person1实例的__proto__属性,让其指向新的原型对象。
6.原生对象原型:在js中原型模型不仅仅应用在创建对象上,连原生的对象(Array、Object、String)都采用了这种模式,都是在其构造函数的原型上定义了方法。例如Array对象的sort()方法,String对象的subString()方法等。当内置方法不能满足开发需求时,我们也可以在原生对象的原型对象上自定义一些方法。
console.log(string.proto) // String {"",length: 0,constructor: ƒ,anchor: ƒ,big: ƒ,blink: ƒ, …}
console.log(array.proto) // [constructor: ƒ,concat: ƒ,pop: ƒ,push: ƒ,shift: ƒ, …]
// 在String对象的原型对象上自定义一个startWith()方法
String.prototype.startWith = function(text) {
return this.indexOf(text) == 0;
}
console.log(string.startWith("a")) // true
console.log(string.startWith("b")) // false
7.有了原型模式的基础,此时再来看看创建对象的几种方式,以及各自的优劣
工厂模式:
let person1 = Person("dayday",20,"man");
person1.sayHello() // hello dayday
console.log(person1 instanceof Object) // true
上面Person就是一个工厂函数,传入什么样的参数,Person就会返回一个具有相应属性的对象。工厂模式虽然解决了创建多个类似对象的问题,但是却无法解决对象识别的问题,即创建出来的所有对象都来自Object对象。同时创建的所有对象之间不存在联系,也无法共享公共方法。
构造函数模式:
let person1 = new Person("dayday","man");
let person2 = new Person("huahua",18,"woman");
person1.sayHello(); // hello dayday
console.log(person1 instanceof Person) // true,解决了对象识别的问题
console.log(person1.sayHello == person2.sayHello) // false,实例之间不能共享方法
通过new操作符实例化一个对象时,实际上经历了以下几个步骤:
(a)创建一个新对象
(b)将构造函数作用域赋值给新对象(让thisthis指向新对象)
(c)运行构造函数中的代码(给新对象添加属性值)
(d)返回新对象
构造函数模式通过new关键字创建了一个Person对象的实例,解决了对象识别的问题,同时也可以创建多个相似对象,但是构造函数在实例化对象时每个对象的方法都被实例化了一遍,浪费了内存没有达到共享方法的目的。
需要注意的是,构造函数也是普通的函数,如果不使用new构造符直接调用构造函数,即相当于window对象调用了构造函数,因此属性会被添加到window对象上。
组合原型模式:
let person1 = new Person("dayday","woman");
person1.sayHell(); // hello dayday
console.log(person1.sayHell == person2.sayHell) // true
组合构造模式顾名思义结合了原型模式和构造函数模式,将共享的方法放在原型对象上,所有的实例都可以通过原型链访问到共享方法,且一旦原型对象上的属性改变,可以在实力上立即得到反映,组合构造模式解决了传参、创建多个类似对象、共享方法的问题。
动态原型模式:
let person1 = new Person("dayday","woman");
person1.sayHell(); // hello dayday
console.log(person1.sayHell == person2.sayHell) // true
动态原型模型将原型创建原型方法这一步骤放到了构造函数体内,同时只有在sayHello()方法不存在的情况下才会将其添加到原型中,也就是说这段代码只有在初次调用构造函数时才会执行。
寄生构造函数模式:
let person1 = new Person("dayday","man");
这种方法的原理实际和工厂模式的原理一样的,都是返回一个新的对象。
稳妥构造函数模式:
函数体内定义私有变量和方法
let count = 99;
o.setCount = function(newCount) {
count = newCount;
}
o.getCount = function() {
return count;
}
return o;
}
let person1 = new Person("dayday","man");
console.log(person1.getCount()); // 99
person1.setCount(100)
console.log(person1.getCount()); // 100
使用这种方法创建的对象,在构造函数中如果要添加私有属性,可以之间将属性以变量的形式存储在函数体内,这样该变量就只能通过对象的某个方法访问。例如代码中的count变量只能通过调用person1.getCount()方法进行访问,同时也只能通过person1.setCount()方法进行修改。
8.继承
前面介绍了js中的原型链机制,在js中继承主要就是依靠原型链来实现的。
父类
function SuperType() {
this.type = "superType";
}
// 给父类原型对象添加方法
SuperType.prototype.getType = function() {
return this.type;
}
// 子类
function SubType() {
this.type = "subType";
}
// 父类的实例赋值给子类的原型对象(实现了子类对父类的继承)
SubType.prototype = new SuperType();
let instance = new SubType(); // 实例化一个对象
console.log(instance.getType()) // subType
console.log(instance.proto == SuperType.prototype) // true
上面代码中父类SuperType拥有type属性,父类的原型对象拥有gettype()方法,子类SubType也拥有type属性(也可称作重写了父类的type属性),将父类SuperType的一个实例赋值给子类SubType的原型对象即实现了子类对父类的继承。在实例化一个子类的实例时,实例可以通过原型链查找自身的属性和方法,当自身没有要查找的属性时,则会沿着原型链查找原型上的属性,找到则返回响应的属性。
上面的代码还存在着一个问题,那就是我们把父类SuperType的实例赋值给了子类SubType的原型,此时SubType.prototype.constructor是指向SuperType的,因此我们需要再增加一句SubType.prototype.constructor = SubType;让子类原型的construtor重新指向SubType子类。
谨慎的定义方法:有时我们需要在子类中重写父类中的某个方法或者添加父类中没有的方法,此时一定要注意,重写方法语句必须放在替换原型语句之后,不然会导致重写或新添加的方法无效。例如:
function SubType() {
this.type = "subType";
}
SubType.prototype.getType = function() {
return this is ${this.type}
;
}
SubType.prototype = new SuperType();
/ 不能使用字面量方式来重写方法,这样会导致子类的原型对象被重写赋值
SubType.prototype = {
getType: function() {
return this is ${this.type}
;
}
}
/
let instance = new SubType();
console.log(instance.getType()) // type,依然是父类中的gettype()方法。
同样需要注意的是,重写或添加新方法时,不能使用字面量的方式为子类原型赋值,这样会造成原型链被重写,父类和子类之间的关系被破坏。
原型链继承的缺点:
我们使用原型链机制虽然实现了继承,但是依然存在一些问题:当父类中的某个属性的值是引用类型时,我们为了让子类继承的属性值是相互独立的,需要将该属性放在构造函数中,这样就能保证每个子类不会共享该属性。但是当我们实例化一个子类时,同样存在相同的问题,即子类的每个实例会共享子类的属性。
属性
}
SuperType.prototype.getType = function() {
return this.type;
}
function SubType() {
this.type = "subType";
}
SubType.prototype.getType = function() {
return this is ${this.type}
;
}
SubType.prototype = new SuperType();
let instance1 = new SubType();
let instance2 = new SubType();
instance1.colors.pop(); // 修改instance1的color属性
console.log(instance2.colors); // ["red","yellow"],instance2中的colors属性也被修改
使用原型链实现继承还存在一个问题:不能像超类构造函数中传参,因此在实践中很少会单独使用原型链实现继承。
借用构造函数模式:
function SubType(name) {
SuperType.call(this,name);
this.type = "subType";
}
let instance1 = new SubType("dayday");
let instance2 = new SubType("huahua");
instance1.colors.pop();
console.log(instance1.colors); // ["red","yellow"]
console.log(instance2.colors); // ["red","green"]
console.log(instance1.type); // subType
console.log(instance1.name); // dayday
console.log(instance2.type); // subType
console.log(instance2.name); // huahua
上面例子中使用了call函数让父类构造函数在子类的执行环境中执行,因此让子类拥有了父类中定义的属性,这种借用构造函数的方式解决了向父类传参的问题,同时也解决了子类实例共享子类引用类型属性的问题。但是单单只用构造函数的方法还是存在问题:所有的属性方法都在构造函数中定义的,不能实现公共方法的共享,即没有实现方法的复用,浪费内存。
function SubType(name) {
SuperType.call(this,name); // 第二次调用父类构造函数
this.type = "subType";
}
// 继承父类属性、方法
SubType.prototype = new SuperType("dayday"); // 第一次调用父类构造函数
// 实例化
let instance1 = new SubType("dayday");
let instance2 = new SubType("huahua");
instance1.colors.pop();
console.log(instance1.colors); // ["red","yellow"]
console.log(instance2.colors); // ["red","green"]
console.log(instance1.getName()); // dayday
console.log(instance2.getName()); // huahua
console.log(instance2.getName == instance1.getName); // true
上面例子综合了原型链和构造函数的优点,比较完美的实现了子类到父类的继承,实现了向父类传参、方法共享、属性独立的特点。组合继承唯一的缺点在于调用了两次构造函数(为子类原型赋值时调用一次,子类构造函数中又调用了一次),生成了两份实例(第一次调用new SuperType()构造函数时,子类的原型得到了三个name、type、colors属性,第二次调用SuperType()时,子类的构造函数中又得到了name、type、colors属性,因此实际上得到了两份相同的属性,只是构造函数中的属性将原型中的属性覆盖了)。
原型式继承:
let person = {
name: "dayday",age: 20
}
let student = createObject(obj);
上面代码就实现了studtent到person的继承,这种方法studtent和person依然是共享引用类型属性。在ES5中新增的Object.create()方法实际上就是这种原型式继承,该方法接受两个参数,第一个参数是用作新对象原型的对象,第二个对象包含了新对象需要新增的属性和方法。
console.log(student.name); // dayday
寄生组合式继承:
函数
SubType.prototype = prototype; // 子类原型直接被赋值为父类原型,砍掉了子类原型中的父类构造函数属性
}
function SuperType(name) {
this.name = name;
this.type = "superType";
this.colors = ["red",// 重新更正SuperType.prototype中的constructor指向
getName: function() {
return this.name;
}
}
function SubType(name) {
SuperType.call(this,name);
this.type = "subType";
}
inheritPrototype(SubType,SuperType); // 省略了将父类实例赋值给子类原型这一步骤
let instance1 = new SubType("dayday");
let instance2 = new SubType("huahua");
console.log(instance1.name) // dayday
console.log(instance2.name) // huahua
console.log(instance1.getName()) // dayday
console.log(instance2.getName()) // huahua
console.log(instance1.getName == instance2.getName) // true