js的面向对象
面向对象(oop)
javascript没有类的概念,但是javascript通过构造函数来实现类,通过原型链实现继承。
面向对象的三大特性:
-
封装:将对数据的操作细节隐藏起来,只暴露对外的接口,外界调用端不需要(也不可能)知道细节,就能通过对外提供的接口来访问该对象。同时也保证外界无法任意改变对象内部的属性和方法
-
继承:子类继承父类,子类除了拥有父类的所有特性,还有一些自己的具体方法
-
多态:由继承而产生了相关的不同的类,对同一个方法可以有不同响应
1、创建对象
1.1 工厂模式
工厂模式就是,创建一个函数,用函数来封装以特定接口创建对象的细节
function createPerson(name, age, job) { var o = new Object(); o.name = name; o.age = age; o.job = job; o.say = function (){ console.log(this.name) } return o; } var person1 = createPerson('sun', '23', 'doctor'); var person2 = createPerson('sang', '25', 'teacher')
优点:可以创建多个相似对象的问题
缺点:只能返回固定的属性和方法
1.2 构造函数模式
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.say = function (){ console.log(this.name) } } var person3 = new Person('sun', '23', 'doctor'); var person4 = new Person('sang', '25', 'teacher'); // 可以将他的实例标识为一中特定的类型、 person3 instanceof Person // true person3 instanceof Object // true
优点:没有显式的直接创建对象,可以将他的实例标示为一种特定的类型
缺点:每个方法都在每个实例上重新创建了一遍,比如:上述的say方法,实例person3和person4时,都会创建一遍。
注:new操作符做了以下步骤:
-
创建一个新对象
-
将构造函数中的作用域赋值给新对象
-
执行构造函数中的代码
-
返回新对象
为啥叫构造函数:
person3和person4实例中都有一个构造函数(constructor)属性,他指向Person.
即:person3.constructor === Person; person4.constructor === Person
1.3 原型模式
相同的属性,放置在构造函数上。而通用的方法等,放置在其原型上,解决上述构造函数缺点。
function Person(name, age, job) { this.name = name; this.age = age; this.job = job } Person.prototype.sayName = function () { console.log(this.name) } var person5 = new Person('sun', '23', 'doctor'); var person6 = new Person('sang', '23', 'teacher')
优点:让所有对象实例共享它所包含的属性和方法,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中
1.3.1 原型对象
每当我们创建一个函数,就会为该函数创建一个prototype原型属性,这个属性指向的是该函数的原型对象,在默认情况下,所有原型对象都会自动获得一个constructor构造函数属性,它指向的 是prototype属性所在函数的指针,我们通过构造函数,可以继续为原型对象添加其他属性和方法。
在每个原型的实例中,都有一个__proto__属性,指向的是该实例的原型对象prototype
以下为构造函数 + 原型对象模式, 集两者之长
// 构造函数 function Person(name, age, job) { this.name = name; this.age = age; this.job = job; } // 原型 Person.prototype.say = function () { console.log(this.job) } Person.prototype.a = 'bc' var person1 = new Person('sun', '23', 'teacher') var person2 = new Person('sang', '22', 'doctor') console.log(Person.prototype.constructor) // Person console.log(person1.constructor) // Person // Person{say: Function , a: 'bc' } console.log(person1.__proto__)
构造函数,原型对象和实例的关系如下:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针
1.3.2 原生对象原型
所有原生对象的引用类型,都是采用原型模式来创建的
例如:在Array.prototype中可以找到sort() , 在String.prototype中可以找到substring(),
我们可以介于此,来创建新的方法,或者改变原来的方法
例如:数组中新增删除方法
Array.prototype.delete = function (v){ var index = this.indexOf(v); if (index > -1) { this.splice(index, 1) } else { console.log('error') } } var arr = [1,2,3,4,5] arr.delete(1) console.log(arr) // [2,3,4,5]
1.3.3 判断属性来自实例还是原型
-
in 操作, 无法判断是否是实例属性还是原型属性,只能判断该对象是否有该属性
-
hasOwnProperty() 判断属性来自实例属性还是原型属性
// 构造函数 function Person(name, age, job) { this.name = name; this.age = age; this.job = job; } // 原型 Person.prototype.say = function () { console.log(this.job) } Person.prototype.a = 'bc' var person1 = new Person('sun', '23', 'teacher') person1.cc = 'sa' console.log(person1.hasOwnProperty('cc')) // true 来自实例 console.log(person1.hasOwnProperty('name')) // true 来自构造函数 console.log(person1.hasOwnProperty('a')) // false 来自原型 console.log('name' in person1) // true console.log('cc' in person1) // true
2、继承
2.1 原型链
原型链:利用原型让一个引用类型继承另一个引用类型的属性和方法, 其为实现继承的主要方法
function Person() { this.name = 'sang' } Person.prototype.say = function (){ console.log(`I'm ${this.name}`) } function Work() { this.job = 'teacher' } // 继承了Person Work.prototype = new Person() Work.prototype.do = function (){ console.log(`I'm ${this.name} and I'm a ${this.job}`) } var sang = new Work() sang.say() // I'm sang sang.do() // I'm sang and I'm a teacher console.log(sang.__proto__ === Work.prototype) // true console.log(sang.__proto__ === Person.prototype) // false
定义了两个类型:Person和Work, 每个类型有一个属性和方法,Work类型的原型继承了Person(通过创建Person的实例), 所以可以在Work中访问到Person的属性和方法
其实例的__proto__ 指向Work的原型。
以下为上述案例的关系
2.2 默认原型
在之前我们知道,所有原生对象的引用类型,都是采用原型模式来创建的。同时,所有引用类型默认都继承了Object,而这个继承也是通过原型链实现的
所有函数的默认类型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype. 这也正是所有自定义类型都会继承toString(),valueOf()等默认方法的根本原因
所以,上述案例的关系,可以改为:
所以:Work继承了Person, 而Peron继承了Object
2.3 确定原型和实例的关系
可以通过两种方式来确定原型和实例的关系
-
instanceof 测试实例与原型链中出现的构造函数,如果出现过,则返回true
-
isPrototypeOf 只要是原型链找那个出现过的原型,都可以说是该原型链所派生的实例的原型,则返回true.
console.log(sang instanceof Object) // true console.log(sang instanceof Person) // true console.log(sang instanceof Work) // true console.log(Object.prototype.isPrototypeOf(sang)) // true console.log(Person.prototype.isPrototypeOf(sang)) // true console.log(Work.prototype.isPrototypeOf(sang)) // true
3、ES6中的类
ES6中,为为我们提供了一个比较传统的方式,引入了Class, 即类的概念,作为对象模板,通过class关键字,来定义类,通过constructor,来定义构造函数
3.1 类的用法
class Person{ // 构造函数 constructor(name, age) { this.name = name; this.age = age; } say(){ console.log(`I'm ${this.name}`) } } let sang = new Person('sang', 23) sang.say() // I'm sang
构造函数的prototype属性,在 ES6 的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype属性上面
在类的实例上面调用方法,其实就是调用原型上的方法
constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。
其中,他的Person.prototype.constructor还是指向构造函数Person:
console.log(Person.prototype.constructor === Person) // true // 其实例的__proto__ 依旧指向他的原型 console.log(sang.__proto__ === Person.prototype) // true
3.2 class的继承
使用extends关键字,来实现继承
class Person{ // 构造函数 constructor(name, age) { this.name = name; this.age = age; } say(){ console.log(`I'm ${this.name}`) } } let sang = new Person('sang', 23) sang.say() // I'm sang // extends关键字, 继承了Peron所有的属性和方法 class Work extends Person{ constructor(name, age, job) { super(name, age) // 调用父类的constructor(name, age) this.job = job } do() { console.log(`I'm ${this.name} and I'm a ${this.job}`) } } let sun = new Work ('sun', '23', 'teacher'); sun.do() //I'm sun and I'm a teacher console.log(sun instanceof Object) // true console.log(sun instanceof Person) // true console.log(sun instanceof Work) // true console.log(Object.prototype.isPrototypeOf(sun)) // true console.log(Person.prototype.isPrototypeOf(sun)) // true console.log(Work.prototype.isPrototypeOf(sun)) // true
class定义的类中,依旧可以使用instanceof 和 isPrototypeOf 来判断原型和实例的关系。
super关键字有两种情况:1.作为函数使用, 2.作为对象使用
-
作为函数使用, 在继承时,在constructor内调用,代表父类的构造函数,只能在子类的constructor中使用
class A {}
class B extends A {
constructor() {
super();
}}
虽然代表的是其父类的构造函数,但是返回的是父类的实例,所以:这里的super()相当于A .prototype.constructor.call(this)
-
作为对象使用, 在普通方法中,指向父类的原型对象;在静态方法中,指向父类
class A {} A.prototype.x = 2; class B extends A { constructor() { super(); console.log(super.x) // 2 }} let b = new B();
以上的x是定义在A的原型中,所以super.x可以取到他的值, 这里的super就是作为对象使用,指向的是他的父类原型对象,
静态方法: static 直接使用
class A { constructor() { this.x = 1; } static print() { console.log(this.x); } } class B extends A { constructor() { super(); this.x = 2; } static m() { super.print(); } } B.x = 3; // 静态方法, 直接调用 // 其中,他的this指向的是B, 而不是B的实例 B.m() // 3
在静态方法中, super作为对象使用,他指代的就是他的构造函数,