目录
一、面向对象的程序设计
创建对象的两种方式:
/* 第一种创建对象的方式 */
let person = new Object()
person.name = 'zs'
person.age = 20
person.sayName = function () {
console.log(this.name);
}
console.log(person);
person.sayName()
/* 第二种创建对象的方式 */
let phone = {
brand: '小米',
price: 1999,
sayPrice() {
console.log(this.price);
}
}
console.log(phone);
phone.sayPrice()
结果:
工厂函数:
function createPhone(brand, price) {
let obj = new Object()
obj.brand = brand
obj.price = price
obj.sayPrice = function () {
console.log(this.price);
}
return obj
}
let phone1 = new createPhone('小米', 1999)
let phone2 = new createPhone('华为', 4999)
console.log(phone1);
phone1.sayPrice()
console.log(phone2);
phone2.sayPrice()
结果:
构造函数:与工厂函数不同的是,没有显示的创建对象,直接将属性和方法赋给了this对象,没有return。
function createPhone(brand, price) {
this.brand = brand
this.price = price
this.sayPrice = function () {
console.log(this.price);
}
}
let phone1 = new createPhone('小米', 1999)
let phone2 = new createPhone('华为', 4999)
console.log(phone1);
phone1.sayPrice()
console.log(phone2);
phone2.sayPrice()
结果:
这种方式调用构造函数实际上会经历以下四个步骤:
(1)、创建一个新对象
(2)、将构造函数的作用域赋给新对象,此时this就指向了这个新对象
(3)、为这个新对象添加属性和方法
(4)、 返回新对象
将构造函数当做普通函数:
任何函数,只要通过new操作符来调用,那他就可以作为构造函数,而任何函数,如果不通过new操作符调用,那么它跟普通函数没有区别。
function createPhone(brand, price) {
this.brand = brand
this.price = price
this.sayPrice = function () {
console.log(this.price);
}
}
/* 作为构造函数使用 */
let phone1 = new createPhone('小米', 1999)
let phone2 = new createPhone('华为', 4999)
console.log(phone1);
phone1.sayPrice()
console.log(phone2);
phone2.sayPrice()
/* 作为普通函数使用 */
createPhone('一加', 3999)
console.log(window.brand, window.price);
window.sayPrice()
结果:
在全局作用域中调用构造函数时,如果不用new操作符调用构造函数,此时构造函数的指针就会指向window,将传经来的参数和方法挂载到window上,所以就可以直接调用window上的属性和方法。
function createPhone(brand, price) {
this.brand = brand
this.price = price
this.sayPrice = function () {
console.log(this.price);
}
}
/* 使用call改变this指向 */
let obj = {}
createPhone.call(obj, '苹果', 9999)
console.log(obj.brand, obj.price);
obj.sayPrice()
结果:
二、原型与原型链
我们创建的每个函数都有一个prototype原型属性,这个属性是一个对象,他的作用是包含可以有特定类型的所有势力共享的属性和方法,换句话说,不必在构造函数中定义对象信息,而是可以将这些信息直接添加到原型对象中。
function Person() { }
Person.prototype.name = 'zs'
Person.prototype.age = 20
Person.prototype.sex = '男'
Person.prototype.sayName = function () {
console.log('我的名字叫' + this.name);
}
let person1 = new Person()
let person2 = new Person()
console.log(person1.name === person2.name);
console.log(person1.age === person2.age);
console.log(person1.sex === person2.sex);
console.log(person1.sayName === person2.sayName);
console.log(person1);
console.log(person2);
结果:
当给实例赋一个和构造函数相同的属性或者方法时:那么构造函数就会屏蔽与实例相同的属性或者方法。当读取到某个对象的属性或者方法,解析器会会执行两次搜索,第一次搜索实例中是否有这个属性或者方法,如果没有,就会通过实例中的原型链寻找保存在原型中的属性或者方法。就算你给person1.name=null,它也不会影响person2.name,换句话说,你添加这个属性或者方法只会阻碍访问原型上的属性或者方法,并不会修改原型上的属性或者方法。使用delete操作符可以完全删除实例身上的属性或者方法,从而让我们重新访问到原型上的属性或者方法。
function Person() { }
Person.prototype.name = 'zs'
Person.prototype.age = 20
Person.prototype.sex = '男'
Person.prototype.sayName = function () {
console.log('我的名字叫' + this.name);
}
let person1 = new Person()
let person2 = new Person()
person1.name = 'ls'
console.log(person1.name);
console.log(person2.name);
person1.name = null
console.log(person1.name);
console.log(person2.name);
delete person1.name
console.log(person1.name);
console.log(person2.name);
结果:
原型与in操作符:
in操作符会在通过对象能够访问给定属性时返回true,无论该属性时存在于原型还是实例中。
hasOwnproperty()方法第一个参数必需加引号,如果存在于实例中,则返回true,否则返回false。
/* 封装检测这个属性是在实例上还是在原型上,
如果在原型上就放回false,否则放回true */
function isExistPrototype(object, property) {
return object.hasOwnProperty(property) && (property in object)
}
function Person() { }
Person.prototype.name = 'zs'
Person.prototype.age = 20
Person.prototype.sex = '男'
Person.prototype.sayName = function () {
console.log('我的名字叫' + this.name);
}
let person1 = new Person()
let person2 = new Person()
person1.name = 'ls'
console.log(isExistPrototype(person1, 'name'));
console.log(isExistPrototype(person2, 'name'));
结果:
更简单的原型语法:
前面中的例子每添加一个属性或者方法,就要敲一遍Person.prototype,为减少代码量,用如下方法来重写整个原型和对象。
function Person() { }
Person.prototype = {
name: 'zs',
age: 20,
sex: '男',
sayName() {
console.log(this.name);
}
}
let person1 = new Person()
let person2 = new Person()
console.log(person1);
console.log(person2);
结果:
这种写法和以前的写法创建的新对象相同,但是有一点不同,在实例化对象时,新创建的对象这种写法construcor就指向了Object,而以前的那种写法construcor还是指向了Person构造函数的原型上。
function Person1() { }
Person1.prototype = {
name: 'zs',
age: 20,
sex: '男',
sayName() {
console.log('我的名字叫' + this.name);
}
}
function Person2() { }
Person2.prototype.name = 'zs'
Person2.prototype.age = 20
Person2.prototype.sex = '男'
Person2.prototype.sayName = function () {
console.log('我的名字叫' + this.name);
}
let person1 = new Person1()
let person2 = new Person2()
console.log(person1 instanceof Object);
console.log(person1 instanceof Person1);
console.log(person1.constructor === Object);
console.log(person1.constructor === Person1);
console.log('~~~~~~~~~~');
console.log(person2 instanceof Object);
console.log(person2 instanceof Person2);
console.log(person2.constructor === Object);
console.log(person2.constructor === Person2);
结果:
如果constructor值真的很重要,那么我们可以将constructor值设置为Person。
function Person1() { }
Person1.prototype = {
constructor: Person1,
name: 'zs',
age: 20,
sex: '男',
sayName() {
console.log('我的名字叫' + this.name);
}
}
let person1 = new Person1()
console.log(person1 instanceof Object);
console.log(person1 instanceof Person1);
console.log(person1.constructor === Object);
console.log(person1.constructor === Person1);
结果:
原型的动态性:
先创建实例,后修改原型上的属性或者方法,依然可以访问到原型上的属性或者方法。
function Person() { }
let person = new Person()
Person.prototype.sayHi = function () {
console.log('hello world');
}
person.sayHi()
结果:
如果是重写整个原型,在重写原型之前,调用构造函数时,会为实例添加一个指向最初原型的指针,而把原型修改为另一个对象就等于切断了构造函数与最初原型之间的联系。实例中的指针只指向原型,而不指向构造函数。
function Person() { }
let person = new Person()
Person.prototype = {
constructor: Person,
name: 'zs',
age: 20,
sex: '男',
sayName() {
console.log('我的名字叫' + name);
}
}
person.sayName()
结果:
原生对象的原型:
所有的原生引用类型都在其原型上添加了方法,我们也可以在原型上添加自己的方法,但是,我们一般不推荐这么做,因为可能重写原生方法。
let str = 'hello world'
String.prototype.findChar = function (char) {
return this.indexOf(char)
}
console.log(str.findChar('e'));
结果:
原型对象的问题:
对于基本类型,在实例上添加属性,会屏蔽原型上的属性,其他实例在调用构造函数时,依然可以访问到原型上的属性。而对于引用类型,在实例中push或者shift一个数据,则会改变原型上的数组,这是非常危险的。
function Person() { }
Person.prototype = {
constructor: Person,
name: 'zs',
age: 20,
friends: ['李四', '王五'],
sayName() {
console.log('我的名字叫' + this.name);
}
}
let person1 = new Person()
let person2 = new Person()
person1.sayName = function () {
console.log('hello');
}
person1.sayName()
person2.sayName()
console.log('引用类型');
person1.friends.push('小明')
person1.friends.shift()
console.log(person1.friends);
console.log(person2.friends);
结果:
组合使用构造函数模式和原型模式:
实例属性在构造函数中定义,所有实例共享的方法则在原型上定义,这种混合模式不仅每一个实例有自己的实例属性副本,还可以共享对方法的引用,最大限度节省了内存。
function Person(name, age, job) {
this.name = name
this.age = age
this.job = job
this.friends = ['小明', '小强', '小红']
}
Person.prototype.sayName = function () {
console.log('我的名字叫' + this.name);
}
let person1 = new Person('小华', 20, '前端工程师')
let person2 = new Person('小王', 22, '后端工程师')
person1.friends.push('小绿')
person1.friends.shift()
console.log(person1);
person2.friends.unshift('小紫')
person2.friends.pop()
console.log(person2);
结果:
动态原型模式:
可以通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。
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);
}
}
}
let person = new Person('小明', 22, '前端工程师')
person.sayName()
结果:
寄生构造函数模式:
该函数的作用仅仅是封装创建对象的代码。然后再返回新创建的对象。
除了使用new操作符,这个模式和工厂模式是一样的。
假如我们想创建一个具有额外方法的特殊数组,由于不能直接修改Array构造函数。
function specialArray() {
let arr = []
arr.push.apply(arr, arguments)
arr.toPipedString = function () {
return arr.join('|')
}
return arr
}
let colors = new specialArray('red', 'green', 'blue')
console.log(colors.toPipedString());
结果:
稳妥构造函数模式:
没有公共属性,而且其他方法也不引用this对象。稳妥对象最适合在一些安全的环境中,或者防止数据被其他应用程序引用,稳妥构造函数模式遵循与寄生构造函数模式类似的模式。有两点不同:一是新创建对象的实例方法不引用this,二是不使用new操作符调用构造函数。
/* 稳妥构造函数模式 */
function Person1(name, age, job) {
let o = {}
o.name = name
o.age = age
o.job = job
o.sayName = function () {
console.log('我的名字叫' + name);
}
return o
}
let person1 = Person1('李强', 25, '前端工程师')
/* 除了调用sayName外,没有什么方法可以调用其他数据成员 */
person1.sayName()
结果:
三、继承
由于函数没有签名,在js中无法实现接口继承,js只支持实现继承,而且其实现继承主要是依靠原型链来实现的。
原型链:假如我们让原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型的指针,另一个原型中也包含着一个指向另一个构造函数的指针,层层递进,就构成了实例与原型的链条。
function SuperType() {
this.property = true
}
SuperType.prototype.getSuperValue = function () {
return this.property
}
function subType() {
this.subproperty = false
}
subType.prototype = new SuperType()
subType.prototype.getSubValue = function () {
return this.subproperty
}
let instance = new subType()
console.log(instance.getSuperValue());//true
console.log(instance.getSubValue());//false
访问instance会经历三个搜索步骤,首先instance会搜索自身是否自定义了一个属性,如果没有,就搜索instance原型上SubType Prototype上有没有这个属性,如果还是没有,就会搜索SubType Prototype原型上Supertype Prototype有没有这个属性。
别忘记默认原型:
事实上,前面例子中展示的原型链少了一环,所有引用类型默认窦继承了Object,而这个继承也是通过原型链实现继承的,因此默认原型都会包含一个内部指针,指向Object.prototype,这就是为什么所有自定义类型都会继承toString等等方法。
确定原型和实例的关系:
使用instanceof操作符,只要用这个操作符来测试实例与原型链中出现过的构造函数,结果就会返回true。
instance instanceof Object//true
instance instanceof SuperType//true
instance instanceof subType//true
使用isPropertyOf方法,只要原型链中出现过的原型,都可以说是该原型链所派生的实例的原型,因此isPropertyOf方法也会返回true。
Object.prototype.isPrototypeOf(instance)//true
SuperType.prototype.isPrototypeOf(instance)//true
subType.prototype.isPrototypeOf(instance)//true
谨慎地定义方法:
子类型中有时候需要重写超类型中的某个方法,或者需要添加超类型中不存在的某个方法,但不管怎么样,给原型添加方法的代码一定要放在替换原型的语句之后。换句话说就是,给原型添加方法的代码一定要放在继承的语句之后。
function Person() {
this.human = true
}
Person.prototype.getHuman = function () {
return this.human
}
function Woman() {
this.man = false
}
/* 继承了Person */
Woman.prototype = new Person()
//添加新方法,如果这个方法写在了继承之前,则会报错
Woman.prototype.getMan = function () {
return this.man
}
//重写超类型中的方法,如果这个方法写在了继承之前,则会修改失败
Woman.prototype.getHuman = function () {
return false
}
let people = new Woman()
console.log(people.getHuman());//false
console.log(people.getMan());//false
原型链的问题:
在构造函数中定义的属性,每个实例都会有包含自己的color属性,但是,通过原型链继承时,原型实际上会变成另一个类型的实例,原先的实例属性就会变成现在的原型属性。
在创建子类型的实例时,不能向超类型的构造函数中传递参数。
function Father() {
this.colors = ['red', 'green', 'blue']
}
function Son() { }
Son.prototype = new Father()
let son1 = new Son()
son1.colors.push('purple')
console.log(son1.colors);
let son2 = new Son()
console.log(son2.colors);
结果:
借用构造函数:
在子类型构造函数的内部调用超类型构造函数。
function Father() {
this.colors = ['red', 'green', 'blue']
}
function Son() {
/* 继承了Father */
Father.apply(this)
}
let son1 = new Son()
son1.colors.push('purple')
console.log(son1.colors);
let son2 = new Son()
console.log(son2.colors);
结果:
传递参数:
相对于原型链而言,借用构造函数有一个很大的优势,即可以在子类型构造函数中向超类型构造函数中传递参数。
function Father(name) {
this.name = name
}
function Son() {
/* 继承了Father,还传递了参数 */
Father.apply(this, ['小明'])
}
let son1 = new Son()
console.log(son1.name);//小明
借用构造函数问题:
仅仅是借用构造函数,那么也将无法避免构造函数模式存在的问题,方法都在构造函数中定义,因此函数复用无从谈起。
组合继承:
有时候也叫伪经典继承,将原型链和借用构造函数的技术组合到一块。使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。
function Father(name, age) {
this.name = name,
this.age = age
this.colors = ['red', 'green', 'blue']
}
Father.prototype.sayName = function () {
console.log(this.name);
}
function Son(name, age) {
//继承属性
Father.apply(this, arguments)
}
//继承方法
Son.prototype = new Father()
/* 定义子类型的方法 */
Son.prototype.sayAge = function () {
return this.age
}
let people1 = new Son('zs', 20)
people1.colors.push('purple')
console.log(people1);
console.log(people1.sayAge());
people1.sayName()
let people2 = new Son('ls', 30)
console.log(people2);
console.log(people2.sayAge());
people2.sayName()
结果:
原型式继承:
基于已有的对象创建新对象,同时还不必因此创建自定义的类型。在构造函数内部,先创建一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的新实例。
function object(o) {
function temporary() { }
temporary.prototype = o
return new temporary()
}
let person = {
name: '小明',
friends: ['小红', '小强', '小兰']
}
let people1 = object(person)
people1.name = '小绿'
people1.friends.push('小紫')
console.log(people1);
let people2 = object(person)
people2.name = '小黄'
people2.friends.push('小白')
console.log(people2);
结果:
只想让一个对象与另一个对象保持一致的话,原型式继承时完全能够胜任的。
寄生式继承:
创建一个仅用于封装继承过程的函数,该函数在内部已某种方式来增强对象,最会返回对象,适用于能够返回新对象的函数窦适用于此模式。
function Handler(o) {
let obj = Object(o)
obj.sayHi = function () {
console.log('hi');
}
return obj
}
let person = {
name: 'zs',
friends: ['ls', 'ww']
}
let perpel = new Handler(person)
perpel.sayHi()//hi
寄生式组合继承:
组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。 而寄生式组合继承就解决了调用两次超类型的构造函数。寄生式组合式继承,通过借用构造函数来继承属性,通过原型链的混合形式来继承方法。
寄生式组合式继承的最简单形式的函数:
函数接受两个参数:子类型构造函数和超类型构造函数。在函数内部,第一步是创建超类型原型的一个副本,第二个是为创建的副本添加constructor属性,将新创建的对象赋值给子类型的原型。
//寄生式组合继承
function ParasiticCompositeInheritance(father, son) {
/* 创建超类型原型的第一个副本 */
let supertypeCopy = Object(father.prototype)
/* 为创建的副本添加constructor属性 */
supertypeCopy.constructor = son
/* 将超类型的副本赋值给子类型的原型 */
son.prototype = supertypeCopy
}
function Supertype(name) {
this.name = name
}
Supertype.prototype.sayName = function () {
return this.name
}
function SubType(name, age) {
Supertype.call(this, name)
this.age = age
}
ParasiticCompositeInheritance(Supertype, SubType)
SubType.prototype.sayAge = function () {
return this.age
}
let person1 = new SubType('zs', 20)
console.log(person1);
console.log(person1.sayAge());
console.log(person1.sayName());
let person2 = new SubType('ls', 30)
console.log(person2);
console.log(person2.sayAge());
console.log(person2.sayName());
结果: