0
点赞
收藏
分享

微信扫一扫

前端基础-面向对象核心


第6章 再谈 面向对象

学习目标:


  • 了解ES6中新的对象语法
  • 正确使用继承

6.1 对象

6.1.1 谁说JS没有类

在JS中,想要获取一个对象,有多种方式:

​var o1 = {}​​​ ​​var o2 = new Object()​

自定义构造函数方式

function Point(x, y) {
this.x = x;
this.y = y;
this.toString = function () {
return this.x + ', ' + this.y;
};
}

var p = new Point(1, 2);
console.log(p.toString());

但是上面这种使用构造函数获取对象的写法跟传统的面向对象语言(比如 C++ 和 Java)差异很大,很容易让新学习这门语言的程序员感到困惑。

ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。

通过​​class​​关键字,可以定义类。

基本上,ES6 的​​class​​可以看作只是一个​语法糖​,它的绝大部分功能,ES5 都可以做到,新的​​class​​写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。

上面的代码用 ES6 的​​class​​改写,就是下面这样。

//定义类
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}

toString() {
return this.x + ', ' + this.y ;
}
}
var p = new Point(1, 2);
console.log(p.toString());

上面代码定义了一个“类”,可以看到里面有一个​​constructor​​方法,这就是​构造方法​(后面还会讲到),而​​this​​​关键字则代表实例对象。也就是说,ES5 的构造函数​​Point​​​,对应 ES6 的类​​Point​​。

​Point​​​类除了构造方法,还定义了一个​​toString​​​方法。注意,定义“类”的方法的时候,前面不需要加上​​function​​这个关键字,直接把函数定义放进去了就可以了。另外,方法之间不需要逗号分隔,加了会报错。

ES6 的类,完全可以看作构造函数的另一种写法。

使用的时候,也是直接对类使用​​new​​命令,跟构造函数的用法完全一致。

类同样也有​​prototype​​属性,而属性的值依然是实例对象的原型对象;

class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}

toString() {
return this.x + ', ' + this.y ;
}
}
Point.prototype.toValue = function(){
console.log('123');
}
var p = new Point(1, 2);
p.toValue();
console.log(p.toString());

6.1.2 constructor 方法

​constructor​​​方法是类的默认方法,通过​​new​​​命令生成对象实例时,自动调用该方法。一个类必须有​​constructor​​​方法,如果没有显式定义,一个空的​​constructor​​方法会被默认添加。

class Point {
}

// 等同于
class Point {
constructor() {}
}

类必须使用​​new​​ 进行实例化调用,否则会报错。

而类一旦实例化,​​constructor()​​方法就会被执行,就像 人一出生就会哭一样;

​constructor​​​方法默认返回实例对象(即​​this​​);

但是返回值完全可以指定返回另外一个对象;

var o1 = {
f1:function(){
console.log('f1');
}
}
class Point{
constructor (){
return o1;
}
f2(){
console.log('f2');
}
}

var p = new Point();
p.f1(); // f1
p.f2(); //Uncaught TypeError: p.f2 is not a function


​constructor​​方法默认返回值尽量不要修改,一旦修改,我们获取的对象将脱离其原型;


6.1.3 变量提升

我们知道,在JS中,不管是全局变量还是局部变量都存在变量提升的特性,函数也有提升的特性,也可以先调用后声明,构造函数也一样;如下面的代码,完全没问题:

var p = new Point();
p.f2();

function Point(){
this.f2 = function(){
console.log('f2');
}
}

但是,需要注意: ​类不存在变量提升(hoist),这一点与 ES5 完全不同。

new Man();
class Man{}

// Man is not defined


注意,class只是在原有面向对象的基础上新加的关键字而已,本质上依然没有改变JS面向对象方式;


6.2 再谈继承

6.2.1 原型链继承的问题

//声明构造函数Run
function Run(){
this.p = function(){
console.log(this.name+'跑');
}}
//声明构造函数Man
function Man(name){
this.name = name;
}
//设置构造函数Man的原型为Run,实现继承
Man.prototype = new Run();

var m = new Man('张三');

m.p();

// 由构造函数获取原型
console.log(Man.prototype); // 函数对象 Run
//标准方法获取对象的原型
console.log(Object.getPrototypeOf(m)); // 函数对象 Run
//获取对象的构造函数
console.log(m.constructor); // Run 函数

