JavaScript 面向对象


本文为慕课网 JavaScript深入浅出 JavaScript 面向对象笔记。

概念

面向对象程序设计(Object-oriented programming,OOP)是一种程序设计范型,同时也是一种程序开发的方法。对象指的是类的实例。它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性。

——维基百科

一般面向对象包含:继承,封装,多态,抽象


基于原型的继承

function Foo() {
    this.y = 2;
}
console.log(typeof Foo.prototype); //object

Foo.prototype.x = 1;
var obj3 = new Foo();

console.log(obj3.y); //2
console.log(obj3.x); //1

创建函数 Foo 的时候,就会有一个内置的 Foo.prototype 属性,并且这个属性是对象。

在使用 new Foo(); 创建对象实例时。this 会指向一个对象,并且这个对象的原型会指向 Foo.prototype 属性。this.y = 2 给这个对象赋值,并把这个对象返回。把这个对象赋值给 obj3

yobj3 上的,xobj3 的原型 Foo.prototype 上的。

prototype


prototype 属性与原型

prototype 是函数对象上预设的对象属性。

原型是对象上的原型,通常是构造器的 prototype 属性。


function Person(name, age) {
    this.name = name;
    this.age = age;
}

Person.prototype.LEGS_NUM = 2;
Person.prototype.ARMS_NUM = 2;

Person.prototype.hi = function() {
    console.log('Hi, my name is ' + this.name + ". I'm " + this.age + ' years old now');
};

Person.prototype.walking = function() {
    console.log(this.name + ' is walking...');
};

function Student(name, age, className) {
    Person.call(this, name, age); //使 Person 中的 this 指向 Student
    this.className = className;
}

Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;

Student.prototype.hi = function() {
    console.log('Hi, my name is ' + this.name + ". I'm " + this.age + ' years old now, and from ' + this.className + ".");
};

Student.prototype.learn = function(subject) {
    console.log(this.name + ' is learning ' + subject + ' at ' + this.className + '.');
}

//test
var gao = new Student('Gao', '24', 'Class 3123');
console.log(gao); // 这个对象的具体内容见下图
gao.hi(); //Hi, my name is Gao. I'm 24 years old now, and from Class 3123.
gao.LEGS_NUM; //2
gao.walking(); //Gao is walking...
gao.learn('JavaScript'); //Gao is learning JavaScript at Class 3123.
  • Object.create(arg) 创建一个空对象,并且这个对象的原型指向参数 arg
  • Student.prototype.constructor = Student 为了保证一致性,否则 constructor 指向 Person。

原型链

gao 对象的原型链:

Object

下面通过图形展示原型链:

原型链


Object.create(null) & .bind(null)

这两种算是特例。

Object.create(null).bind(null) 这两种方式创建出来的对象是没有 prototype 属性的,为 undefined


prototype 属性

改变 prototype

JavaScript 中的 prototype 是对象,在运行的时候可以修改。

给 prototype 添加或删除一些属性,是会影响到已经创建好的实例对象的。

但是,直接修改 prototype 属性,是不会影响到已经创建好的实例对象的。但是会影响到新的实例对象。如下代码:

// 上接上面的代码

// 给 prototype 添加或删除一些属性
Student.prototype.x = 101;
console.log(gao.x); //101

// 直接修改 prototype 属性
Student.prototype = {
    y: 2
};

// 不会影响到已创建好的实例对象
console.log(gao.x); //101
console.log(gao.y); //undefined

// 会影响到新创建的实例对象
var ying = new Student('Ying', 24, 'UI');
console.log(ying.x); //undefined
console.log(ying.y); //2

内置构造器的 prototype 属性

修改内置构造器的 prototype 属性后,在实例化这个对象后,枚举其属性时,会把修改的内置构造器的 prototype 属性也枚举出来,有时候这是要避免的。可用 defineProperty 方法解决。如下代码:

Object.prototype.x = 1;
var obj = {};
console.log(obj.x); //1
console.log(obj);

for (var k in obj) {
    console.log('result--->' + k);
}
// result--->x

使用 defineProperty 后:

Object.defineProperty(Object.prototype, 'x', {
    writable: true,
    value: 1
});
var obj = {};
console.log(obj.x);//1
console.log(obj);
for (var k in obj) {
    console.log('result--->' + k);
}
// nothing output here

其实也可以这样枚举,使用 hasOwnProperty 方法:

for (var key in obj) {
    if (obj.hasOwnProperty(key)) {
        console.log("result--->" + key);
    }
}

创建对象-new/原型链

prototype


instanceof

console.log([1, 2] instanceof Array); //true
console.log([1, 2] instanceof Object); //true
console.log(new Object() instanceof Array); //false

左边要求是对象,右边要求是构造器或函数。它会判断:右边的构造器中的 prototype 属性是否出现在左边的对象的原型链上。

  • 注意:不同的 window 或 iframe 间的对象类型检测不能使用 instanceof

实现继承的方式

