js面向对象基础

javascriptnew xxx的时候发生了什么?

大家在日常的js开发中,会经常见到类似于下面的代码块:

function Cat(name,color){
    this.name = name;
    this.color = color;
}
Cat.prototype.sayName = function(){
    console.log('this cat name is:'+this.name);
}
Cat.prototype.sayAge = function(){
    console.log('this cat age is:'+this.age);
}

var cat = new Cat('大黄','黄色');

到这边的话,我们就成功创建了一个Cat类的对象cat。(注意下,虽然这边我们仍然谈类,但是由于JS是一门鸭子类型的语言,只要具备某个类一定的特性,比如某些方法、属性,那么我们就认为这个对象就是这个类的)
那么在cat对象的创建过程中,又发生了那些事情呢?创建一个对象,一般会经历如下几个步骤:

  • 创建一个空对象o

  • o的原型指向函数的原型

  • 在新创建的对象o上执行函数,即function.apply(o,arguments)

  • 返回对象o

经过以上四个步骤我们就可以创实例化一个Cat类型的对象。

这里说个大家比较容易出错的地方,就是function优惠返回值的情况。比如:

function F(name){
    this.name = name;
    return "F";
}

这边如果执行var f = new F("warjiang");jsnew F('warjiang')的时候回忽略F的返回值。f最后是一个对象,而不是"F".执行结果如下:

什么时候用prototype,什么时候用this

接着上面的例子,我们在定义Cat类的时候,对于name,color这些属性的定义是采用的this的方式。而对于sayName,sayAge的定义是采用的prototype的方式。那么什么时候该用this,什么时候该用prototype呢?在说这个问题之前,我想重新再提一下=====。相信每个jser都应该知道这个两个等号与三个等号的区别,两个等号只比较数值大小,不关心类型;三个等号不仅比较值大小,还要比较类型是否一致。下面我们先看个例子:

// number类型
var num1 = 22;
var num2 = 22;
console.log(num1 == num2);//true
console.log(num1 === num2);//true


// string类型
var str1 = "hello world";
var str2 = "hello world";
console.log(str1 == str2)//true
console.log(str1 === str2)//true

// 对象
var student1 = {
    name:"tony",
    age:22
};
var student2 = {
    name:"warjiang",
    age:22
}
console.log(student1 == student2);//false
console.log(student1 === student2);//false

// Date类型
var d1 = new Date('2017-3-15 10:23:00');
var d2 = new Date('2017-3-15 10:23:00');
console.log(d1 == d2);//false
console.log(d1 === d2);//false;

// 自定义类型
function Cat(name,color){
    this.name = name;
    this.color = color;
}
Cat.prototype.sayName = function(){
    console.log("my name is " + this.name);
}

var cat1 = new Cat("大黄","黄色");
var cat2 = new Cat("大黄","黄色");
console.log(cat1 == cat2);//false
console.log(cat1 === cat2);//false

通过上面的例子,我们可以总结出,在js中除了基本类型bool,string,number,undefined,null是按照value的方式进行相等的比较之外,其余的对象(不管是自定义的还是JS内置的)在做比较的时候都是比较两个对象的内存地址,如果两个对象的内存地址不相等,则表示两个对象就是不相等的。所以才会出现上面例子中的,即使连个对象中所有的属性都相等,仍然会出现两个对象不相当的情况。
ok回到正题上了,有了这边等号比较的基础之后,我们在来看看之前的问题,什么时候应该使用prototype,什么时候应该使用this。再来个?

function Cat(name,color){
    this.name = name;
    this.color = color;
    this.sayColor = function(){
        console.log(this.color);
    }
}
Cat.prototype.sayName = function(){
    console.log("my name is:"+this.name);
}

var cat1 = new Cat("大黄","黄色");
var cat2 = new Cat("大白","白色");
console.log(cat1.sayColor == cat2.sayColor);//false
console.log(cat1.sayName == cat2.sayName);//true

上面例子中两个Cat类型的对象cat1,cat2在内存中是什么情况呢。

通过上面的内存图,我们可以知道cat1sayColorcat2sayColor方法分别指向两块内存地址,所以cat1.sayColor==cat2.sayColorfalse,而cat1.sayNamecat2.sayName都是来源于prototype对象上的sayName方法,他们的内存地址其实是一个,故而cat1.sayName == cat2.sayName方法为true
通过上面的两个例子,我们不难看出,如果我们使用this的话,则this上的属性、方法都是属于对象本身的,一般可以用于私有方法、属性,而如果使用prototype的话,prototype上的属性、方法在各个类对象上面共享,一般可以用于共有方法、属性。