运行上面的代码,我们发现对象 ​​m​​​ 本来是通过构造函数​​Man​​​ 得到的,可是,​​m​​ 对象丢失了构造函数,并且原型链继承的方式,打破了原型链的完整性,不建议使用;


这个问题,在3.5章节也提到过,想解决也很简单,只要在父级中手动指定子级正确的构造函数即可:



修改上面的代码:



前端基础-面向对象核心_父类


6.2.2 冒充方式的继承

前面我们在学习JS面向中的面向对象编程时,谈到了继承;

所谓的继承,其实就是在子类(子对象)能够使用父类(父对象)中的属性及方法;

function f1(){
this.color = '黑色';
this.h = function(){
console.log(this.color+this.sex);
}
}

function F2(){
this.sex = '铝';
this.fn = f1;
}

var b = new F2();
b.fn();
b.h();

运行以上代码可知,由构造函数获取的对象 ​​b​​可以调用函数f1中的属性及方法;

有运行结果可知,​​f1​​​ 函数中的​​this​​​ 实际指向了对象​​b​​​ ,对象​​b​​​ 实际上已经继承了​​f1​

这种方式称为 **对象冒充 **方式继承,ES3之前的代码中经常会被使用,但是现在基本不使用了;

为什么不使用了呢?

还是要回到上面的代码,本质上讲,我们只是改变了函数​​f1​​ 中this的指向,

f1中的this指向谁,谁就会继承f1;

而call和apply就是专门用来改变 函数中this指向的;

call或apply 实现继承

function Run(){
this.p = function(){
console.log('ppp');
}
}
function Man(){
//将Run函数内部的this指向Man的实例化对象;
Run.call(this);
}
var m = new Man();
m.p();

//获取对象的构造函数
console.log(m.constructor); // Man 函数
// 由构造函数获取原型
console.log(Man.prototype); // 函数对象 Man
//标准方法获取对象的原型
console.log(Object.getPrototypeOf(m)); // 函数对象 Man

call或apply 实现的继承依然是使用对象冒充方式实现, 此方式即实现了继承的功能,同时也不再出现原型继承中出现的问题;

6.2.4 Object.create() 创建实例对象及原型继承

构造函数作为模板,可以生成实例对象。但是,有时拿不到构造函数,只能拿到一个现有的对象。我们希望以这个现有的对象作为模板,生成新的实例对象,这时就可以使用​​Object.create()​​方法。

var person1 = {
name: '张三',
age: 38,
greeting: function() {
console.log('Hi! I\'m ' + this.name + '.');
}
};

var person2 = Object.create(person1);

person2.name // 张三
person2.greeting() // Hi! I'm 张三.

console.log(Object.getPrototypeOf(person2) == person1); //true

上面代码中,对象​​person1​​​是​​person2​​的模板,后者​继承​了前者所有的属性和方法;

​Object.create​​ 的本质就是创建对象;

var obj1 = Object.create({});
var obj2 = Object.create(Object.prototype);
var obj3 = new Object();

//下面三种方式生成的新对象是等价的。

如果想要生成一个不继承任何属性(比如没有​​toString​​​和​​valueOf​​​方法)的对象,可以将​​Object.create​​​的参数设为​​null​​。

var obj = Object.create(null);

obj.valueOf()
// TypeError: Object [object Object] has no method 'valueOf'


使用​​Object.create​​方法的时候,必须提供对象原型,即参数不能为空,或者不是对象,否则会报错。


6.2.5 Class 的继承

class Run{
p(){
console.log('ppp');
}
}
class Man extends Run{
}
var m = new Man();
m.p();

Class 可以通过​​extends​​关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

ES5 的继承,实质是先创造子类的实例对象,然后再将父类的方法添加到上面(​​Parent.call(this)​​)。

ES6 的继承机制完全不同,是先创造父类的实例对象,然后再用子类的构造函数修改。

class Run{
p(){
console.log('ppp');
}
}
class Man extends Run{
// 显式调用构造方法
constructor(){}
}
var m = new Man();
m.p();
// Uncaught ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived

上面代码中,​​Man​​​继承了父类​​Run​​​,但是子类并没有先实例化​​Run​​,导致新建实例时报错。

class Run{
p(){
console.log('ppp');
}
}
class Man extends Run{
constructor(){
// 调用父类实例(实现父类构造方法)
super();
}
}
var m = new Man();
m.p();



举报

相关推荐

0 条评论