面向对象之自我修养

1,305 阅读13分钟

构造函数

在构造函数里面,可以自定义对象的属性和方法,在运行时自动出现在执行环境中,通过new操作符得到一个包含这些自定义属性和方法的实例对象。

function Person( name, age ){
    this.name = name;
    this.age = age;
}
let person1 = new Person( "xqs", "28" );
console.log( person1 );

刚开始接触到面向对象时,一时不能理解什么是构造函数,也不知道构造函数与普通函数之间有什么区别?

其实,如果仅仅对一个函数而言(未调用),它就是普通的一个函数体,当我们通过new操作符去调用时,此时它就是一个构造函数,当不通过new操作符调用,无论采用什么样的调用方式,例如Person()、Person.call(this),它都仅仅是一个普通函数,最多无非是它内部的this(函数执行上下文)不同而已。

为了稍微做一下区分,我们通常会把构造函数名的首字母大写,就像Person,第一个字母P大写。
当然了,通过new操作符调用,与普通方式调用,在函数执行过程还是有很大区别的。

我们来说一下,通过new操作符调用时会发生什么:

  • 在函数内部代码执行之前,隐式创建了一个对象
  • 把函数内部的执行上下文,也就是this,指向这个新创建的对象
  • 执行函数内部的代码
  • return这个新创建的对象

此时我们打印person1,

{
    name: "xqs",
    age: "28"
}

可以看到它是一个对象,称之为实例对象。
还有一个点需要说一下的就是,我们在写构造函数的时候是不需要return语句的,因为它会自动给我返回this对象,但是如果我们非要在构造函数内部加return,那就有所不同了。

//eg: return Number
function Person( name, age ){
    this.name = name;
    this.age = age;
    return 123456;
}
var person0 = new Person( "xqs", "28" );
console.log( person0 );  // { name: "xqs", age: "28" }

//eg: return Array
function Person1( name, age ){
    this.name = name;
    this.age = age;
    return [1, 2, 3];
}
var person1 = new Person1( "xqs", "28" );
console.log( person1 );  // [1, 2, 3]

//eg: return Object
function Person2( name, age ){
    this.name = name;
    this.age = age;
    return {age: "永远十八岁"};
}
var person2 = new Person2( "xqs", "28" );
console.log( person2 );  // {age: "永远十八岁"}

//eg: return Function
function Person3( name, age ){
    this.name = name;
    this.age = age;
    return function (){
        consolel.log("我是一个函数!")
    };
}
var person3 = new Person3( "xqs", "28" );
console.log( person3 );  // function (){ consolel.log("我是一个函数!") };

//eg: return null
function Person4( name, age ){
    this.name = name;
    this.age = age;
    return null;
}
var person4 = new Person4( "xqs", "28" );
console.log( person4 );  // {name: "xqs", age: "28"}

通过上面的打印结果我们大胆推测:

  • 当构造函数内部没有显示的return时,return的是this
  • 当构造函数内部有显示的return,并且return的是基本类型时,实际return的还是this
  • 当构造函数内部有显示的return,但return的是一个引用类型时,那么实际return的也就是这个引用类型的值(指针)了。

实际上也确实是如此,所以为了避免出错,或者没有其它的用途,最好不要在构造函数内部添加return语句。

实现new

理解new操作符的执行过程,实现基础的new也就很简单了!

//实现
function creatNew(Fn){
    var obj = Object.create( Fn.prototype );
    var args = Array.prototype.slice.call(arguments, 1);
    var result = Fn.apply(obj, args);
    return (result&&["object", "function"].indexOf(typeof result) > -1) ? result : obj;
}

//eg:
function Person( name, age ){
    this.name = name;
    this.age = age;
}
creatNew( Person, "测试new", "20" );

原型对象

无论我们以何种方式创建一个函数时(函数声明或函数表达式),它都会有一个prototype属性,这个属性是一个指针,指向一个对象,我们可以为这个对象添加一些属性和方法,供由这个函数new出来的实例对象共享,我们称这个对象为原型对象。每个原型对象里面都包含一个constructor属性,指向它的构造函数,称之为构造函数属性。

在说完构造函数属性之后,还有一点很重要,在我们通过new构造函数得到一个实例对象时,该实例对象也会默认包含一个内容属性,这个属性__.proto__也是一个指针,指向构造函数的原型对象,(不是所有的浏览器识别这种写法,Firefox、Safari、Chrome可识别)。

我们现在了解了构造函数、实例对象、原型对象,下面通过一个图来加深一下理解。