因此对于一些共有的方法、属性,我们可以放在prototype上面,而对于一些私有的方法、属性,我们可以放在prototype上
下面的话,我再补充一个额外的例子

function Cat(name,color){
    this.name = name;
    this.color = color;
}
Cat.prototype.type = "猫科动物";
Cat.prototype.sayType = function(){
    console.log(this.type);
}
var cat1 = new Cat("大黄","黄色");
var cat2 = new Cat("大白","白色");

cat1.sayType();//猫科动物
cat1.type = "狗科动物";
cat2.sayType();//猫科动物
cat1.sayType();//狗科动物

这边的话,可能有些同学会认为cat1.type = "狗科动物"这句话,修改的是prototype中的type,但是实际上cat1.type="狗科动物",只是在cat1对象上面增加了一个type属性,值为"狗科动物",而原型上面的type并没有收到影响。我们可以在控制台中查看cat1与cat2的详细如下:

如果想修改prototype中的type的话,可以把cat1.type修改为cat1.__proto__.type = "狗科动物"(这边也要注意下,__proto__并不是所有浏览器都支持);此时执行结果就像大家想的那样分别为猫科动物,狗科动物,狗科动物

JavaScript的继承

谈到js的继承,首先先了解下js中原型链。每个js对象都有一个prototype的属性,这个属性会指向一个新的对象,这个新的对象也会有一个prototype的属性。这样一直到Object.
这边稍微提下,有些人可能会问浏览器中__proto__prototype的区别,可以理解为__proto__chrome、firefox这些浏览器对prototype的一种实现、表现,东西还是一个东西。原型链我也举个例子

var o = {
    name:'warjiang',
    age:24
}
var b = {
    name:'bb'
}
b.__proto__ = o;
console.log(b.name);//bb
console.log(b.age);//24
console.log(b.__proto__.name);//warjiang

讲完原型链,我们就开始开始讲讲继承。这边的话,我看也有人说用属性拷贝或者是方法借用这种方式来实现继承,不过我不是非常认可。这边我主要用讲原型来实现继承。
用原型链实现继承,就是让子类的prototype指向父类的prototype,比如下面这样:

function Parent(){
    this.type = "parent";
}
Parent.prototype.sayType = function(){
    console.log(this.type);
}

function Child(){
    
}
Child.prototype = Parent.prototype;
Child.prototype.constructor = Child;

但是如果这样写会有一个问题,就是我们在修改子类prototype的constructor的同时也修改了父类的prototype的constructor,这种情况是不允许的。那么解决的思路一般就是两种,一个是通过new Parent()来解决,我们知道new Parent()的过程帮我们创造一个prototype指向Parent的prototype的对象(记作o,o.prototype=parent.prototype),这个时候,让子类prototype指向o这个对象的时候,Child.prototype->o,o.prototype->Parent.prototype,这样一个原型链就这么链接起来了,同时这个时候如果去修正Child.prototype.constructor为Child的时候,相当于执行o.constructor = Child,并不会对Parent.prototype造成影响。这种做法的表现形式如下:

function Parent(){
    this.type = "parent";
}
Parent.prototype.sayType = function(){
    console.log(this.type);
}

function Child(){
    
}
Child.prototype = new Parent();
Child.prototype.constructor = Child;

此时如果我们去new Child()对象的时候,我们可以如下的继承图

分析Child.prototype=new Parent();这个过程我们会发现,new Parent()其实对Parent的prototype起到保护的作用,因此我们完全可以通过一个空对象来完成这样的功能,如下:

function Parent(){
    this.type = "parent";
}
Parent.prototype.sayType = function(){
    console.log(this.type);
}
var f = function(){}
f.prototype = Parent.prototype;
function Child(){
    
}
Child.prototype = new f();
Child.prototype.constructor = Child;

这么做的好处在于new f()的过程相对于new Parent()的过程更加轻量,更加节约内存。

参考

Prototye inheritance

Javascript 面向对象编程(一):封装

Javascript 面向对象编程(二):封装

Javascript面向对象编程(三):非构造函数的继承


warjiang
572 声望14 粉丝