封装
工厂模式创建对象
function createPerson(name, age){
let obj = {};
obj.name = name;
obj.age = age;
obj.sayHi= function(){
console.log('My name is '+ name +'!');
}
return obj;
}
let p1 = createPerson('Sam', 18);
p1.sayHi() //My name is Sam!
构造函数模式创建对象
function Person(name, age){
this.name = name;
this.age = age;
}
Person.prototype.sayHi = function(){
console.log('My name is '+ this.name +'!');
}
let p2 = new Person('ayu', 22);
p2.sayHi() //My name is ayu!
其实内部流程跟上面的工厂模式差不多,主要有几点区别:
- Person称为类,约定俗成以大驼峰命名方式,Person这个函数叫做构造函数。
- 构造函数有个 prototype 属性,这个就是对象原型(存放实例公共方法),JS中主要以原型方式实现继承。
- 用 new 关键字方式创建实例,代码中的 p2 称为Person类的实例。
- 构造函数模式下,如果函数不需要传参 new Person() 与 new Person 效果一样。
注意:任何类的类型都是function,任何实例的类型都是object
那么构造函数模式创建对象的整个流程大致是:
- 浏览器默认创建一个空对象
- 构造函数体代码由上往下执行,this 指向该对象
- 原型指向
- 返回该对象(如果构造函数内写有返回值且返回值类型为object,那么new出来的也是该返回值而不是类的实例)
依照这个流程我们也可以手动实现一个 new 关键字效果:
/**
* @param {function} Fn 构造函数
* @param {...any} arg 一些其他参数
*/
function _new(Fn, ...arg){
let instance = {};
instance.__proto__ = Fn.prototype; //你也可以用Object.create
let res = Fn.call(instance, ...arg);
return typeof res === 'object' ? res : instance;
}
let p3 = _new(Person, 'lala', 19);
p3.sayHi() //My name is lala!
ES6
ES6提供了class关键字,所以现在可以用class来定义类,不过这种方式实际上是构造函数方式的语法糖。
class Person{
//构造函数
constructor(name, age){
this.name = name;
this.age = age;
}
//原型上的方法,不能用键值对方式
sayHi(){
console.log('My name is '+ this.name +'!');
}
//静态方法,不能是静态属性
static aa(){...}
}
ES6用class封装类有几点注意:
- 类名不能直接当普通函数使用,也就是说构造函数模式的Person有类名、普通函数、普通对象3个身份,而class模式的Person只有类名、普通对象两个身份。
- class体内定义的函数会绑到原型上,不可用键值对方式定义。
- class体内可用static关键字定义静态方法,但不能是静态属性。
- class体外依然可以像平时一样用Person.prototype.xx方式扩展。
继承
原型
1、每一个函数都有一个天生自带的属性 prototype(称为显示原型),并且该属性是个对象数据类型。
2、prototype 身上也天生自带一个属性 constructor,属性值是当前构造函数本身。
3、每一个对象数据类型也天生自带一个属性 __proto__ (称为隐式原型),属性值是当前实例所属类的原型(prototype)。
4、Object 在JS中是所有对象数据类型的基类,Object.prototype上没有 __proto__ 属性。
注意:函数在JS中比较特殊。函数作为 Function 的实例,自然也有 __proto__ 属性,函数.__proto__ 指向 Function.prototype。Object构造函数是 Function 的实例,但 Function.prototype又是一个Object的实例。
原型链
还是参考上图
通过 对象名.属性名 方式获取属性值的时候,首先会在对象的私有属性上查找,如果私有属性中存在,则获取的是私有属性值。
如果私有属性中没有,则通过对象的 __proto__ 找到所属类的原型(原型上定义的属性和方法都是类的实例共有的),如果原型上有,则获取原型上的属性值。
如果原型上没有,则继续通过原型的 __proto__ 到下一层查找,一直找到 Object.prototype 为止。
这种查找模式叫作原型链模式。
注意:IE浏览器中禁止手动操作 __proto__,因为每个实例都有 __proto__ 属性,而 __proto__ 指向的原型对象是共有的,要想操作这个对象用 类.prototype 操作就行了。
几种继承方式
上面提到过,JS中主要以原型来实现继承,实际下面几种方式都是围绕着这个中心思想。
就像html文档中的DOM元素一样也是原型链继承的:
div.__proto__ = HTMLDivElement.prototype
HTMLDivElement.prototype.__proto__ = HTMLElement.prototype
HTMLElement.prototype.__proto__ = Element.prototype
Element.prototype.__proto__ = Node.prototype
Node.prototype.__proto__ = EventTarget.prototype
EventTarget.prototype.__proto__ = Object.prototype
原型继承法
子类原型是一个父类的实例
//父类
function Animal(height){
this.height = height;
}
Animal.prototype.walk = function(){
console.log('I can walk')
}
//子类
function Person(name, age){
this.name = name;
this.age = age;
}
Person.prototype = new Animal;
Person.prototype.constructor = Person;
let p1 = new Person('Sam', 22)
p1.walk() //I can walk
该方式优缺点一目了然,继承了父类的共有方法,但没有继承父类的私有属性。
call继承法
解决上面存在的继承父类私有属性。在子类构造函数中直接调用父类构造函数(用call以普通函数形式执行)。
//父类
function Animal(height){
this.height = height;
}
Animal.prototype.walk = function(){
console.log('I can walk')
}
//子类
function Person(name, age, height){
this.name = name;
this.age = age;
Animal.call(this, height);
}
let p1 = new Person('Sam', 22, 180)
console.log(p1.height) //180
该方式缺点也一目了然,继承了父类的私有属性,但却没有父类原型上的方法。强迫症都犯了,为什么不组合一下上面两种方式,忍一忍,别人的文章都是细分的,我也要细分,笑哭。
冒充对象继承法
说白了就是将父类的所有属性 for in 遍历克隆一份给子类。
//父类
function Animal(height){
this.height = height;
}
Animal.prototype.walk = function(){
console.log('I can walk')
}
//子类
function Person(){
let temp = new Animal;
for(let key in temp){
this[key] = temp[key]
}
}
该方式缺点还是一目了然,将父类的私有共有属性方法都克隆给子类私有。
混合继承法
组合原型继承与call继承
//父类
function Animal(height){
this.height = height;
}
Animal.prototype.walk = function(){
console.log('I can walk')
}
//子类
function Person(name, age, height){
this.name = name;
this.age = age;
Animal.call(this, height)
}
Person.prototype = new Animal;
Person.prototype.constructor = Person;
let p1 = new Person('Sam', 18, 179);
p1.walk() //I can walk
已经逐渐趋近完美了,就是子类原型上还有父类的私有属性,虽不碍事,但总不完美。
寄生组合式继承(推荐)
名字看看就好,实际就是借助 Object.create()
//父类
function Animal(height){
this.height = height;
}
Animal.prototype.walk = function(){
console.log('I can walk')
}
//子类
function Person(name, age, height){
this.name = name;
this.age = age;
Animal.call(this, height)
}
Person.prototype = Object.create(Animal.prototype);
Person.prototype.constructor = Person;
let p1 = new Person('Sam', 21, 183);
p1.walk() //I can walk
完美
简单粗暴继承法
子类原型直接指向父类原型
//父类
function Animal(height){
this.height = height;
}
Animal.prototype.walk = function(){
console.log('I can walk')
}
//子类
function Person(name, age){
this.name = name;
this.age = age;
}
Person.prototype = Animal.prototype;
Person.prototype.constructor = Person;
let p1 = new Person('Sam', 18);
p1.walk() //I can walk
ES6
语法相当简单,实际效果类似上面的寄生组合式继承法
class Person extends Animal{
constructor(){
super() //类似call继承
...
}
...
}
多态
多态意思是方法的多种形态。主要分为重载和重写。
重载:方法名相同,参数不同。明确表示JS中不存在重载。你可以函数内部通过判断参数数量、类型来达到类似重载的效果。
重写:子类重写父类的方法。