类的创建和继承(ES5+ES6)
前导知识
进一步了解之前先要分清楚构造函数的私有属性和方法、静态属性和方法、实例属性和方法、原型属性和方法。
// 构造函数 function Person(name) { // 私有属性 let age = 25 // 私有方法 function sex() { console.log('male') } //闭包 this.getInner = function () { console.log(age); } // 实例属性 this.name = name || 'someName'; // 实例方法 this.sleep = function () { console.log(this.name + '在睡觉'); } } // 静态属性 Person.from = 'guangzhou' // 静态方法 Person.run = function () { console.log("在跑步"); } //原型属性 Person.prototype.height = 180 // 原型方法 Person.prototype.eat = function (food) { console.log(this.name + '在吃' + food); }; //实例对象 let p1 = new Person("p1");
私有属性/方法
私有属性/方法由于定义在函数内,存在与构造函数的函数作用域,函数外部只能通过闭包的方式访问。
p1.getInner() //25
实例属性/方法
只有通过new
运算符新建的对象才能访问,因为实例属性/方法被this
赋给了新建的对象。
Person.sleep() //Person.sleep is not a function p1.sleep() //p1在睡觉
静态属性/方法
即构造函数本身添加的成员,只能被构造函数访问,不能被实例对象访问。
Person.run() //在跑步 p1.run() //p1.run is not a function
原型属性/方法
原型中的属性/方法实例对象和构造函数都可以访问到。
p1.eat("food") //p1在吃food Person.prototype.eat("food") //undefined在吃food,这里如果不用.prototype就是访问Person的静态方法 Person.eat() //undefined 直接访问函数的静态属性和方法时,如果找不到,不会去函数的原型上查找
总结
- 函数的私有属性和方法在函数体外部是不能直接通过属性访问符
.
访问的,如果要访问,可以使用闭包 - 函数的静态属性和方法可以在函数体外部直接访问
- 函数的实例属性和方法是绑定到函数返回的实例上的,当然,函数得作为构造函数使用
new
调用 - 函数的原型属性和方法是挂在函数的原型对象上的,实例的原型对象会关联到函数的原型对象,当访问实例的属性和方法时,如果实例上找不到,会通过原型委托到函数的原型上。但是,当直接访问函数的静态属性和方法时,如果找不到,不会去函数的原型上查找
类的创建/继承
原型继承
在上个小节的代码块中,已经通过new
和构造函数生成实例对象,既创造了一个类,这个类有自己的实例属性和方法,同时通过prototype
让每个实例继承原型属性和方法。
特点:基于原型链,既是父类的实例,也是子类的实例
缺点:无法实现多继承
构造继承
使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)
//父类构造函数 function Person(name) { // 父实例属性 this.name = name || 'someName'; // 父实例方法 this.sleep = function () { console.log(this.name + '在睡觉'); } } //父原型属性 Person.prototype.height = 180 //子类构造函数 function PersonChild(name) { Person.call(this); this.name = name || 'someChildName'; } let personChild = new PersonChild(); console.log(personChild.name); // someChildName console.log(personChild.height); // undefined personChild.sleep(); // someChildName在睡觉 console.log(personChild instanceof Person); // false console.log(personChild instanceof PersonChild); // true
特点:可以实现多继承
缺点:只能继承父类实例属性和方法,不能继承原型上的属性和方法。
组合继承
相当于构造继承和原型链继承的组合体。通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用
function Person(name) { // 实例属性 this.name = name || 'someName'; // 实例方法 this.sleep = function () { console.log(this.name + '在睡觉'); } } //原型属性 Person.prototype.height = 180 function PersonChild(name) { Person.call(this); this.name = name || 'someChildName'; } PersonChild.prototype = new Person(); PersonChild.prototype.constructor = PersonChild; let personChild = new PersonChild(); console.log(personChild.name); // someChildName console.log(personChild.height); // 180 personChild.sleep() // someChildName在睡觉 console.log(personChild instanceof Person); // true console.log(personChild instanceof PersonChild); // true
特点:可以继承实例属性/方法,也可以继承原型属性/方法
缺点:调用了两次父类构造函数,生成了两份实例
寄生组合继承
通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性
function Person(name) { // 实例属性 this.name = name || 'someName'; // 实例方法 this.sleep = function () { console.log(this.name + '在睡觉'); } } //原型属性 Person.prototype.height = 180 function PersonChild(name) { Person.call(this); this.name = name || 'someChildName'; } let Super = function(){}; Super.prototype = Person.prototype; PersonChild.prototype = new Super(); let personChild = new PersonChild(); console.log(personChild.name); // someChildName console.log(personChild.height); // 180 personChild.sleep() // someChildName在睡觉 console.log(personChild instanceof Person); // true console.log(personChild instanceof PersonChild); // true
ES6的方式
创建
class Person { // 实例属性和方法 constructor(name) { this.name = name || 'someName'; this.fn = function (params) { console.log(params); } } //原型方法 sleep() { console.log(this.name + '在睡觉'); } } let p1 = new Person('someName'); p1.sleep(); //someName在睡觉
class
中的constructor
函数负担起了之前的构造函数的功能,类中的实例属性和方法都可以在这里初始化。ES6 的class
可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到。
继承
class
可以通过extends
关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。
class Person { // 实例属性和方法 constructor(name) { this.name = name || 'someName'; this.fn = function (params) { console.log(params); } } //原型方法 sleep() { console.log(this.name + '在睡觉'); } } class PersonChild extends Person { constructor(name) { super(name); } } let personChild = new PersonChild('someName'); personChild.sleep(); // someName在睡觉,父类原型方法 personChild.fn("父类实例方法") // 父类实例方法 console.log(personChild instanceof Person); // true console.log(personChild instanceof PersonChild); // true
super
关键字,表示父类的构造函数,用来新建父类的this
对象。
子类必须在constructor
方法中调用super
方法,否则新建实例时会报错。这是因为子类自己的this
对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super
方法,子类就得不到this
对象。
注意:
- 父类的静态方法,也会被子类继承。
- 如果要在子类的方法中调用父类方法,需要用
super.fn()
调用。- ES6的继承与ES5实现的类继承,还有一点不同。ES5是先创建子类的实例,然后在子类实例的基础上创建父类的属性。而ES6是相反的,先创建父类的实例,然后在父类实例的基础上扩展子类属性。利用这个属性可以做到一些ES5无法实现的功能:继承原生对象。