function Person( name, age ){
    this.name = name;
    this.age = age;
}
let person1 = new Person( "xqs", 28 );
console.log( person1 );
let person2 = new Person( "lsm", "永远十八岁" );
console.log(person2);

alt 通过这个图我们可以看出,实例对象与构造函数之间并没有什么直接的联系,而实例对象与原型对象之间却存在着连接。

Person.prototype.sayName = function(){
    console.log(this.name);
};
person1.sayName();  // "xqs"
person2.sayName();  // "lsm"

我们并没有在person1、与person2这两个实例上定义sayName方法,但却可以访问,我们之前也提到过,因为实例可以共享它们的原型对象的属性和方法,包括constructor属性也是共享的。

Person.prototype.name= "小明";
console.log( person1.name );  //"xqs"
console.log( person2.name );  //"lsm"

此时我们又在原型对象上定义了name属性,为什么person1.name与person2.name打印出来的不是“小明”呢?

是因为我们在访问实例对象的属性时,如果实例对象上本就存在这个属性,那么就会取到这个属性值屏蔽掉原型对象上的同名属性;如果实例对象上没有这个属性或方法,此时才会去它的原型对象上去找,就像刚刚可以访问到sayName这个方法一样,它就是存在于原型对象上面。

我们可以通过Object.getPrototypeOf()方法得到实例的原型对象,也可以通过isPrototypeOf()来确定,两者是不是存在实例与它对应的原型对象这种连接关系,还可以通过hasOwnProperty()这个方法检测这个属性或方法是存在于实例对象上,还是在它的原型对象上。

console.log( Object.getPrototypeOf( person1 ) === Person.prototype );   //true
console.log( Person.prototype.isPrototypeOf( person1 ) );               //true
console.log( person1.hasOwnProperty( "age" )  );                        //true
console.log( person1.hasOwnProperty( "sayName" )  );                    //false

重写原型

先看一个例子:

function Person( name, age ){
    this.name = name;
    this.age = age;
}
let person1 = new Person( "xqs", 28 );

Person.prototype = {
    sayName: function(){
        return this.name;
    }
}
let person2 = new Person( "lsm", "永远十八岁" );

console.log( person1.sayName );    // undefined
console.log( person2.sayName() );  // "lsm"

通过上面的例子可以看到,当我们去获取person1.sayName这个方法时,得到的是undefined,调用person2.sayName()时可以成功的到打印结果,同样都是通过Person构造函数得到实例那为什么会出现不同的现象呢?

而且我们知道如果实例上没有这个属性或方法就会到它的原型对象上去,明明在原型对象上面定义了sayName这个方法,那么person1.sayName为什么就是undefined呢?

那是因为我们对Person.prototype进行了重写,这个指针指向了另外一个对象实例,而person1.__proto__指向的还是之前的原型对象,也就是说 person1.__proto__ === person2.__proto__为false。

重写原型对象这个操作,其实切断了现有原型与之前任何已经存在的实例对象之间的联系,所以person1.__proto__指向的不是重写后的原型对象,因而没有sayName这个方法也是很正常的。 下面通过图解会更加明确:
alt 通过这个图可以看到,person1.__proto__指向的还是原来的原型对象,因为person1是在重写原型对象之前就已经存在的,当重写Person.prototype时,并不能改变person1.__proto__这个指针指向,而person2是重写原型之后实例化出来的,所以person2.__proto__指向最新的Person.prototype。

还需要主要的一点是,重写原型对象后的constructor属性不再指向Person,而是指向Object,因为它是Object的实例。如果我们还想让它指向Person,可以这样做:

Person.prototype = {
    constructor: Person,
    sayName: function(){
        return this.name;
    }
}
//或者
Person.prototype.constructor = Person;

继承

原型链

之前说过,在读取实例对象上的属性时,会一层一层查找,如果自身没有就会去它的原型对象上面去找,那如果它的原型对象上面也没有,并且它的原型对象的原型对象又指向其它实例的时候会发生什么呢?

它会接着到原型对象指向的实例的原型对象上面去找,这也就形成了一个简单的原型链。
下面看一个例子:

function Person( name, age ){
    this.name = name;
    this.age = age;
    this.colors = ["red", "blue", "green"];
}
Person.prototype = {
    constructor: Friends,
    sayName: function(){
        return this.name;
    }
}

function Friends( job ){
    this.job = job
};
Friends.prototype = new Person( "lsm", "永远十八岁" );
Friends.prototype.sayJob = function(){
    return this.job;
};

let instance = new Friends( "代码搬运工" );

console.log( instance.sayName() );    // "lsm"
console.log( instance.sayJob() );     // "代码搬运工"