function Person() {}

function Student() {}

Student.prototype = Person.prototype; //1

Student.prototype = new Person(); //2

Student.prototype = Object.create(Person.prototype); //3

Student.prototype.constructor = Student;

注释中:

1 是错误的。如果改变了 Student 就会改变 Person

2 可以实现继承,但是其调用了构造函数,若父类构造函数中有形参,那么传值就会比较奇怪。

3 是最好的方法。创建了一个空对象,并且对象的原型指向参数 Person.prototype。这样便实现了继承。同时原型链写,不向上查找。但是 Object.create 是ES5 中的方法,所以可以使用下列代码做兼容:

if (!Object.create) {
    Object.create = function(proto) {
        function F() {}
        F.prototype = proto;
        return new F;
    };
}

模拟重载

function Person() {
    var args = arguments;
    if (typeof args[0] === 'object' && args[0]) {
        if (args[0].name) {
            this.name = args[0].name;
        }
        if(args[0].age){
            this.age = args[0].age;
        }
    } else {
        if (args[0]) {
            this.name = args[0];
        }
        if (args[1]) {
            this.age = args[1];
        }
    }
}

//重写 toString 方法
Person.prototype.toString = function() {
    console.log('name='+this.name+', age='+this.age);
};

var gao = new Person({name:'Gao',age:24});
gao.toString(); // name=Gao, age=24

var ying = new Person('Ying',25);
ying.toString(); // name=Ying, age=25

对参数进行判断,模拟实现重载。


调用子类方法

function Person(name) {
    this.name = name;
}

function Student(name, className) {
    this.className = className;
    Person.call(this, name); // 调用基类的构造器
}

var gao = new Student('Gao', '3123');
console.log(gao); // Student {className: "3123", name: "Gao"}

Person.prototype.init = function() {};

Student.prototype.init = function() {
    // do sth...
    Person.prototype.init.apply(this, arguments); // 同时也想调用父类被覆盖的方法
};

主要是两种:调用父类的构造器,调用原型链上父类被覆盖的方法。


链式调用

function ClassManager() {}
ClassManager.prototype.addClass = function(str) {
    console.log('Class: ' + str + ' added');
    return this;
};

var manager = new ClassManager();
manager.addClass('classA').addClass('classB').addClass('classC');
// Class: classA added
// Class: classB added
// Class: classC added

重点在于 return this。返回这个 ClassManager 的实例。这样这个实例又可以继续调用方法。


抽象类

在构造器中 throw new Error(''); 抛异常。这样防止这个类被直接调用。

function DetectorBase() {
    throw new Error('Abstract class can not be invoked directly!');
}

DetectorBase.detect = function() {
    console.log('Detection starting...');
}
DetectorBase.stop = function() {
    console.log('Detection stopped.');
};
DetectorBase.init = function() {
    throw new Error('Error');
}

var d = new DetectorBase();// Uncaught Error: Abstract class can not be invoked directly!

function LinkDetector() {}
LinkDetector.prototype = Object.create(DetectorBase.prototype);
LinkDetector.prototype.constructor = LinkDetector;

var l = new LinkDetector();
console.log(l); //LinkDetector {}__proto__: LinkDetector
l.detect(); //Uncaught TypeError: l.detect is not a function
l.init(); //Uncaught TypeError: l.init is not a function

var d = new DetectorBase(); 是不能实例化的,会报错

l.detect(); 但是这个为什么报错我就不知道了。

已经在原课程下提问了,期待老师的讲解。 抽象类中子类为什么不能调用父类的非抽象方法?

问题已经解决了,应该是老师当时的课件写错了,应该再基类中将这两个方法写在其原型 prototype 上。如下:

function DetectorBase() {
    throw new Error('Abstract class can not be invoked directly!');
}

DetectorBase.prototype.detect = function() {
    console.log('Detection starting...');
};
DetectorBase.prototype.stop = function() {
    console.log('Detection stopped.');
};
DetectorBase.prototype.init = function() {
    throw new Error('Error');
};

// var d = new DetectorBase();// Uncaught Error: Abstract class can not be invoked directly!

function LinkDetector() {}
LinkDetector.prototype = Object.create(DetectorBase.prototype);
LinkDetector.prototype.constructor = LinkDetector;

var l = new LinkDetector();
console.log(l); //LinkDetector {}__proto__: LinkDetector
l.detect(); //Detection starting...
l.init(); //Uncaught Error: Error

模块化

var moduleA;
moduleA = function() {
    var prop = 1;

    function func() {}

    return {
        func: func,
        prop: prop
    };
}(); // 立即执行匿名函数

prop,func 不会被泄露到全局作用域。

或者另一种写法,使用 new

moduleA = new function() {
    var prop = 1;

    function func() {}

    this.func = func;
    this.prop = prop;
}

更复杂的可以使用 Sea.js Kissy Require.js 模块化工具。


最后补充一点设计模式相关的资料,我还没有来得及看的:


Content