首先重写了Person.prototype,然后把Friends.prototype重写成了Person的一个实例对象,那么Friends.prototype此时就是Person一个实例,自然就可共享Person.prototype上的属性和方法。

通过new Friends得到的实例对象instance,可以共享Friends.prototype上的属性和方法,那自然也可以继承Person.prototype上的属性和方法了!

因此,instance不仅可以调用自身原型对象的sayJob方法,同时也可以调用Person.prototype上的sayName方法了!

还需要注意的一点是,我们在重写Person.prototype,把它的constructor属性设置成了Friends,那么即使Friends.prototype被重写成了Person的实例对象,Friends.prototype.constructor依然是指向Friends。还有就是,在重写原型的时候也提到过,要在重写原型对象之后,再做实例化操作,才会起作用!

除此之外,我们还要牢记instanceof这个操作符,它的作用主要用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。 以上面的例子进行演示:

instance instanceof Friends    //true

instance instanceof Person     //true

instance instanceof Object     //true

我们在通过new操作符调用构造函数时,内部是this是它的实例,因此this instanceof 构造函数为true,如果是以普通的方式调用,那么肯定是false。在只能通过new操作符调用的函数内部,通常会率先利用instanceof进行检测,以防止出错。
例如:

// underscore.js
var _ = function(obj) {
    if (obj instanceof _) return obj;
    if (!(this instanceof _)) return new _(obj);
    this._wrapped = obj;
};

//Vue.js
function Vue(options) {
    if (!(this instanceof Vue)) {
    	warn('Vue is a constructor and should be called with the `new` keyword');
    }
    this._init(options);
}

上面我们介绍了通过原型链的方式实现继承,但是缺点也很明显。当初,我们为了避免实例公用同一个引用类型的属性,会把属性在构造函数内部定义,而非定义在原型对象上,例如this.color。

那么当我们通过原型链实现继承的时候会发现,this.color仍然会被所有的Friends的实例所共享。这是缺点之一!
其二,我们发现原型链上,构造函数Person接收到的所有参数,会影响到所有的Friends的实例,这也是我们不愿看到的!

借用构造函数

为了避免原型链继承会出现的问题,可以巧妙的伪造构造函数内部的this实现继承!

function Person( name, age ){
    this.name = name;
    this.age = age;
    this.colors = ["red", "blue", "green"];
}

function Friends( name, age, job ){
    Person.call( this, name, age );
    this.job = job;
};
let instance = new Friends("lsm", "永远十八岁");
instance.colors = [];
let instance2 = new Friends();

console.log( instance.name );          // "lsm"
console.log( instance.colors );        // []
console.log( instance2.colors );       // ["red", "blue", "green"]

这种方式虽然避免了原型链继承存在的问题,但是缺点同样也是很明显。
如果Friends的实例想继承Person中的方法,那么该方法必须在构造函数Person定义才会有效,这样的话所有的Friends实例上都会存在这样一个方法,这很明显没有达到函数复用的要求!

组合继承

function Person( name, age ){
    this.name = name;
    this.age = age;
    this.colors = ["red", "blue", "green"];
}
Person.prototype = {
    constructor: Friends,
    sayName: function(){
        return this.name;
    }
}

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

Friends.prototype = new Person();
Friends.prototype.sayJob = function(){
    return this.job;
};

let instance = new Friends("lsm", "永远十八岁");

console.log( instance.sayName() );    // "lsm"
console.log( instance.colors );       // ["red", "blue", "green"]

通过这上面例子可以看出,这不就是原型链与借用构造函数的结合体吗?是的,确实如此!
这种方式不仅可以让Friends所有的实例都拥有私有的属性,还继承了Person对象上的方法。这种结合同时解决了原型链与借用构造函数存在的问题,岂不是完美?

事实是否定的,因为我们会发现,首先Person内部定义的属性都存在于Friends.prototype上,但是相同的属性在new Friends时,又会在实例上重新定义一次。我们知道除非手动删除实例上的属性,否则存在于Friends.prototype上的同名属性一定会被屏蔽,起不到任何作用。
那么在Friends.prototype上定义的这些属性,看起来是那么的没有必要!

有没有一种捆绑销售的感觉,比如我只想办一个宽带,但是电信公司告诉我必须办一张电信卡才可以办宽带。
但是这张卡除了用了交宽带费,我可能这辈子都不用它干别的,但是我又不能扔掉,是不是很难受,哈哈! Friends.prototyper上与实例同名的那些属性就是如此,比如name 、age、colors,你可能永远都不会用到,但是你又想用Person.prototype上的方法,那么就必须接受这些同名属性!
就像电信公司说,“你要办电信宽带啊,先去办张电信卡吧!”

原型式继承

function object(o){ 
    function F(){} 
    F.prototype = o; 
    return new F(); 
}

var person = { 
    name: "Nicholas", 
    friends: ["Shelby", "Court", "Van"] 
}; 

var anotherPerson = object(person); 
//var anotherPerson = Object.create(person); 
anotherPerson.name = "Greg"; 
anotherPerson.friends.push("Rob"); 

var yetAnotherPerson = object(person); 
yetAnotherPerson.name = "Linda"; 
yetAnotherPerson.friends.push("Barbie"); 

console.log(anotherPerson.friends);      // ["Shelby", "Court", "Van", "Rob", "Barbie"]
console.log(yetAnotherPerson.friends);   // ["Shelby", "Court", "Van", "Rob", "Barbie"]

通过这种方式,我们可以根据已有的对象创建出新的对象,已有的对象作为新对象的原型对象。

那么,新对象的__proto__就指向这个已有的对象,例如anotherPerson.__proto__ === person为true。
这种方式好处是,我们不必专门的创建构造函数来实现实例的继承。

寄生式继承

function createAnother(original){ 
    var clone = Object.create(original); //创建一个新对象
    clone.sayHi = function(){            //以某种方式来增强这个对象
        console.log("hi"); 
    }; 
    return clone;                        //返回这个对象
}

var person = { 
 name: "Nicholas", 
 friends: ["Shelby", "Court", "Van"] 
}; 
var anotherPerson = createAnother(person); 
anotherPerson.sayHi();    //"hi"

这种方式没什么好说的,其实就是原型式继承的增强体,同时缺点也很明显。
每创建出一个新对象,这个对象都会拥有一个私有的方法,但是所有实例的这个方法又都是相同的,并没有达到方法复用的标准。

寄生组合式继承

寄生组合式继承可以说是目前应用最多,且最理想的继承方式!

//原型式继承
function inheritPrototype(subType, superType){ 
    var prototype = Object.create(superType.prototype); //创建对象
    prototype.constructor = subType;                    //指定构造函数
    subType.prototype = prototype;                      //实现继承
}

function SuperType(name){ 
    this.name = name; 
    this.colors = ["red", "blue", "green"]; 
} 
SuperType.prototype.sayAge= function(){ 
    console.log(this.age); 
};

//借用构造函数
function SubType(name, age){                 
    SuperType.call(this, name); 
    this.age = age; 
} 

inheritPrototype(SubType, SuperType); 

SubType.prototype.sayAge = function(){ 
    console.log(this.age); 
};

var newSubType = new SubType("lsm", "永远十八岁");
console.log( newSubType.name );            //"lsm"
console.log( newSubType.sayAge() );        //"永远十八岁"

因为,通过这种方式得到的实例,不仅继承了公用的方法,还可以设置私有属性,同时也不会在原型对象上添加多余的属性!

看一个例子:

<div id="mount-point"></div>

<script>
    //Vue.extend源码删减
    Vue.extend = function () {
        var Super = this;
        var Sub = function VueComponent(options) {
            this._init(options);    //在实例上添加私有属性
        };
    
        //原型式继承
        Sub.prototype = Object.create(Super.prototype);
        Sub.prototype.constructor = Sub;
    
        return Sub
    };
    
    // 创建构造器
    var Profile = Vue.extend({});
    
    // 创建 Profile 实例,并挂载到一个元素上。
    var ProfileObj = new Profile();
    ProfileObj.$mount('#mount-point');
</script>

上面的例子其实就是寄生组合式继承的应用!

Function & Object

对于Function与Object的理解:

Object.__proto__ === Function.prototype                  // true

Function.prototype.__proto__ === Object.prototype        // true

Object.__proto__.__proto__ === Object.prototype          // true

Function.__proto__ === Function.prototype                // true

alt Object.prototype 是浏览器底层根据规范创建的一个对象,构造函数Object的原型对象是Object.prototype,那么Object.prototype.constructor就指向Object。

也因为Object是构造函数,所以Object.__proto__指向Function.prototype。

Function的原型对象是Function.prototype,但当Function作为构造函数时,Function.__proto__指向的就是Function.prototype,所以Function.__proto__ === Function.prototype为true。
同时也因为Function.prototype是一个对象,自然是构造函数Object的一个实例,所以Function.prototype.__proto__ === Object.prototype为true,Function.__ proto__.__proto__ === Object.prototype为true。

因为Object.__proto__ === Function.prototype为true,那么Object.__proto__.__proto__ === Object.prototype肯定也是true了!