让前端飞Web前端之路

理解构造函数与原型对象

2017-11-30  本文已影响43人  itclanCoder

前言

在Es6之前,由于javascript没有对类的支持,也就是说它并不具备如传统后台语言(比如java)拥有类的功能,所谓类就是用来描述事物中的属性和行为的,类的特征是由成员组成的,而属性对应的就是类中的成员变量,而方法对应的就是类中的成员方法,这是传统oop语言的描述,然而在javascript中,虽没有类的概念,但是它往往是通过构造函数和原型对象来给对象模拟与类相似的功能,但是这些相似的功能并不一定表现的与类完全一致,其实创建构造函数的过程,就是创建模板的过程,类一定程度上与此相似,创建多个共享的特定的属性和方法,用于生成对象的饼干工具,主要目的是提高代码的可复用性,也提高了代码的性能,有时候,在我们无意间就已经在使用了这些特性,什么构造函数,原型,个人觉得,初次理解起来很是抽象,也稀里糊涂的觉得实际开发中到底有什么卵用,也许后者在不涉及复杂的功能需求时,平时用得不多,显然Es6中已新增了类class的功能,越来越严格,越来越像后端语言,Es6,Es7,Es8新增的诸多方法也越来越强大,标准也越来越完善,但是我觉得理解构造函数与原型对象是必须的,是js面向对象编程的基础,今天就我的学习和使用跟大家分享一下学习心得

收听音频,可戳链接,很久没开口了,嘴巴变笨了的

正文从这里开始~

什么是函数

先看下面一简易代码

var funA = function(){
    console.log("我是匿名函数保存在变量funA中");
}
var funB = [function(){
    console.log("我是匿名函数保存在数组funB中");
}]
var funC = {
     method:function(){
        console.log("我是匿名函数保存在对象funC中");
    }
}
// 函数的调用
funA();     // 普通函数的调用
funB[0]();  // 函数存入数组中的调用
funC.method(); // 对象调用方法的使用

// 函数可以作为参数传递,也可以作为返回值返回
var funD = function(funParm){
    return funParm;
}
var runFunParmPassedToFunD = funD(function(){
    console.log("欢迎关注微信itclanCoder公众号");
})
runFunParmPassedToFunD();

// 函数是对象,也就是说函数也拥有属性
var FunE  =  function(){}
FunE.property = "随笔川迹";
console.log(FunE.property);
// 证明函数式对象
console.log("funA的数据类型是",typeof funA);
console.log("funA具体所属",Object.prototype.toString.call(funA));
console.log("funA是由Object的一个实例对象?",funA instanceof Object); 
console.log("funA函数下面的构造器是",funA.constructor);
console.log("funA函数是由Object构造出来的?",funA.constructor == Object); // false
console.log("funA下面的原型",funA.prototype);   // funA下面的原型
console.log("Object下的原型",Object.prototype); // Object对象下原型
console.log("funA原型下构造器",funA.prototype.constructor);//function fun(){}
console.log("对象原型下的构造器",Object.prototype.constructor);

控制台输出结果如下:

什么是函数什么是函数
结论:

什么是构造函数

定义:构造函数就是你用new关键字创建对象时调用的函数
作用(优点):创建多个共享特定属性和行为的对象,主要是用于生成对象的饼干模具
缺点:当实例化多个对象时,会重复的创建对象,造成内存空间的浪费,增大CPU的开销,并没有消除代码的冗余,(如后面代码所示,原型正好解决了此类问题)

 // 声明一构造函数,首字母大写
function Animal(name,age){
    // this == new Animal();new会自动的创建this对象,且类型就是该构造安徽省农户的类型,构造函数不需要返回值,因为new会显示的返回,return的值就等于函数名+()的调用
    this.name = name;     // 自定义属性
    this.age = age;       // 同上
    this.fun = function(){  // 自定义方法
        return this.name+" "+this.age+"岁了";
    }
}
// 实例化对象
var animal1 = new Animal("cat",2);
var animal2 = new Animal("dog",3);                 console.log(animal1.name,animal1.age,animal2.name,animal2.age); // cat 2 dog 3 
console.log(animal1.fun(),animal2.fun()); // cat 2岁了 dog 3岁了
console.log(animal1.hasOwnProperty("name"));
console.log(animal1.hasOwnProperty("age"));
console.log(animal1 instanceof Animal);  // true,证明animal1是Animal1是Animal构造函数创建出来的
console.log(animal2 instanceof Animal);
console.log(animal1.constructor === Animal); // true
console.log(animal2.constructor === Animal); // true
console.log(animal1.fun == animal2.fun);  // false

示例代码截图如下

什么是构造函数什么是构造函数
问题:同一个构造函数创建出来不同的实例化对象,公用的方法不等同,也就是说,当你new一个构造器对象,上面的构造函数就执行一遍,每次都会新建一个function,会新开辟一个内存空间,每次都是指向一个新的堆的对象 ,这样占用内存消耗非常的大,怎么解决这个问题
解决办法1:将构造函数里面自定义的方法拿出来,独立放在构造函数外
如下示例代码所示
// 声明一构造函数,首字母大写
function Animal(name,age){
    this.name = name;     // 自定义属性
    this.age = age;       // 同上
    this.fun = fun;
}
// 把构造函数里面自定义的方法拿出来
function fun(){
    return this.name+" "+this.age+"岁了";
}
// 实例化对象
var animal1 = new Animal("cat",2);
var animal2 = new Animal("dog",3);
console.log(animal1.fun === animal2.fun);  // true

控制台截图如下所示

将构造函数中的方法拿到外面来,解决实例化多个对象时,重复的创建函数将构造函数中的方法拿到外面来,解决实例化多个对象时,重复的创建函数
解决办法2利用原型正好解决实例化多个对象时,避免构造函数内的方法重复创建(如后面的示例代码所示)

普通函数与构造函数的区别

示例代码如下所示:

// 声明函数
function Animal(name,age){
   this.name = name;     
   this.age = age;      
   this.fun = function(){  
       return this.name+" "+this.age+"岁了";
   }
//console.log(this); window
}
// 无new的情况
var animal1 = Animal("cat",2);
var animal2 = Animal("dog",3);
console.log(animal1 instanceof Animal); // false
console.log(animal2 instanceof Animal); // false
console.log(Object.prototype.toString.call(animal1));//[object Undefined]
console.log(Object.prototype.toString.call(animal2));
console.log(name,age);   // dog 3
console.log(animal1.name,animal1.age); //报错

控制台输出结果


无new函数的调用无new函数的调用

从上面的代码中可以看出,当一个函数无new关键字的调用时,构造函数中的this对象指向的是全局对象window,所以构造函数式依靠new提供返回值,上面的类型检测,值为undefined,正是如此,没有使用new,则为普通函数,只不过是一个没有返回值的语句函数,对this赋值属性和方法,相当于在全局下添加属性和方法,如果加了use strict,在严格模式下,还会报,严格模式下,并没有全局对象设置this,返回的是undefined

针对以上问题,如果想普通函数也具有构造函数的功能,怎么做?如下代码所示

// 声明构造函数
function Animal(name,age){
// 加一this条件判断,用instanceof来检查自己是否被new调用
if(this instanceof Animal){
   this.name = name;     
   this.age = age;      
   this.fun = function(){  
     return this.name+" "+this.age+"岁了"; }
}else{
// 以new递归调用自己来为对象创建正确的实例,这样做的目的是在不同的情况下表现出一致的行为,常常是为了保护那些忘记了使用new的情况
     return new Animal(name,age);
}
}
// 无new的情况
var animal1 = new Animal("cat",2);
var animal2 = Animal("dog",3);
console.log(animal1 instanceof Animal); // true
console.log(animal2 instanceof Animal); // true             
console.log(Object.prototype.toString.call(animal1));//[object object]
console.log(Object.prototype.toString.call(animal2));//[object object]
console.log(animal1.name,animal1.age);
console.log(animal2.name,animal2.age);

控制台输出结果如下


为了使普通函数与构造函数结果一致,加一条件判断,作用域安全函数为了使普通函数与构造函数结果一致,加一条件判断,作用域安全函数

为何内置构造函数无new也能工作

示例代码如下所示

var arr = Array; // 当没有参数时,构造函数后面的圆括号可以省略
var obj = Object({
    name:"随笔川迹",
    sex:"boy",
    fun:function(){
       return this.name+" "+this.sex+" "+Object.prototype.toString.call(this);
}});
console.log(obj.fun());

截图如下所示

内置构造函数无new也能工作内置构造函数无new也能工作
原因:因为那些内置系统构造函数(Array,Object,RegExp,Date,Error,String等)都被设计为作用域安全的构造函数,也就是说在整个全局范围内都是可见的,一个作用域安全的构造函数无new也可以工作,并返回同样类型的对象

原型对象

protype:

function ProtoFun(width,height){
    this.width = width;
    this.height = height;
    this.method = function(){
        return "我是构造函数下自定义的方法"
     }
}
// 构造函数.原型下添加属性
ProtoFun.prototype.color = "red"; 
// 构造函数.原型下添加方法
ProtoFun.prototype.fun = function(){
  return this.width+" "+this.height+" "+this.color;
}
// 上面两个通常可以合并成下面一个
ProtoFun.prototype.init = {
  color:"red",
  fun:function(){
     return this.width+" "+this.height+" "+this.color;
  }
}
var elemObj1 = new ProtoFun(100,100);
var elemObj2 = new ProtoFun(200,200);
console.log(elemObj1.width,elemObj1.height);  // 100 100
console.log(elemObj2.width,elemObj2.height);  // 200 200                  
console.log(elemObj1.color,elemObj1.fun(),elemObj1.init.color,elemObj1.init.fun());           
console.log(elemObj2.color,elemObj2.fun(),elemObj2.init.color,elemObj2.init.fun());
console.log(elemObj1.method===elemObj2.method); // false
console.log(elemObj1.fun === elemObj2.fun);     // true

控制台输出结果如下

什么是原型,改写构造函数(对象)的属性和方法,达到共享属性和方法什么是原型,改写构造函数(对象)的属性和方法,达到共享属性和方法

如下示例:

// 未用原型写法,普通写法求和
var arr1 = [1,2,3,4,5,6,7,8,9,10];
var arr2 = [2,4,6,8,10,12,14,16]
arr1.sum = function(){
    var result = 0;
    for(var i = 0;i<arr1.length;i++){ // 这里也可以换成this.length
    result += this[i];
}
    return result;   // 返回结果
}
arr2.sum = function(){
var result = 0;
for(var i = 0;i<arr2.length;i++){
    result += this[i];
}
return result;   // 返回结果
}
console.log("数组arr1和为",arr1.sum()); // 55
console.log("数组arr2和为",arr2.sum()); // 72

控制台截图如下:

未使用原型下的方法未使用原型下的方法

原型写法

 // 原型写法
var arr1 = [1,2,3,4,5,6,7,8,9,10];
var arr2 = [2,4,6,8,10,12,14,16]
Array.prototype.sum = function(){
var result = 0;
 for(var i = 0;i<this.length;i++){
    result += this[i];
 }
    return result;
}
console.log("数组arr1的和为",arr1.sum()); // 数组arr1的和为55 
console.log("数组arr2的和为",arr2.sum()); // 数组arr2的和为72
console.log(arr1.sum === arr1.sum);       // true
//普通函数封装写法,也就是闭包写法
var arr1 = [1,2,3,4,5,6,7,8,9,10];
var arr2 = [2,4,6,8,10,12,14,16]
function AddResult(arr){
    arr.sum = function(){
    var result = 0;
    for(var i = 0;i<this.length;i++){ // 这里也可以换成this.length
        result += this[i];
    }
       return result;   // 返回结果
   }
 return arr.sum();
}
console.log("数组arr1和为",AddResult(arr1)); // 数组arr1和为55
console.log("数组arr2和为",AddResult(arr2)); // 数组arr2和为72

区分构造函数自定义属性与原型属性

如下示例代码所示:

function Person(name,publicNum){
  this.name = name;
  this.publicNum = publicNum;
  this.userDefined = function(){
     return "我是构造函数自定义方法unerDefined"
  }
}
Person.prototype.age = 25;
Person.prototype.init = {
    city:"beijing",
    job:"coder",
    method:function(){
        return "我是原型下的方法输出"
    }
}
// 定义鉴别原型属性方法
function hasPrototypeProperty(object,variable){
    return !object.hasOwnProperty(variable) && (variable in object);
}   
var person = new Person("随笔川迹","itclancoder");
console.log(person.name,person.publicNum,person.userDefined(),person.init.city,person.init.job,person.init.method());
console.log(hasPrototypeProperty(person,"name"));
console.log(person.hasOwnProperty("name"));
console.log(person.hasOwnProperty("age")); // hasOwnProperty只能检测自定义属性,false
console.log(hasPrototypeProperty(person,"age"));
console.log("age" in person);  // true,in操作符既能检测自定义属性也能检测出原型下的属性

控制台输出结果如下:


鉴别原型属性还是自定义属性鉴别原型属性还是自定义属性

使用对象字面量形式改写原型对象会改变构造函数的属性,指向问题

function Person(name,job){
    this.name = name;
    this.job = job;
}
Person.prototype.init = {
    name:"小川",
    job:"码男",
    outName:function(){
        return this.name;
    },
    outJob:function(){
        return this.job;
    }
}
var person = new Person("随笔川迹","coder");
console.log(person.name,person.job);
console.log(person.init.outName(),person.init.outJob());
console.log(person.constructor === Person);  // true
console.log(person instanceof Person); // true

控制台输出结果如下:


constructor指向问题constructor指向问题

若将上面的代码更改如下:

function Person(name,job){
    this.name = name;
    this.job = job;
}
// 使用对象字面量形式改写原型对象
 Person.prototype ={
    name:"小川",
    job:"码男",
    outName:function(){
        return this.name;
},
outJob:function(){
       return this.job;
 }
}
var person = new Person("随笔川迹","coder");
console.log(person.name,person.job);
console.log(person.outName(),person.outJob());
console.log(person.constructor === Person);  // false
console.log(person.constructor === Object);  // true
console.log(person instanceof Person);      // true

控制台输出结果如下

对象字面量改写原型对象constructor指向会发生问题对象字面量改写原型对象constructor指向会发生问题
正确写法:当一个函数被创建时,它的prototype属性也被创建,且该原型对象的constructor属性指向该函数,当使用对象字面量形式改写原型对象Person.prototype时,则该constructor指向指向的是Object,为了避免这一点,需要手动的改写原型对象手动设置constructor属性,更改如下:
function Person(name,job){
   this.name = name;
   this.job = job;
}
// 使用对象字面量形式改写原型独享
Person.prototype ={
    constructor:Person,  // 手动指定这里的指向该构造函数
    outName:function(){
        return this.name;
    },
    outJob:function(){
        return this.job;
    }
}
var person = new Person("随笔川迹","coder");
console.log(person.name,person.job);
console.log(person.outName(),person.outJob());
console.log(person.constructor === Person);  // true
console.log(person.constructor === Object);  // false
console.log(person instanceof Person);      // true

在原有的对象基础上上,通过prototype进行额外的,封装,拓展

实例代码如下:

// 通过原型prototype对现有的内容进行额外的拓展,给数组Array添加方法
Array.prototype.sum = function(){
   return this.reduce(function(m,n){
        return m+n;
    })
}
var arrNums = [1,2,3,4,5,6,7,8,9,10];
var result = arrNums.sum();
console.log("arrNums的和为",result);  // arrNums的和为 55
// 给String添加额外的方法
String.prototype.capitalize = function(){
    return this.charAt(0).toUpperCase()+this.substring(1);
}
var message = "suibichuanji hello";
console.log(message.capitalize());   // Suibichuanji hello

控制台输出如下:


通过原型prototype进行额外拓展通过原型prototype进行额外拓展

以上例子中,我们是可以通过对系统提供的内置对象进行额外拓展的,也就是说系统对象(Date,String,Object,Array,RegExp等)是构造函数,当现有提供的功能没法满足时,就可以根据prototype进行拓展,因此都有原型对象给你去改变,在该新增的方法前面添加构造函数.prototype就可以了,上面的例子中是给Array.prototype添加了一个sum()求和的方法,该方法对数组所有元素进行求和并返回,arrNums数组通过原型对象自动就有了这个sum()方法,在sum()方法内部,this指向数组对象实例arrNums,所以该方法也可以使用数组的其他方法,什么reduce(),substring(),等都可以

原型中的属性优先级

示例代码如下所示

var arr = [];
arr.name = "随笔川迹";
Array.prototype.name = "川流不息";
console.log(arr.name);  // 随笔川迹 

控制台输出如下

原型属性的优先级原型属性的优先级
从上结果中可以得出:当构造函数自定义的属性名与该构造函数下原型属性名相同时,构造函数的自定义属性优先于原型属性(可以把构造函数理解为内联样式),而原型属性或者原型方法可以看做是class)
小结:构造函数就是用new关键字调用的普通函数,可以随时定义自己的构造函数来创建多个具有同样的属性的对象,可以用instanceof操作符(建议用这个)者直接访问constructor属性来鉴别对象是被哪个构造函数创建的,每一个函数都具有prototype属性,它定义了构造函数所有对象共享属性

面向对象小实例

效果图:

简单选项卡示例简单选项卡示例
css层叠样式代码
*{
    padding:0;
    margin:0;
}
#wrap{
   width:300px;
   height:260px;
   border:1px solid #ccc;
   margin:0 auto;
}
#wrap:after{
   content:"";
   height:0;
   display:block;
   clear:both;
   zoom:1;
}
#wrap div{
  height:100%;
  display:none;
  text-indent:10px;
  background:#2263A3;
  color:#fff;
}
#wrap div:nth-of-type(1){
  display:block;
}
#wrap input.active{
  background:#2263A3;
  color:#fff;
}
#wrap input{
    width:100px;
    height:30px;
    background:#abcdef;
    text-align:center;
    line-height:30px;
    outline:none;
    border:none;
    float:left;
    cursor:pointer;
    margin-bottom:30px;
 }

html结构

<div id="wrap">
  <input type="button" class="active" value="公告" name="">
  <input type="button" value="规则" name="">
  <input type="button" value="论坛" name="">
  <div>欢迎关注微信itclancoder公众号</div>
  <div>点击右上方蓝字即可关注</div>
  <div>什么都没有留下</div>
</div>

js代码

// 普通写法
// 获取元素
var oWrap = document.querySelector("#wrap");
var aBtns = oWrap.getElementsByTagName("input");
var aDivs = oWrap.getElementsByTagName("div"); 
// 循环
for(var i = 0;i<aBtns.length;i++){
    aBtns[i].index = i;  //添加索引
    aBtns[i].onclick = function(){  // 添加事件
    for(var j = 0;j<aBtns.length;j++){
    aBtns[j].className = "";       // 先去除掉所有的className
    aDivs[j].style.display = "none"; // 先隐藏
}
   // 添加class
   this.className = "active";
   aDivs[this.index].style.display = "block"; // 内容显示
}
}

jquery写法

$(function(){
    $(" #wrap input").click(function(){
    var $index = $(this).index(); // 获取索引
      $(this).addClass("active").siblings().removeClass("active");
     $("#wrap div").eq($index).show().siblings("div").hide();
   })
})```
`面向对象写法`

function 构造函数(){
this.属性 // 对象.属性
}
构造函数.原型.方法 = function(){}
var 对象1 = new 构造函数();
对象1.方法();

`面向对象选项卡代码示例如下所示`

window.onload = function(){
var t = new TabSelect(); // 实例化对象
t.init(); // 实例化对象调用方法
}
// 声明构造函数
function TabSelect(){
// this == TabSelect,添加自定义属性,获取对象
this.oWrap = document.querySelector("#wrap");
this.aBtns = this.oWrap.getElementsByTagName("input");
this.aDivs = this.oWrap.getElementsByTagName("div");
}
// 构造函数的原型下添加方法(初始化)
TabSelect.prototype.init = function(){
var that = this; // 注意这里的this指向,是TabSelect,用一个局部变量将this给存储起来,其实这种方式是根据词法作用域,闭包的方式来解决的
for(var i = 0;i<this.aBtns.length;i++){
this.aBtns[i].index = i; //添加索引
this.aBtns[i].onclick = function(){
that.change(this);
//console.log(this);匿名函数里面的this指向的是input按钮元素
};
}
}
// 构造器函数原型对象下添加方法
TabSelect.prototype.change = function(obj){
//console.log(obj); // input点击按钮元素
for(var j = 0;j<this.aBtns.length;j++){
this.aBtns[j].className = ""; // 先去除掉所有的className
this.aDivs[j].style.display = "none"; // 先隐藏
}
// 添加class
obj.className = "active";
this.aDivs[obj.index].style.display = "block"; // 内容显示
}

`小结`:
本例从普通写法,jquery写法,在到最后面向对象选项卡写法,完成一简易的选项卡,其中jquery写法最为简单,容易懂,但是这里我只是为了尝试用面向对象的思想去写应用,实际开发中,无论哪种方式,只要能实现出来就行,从普通的写法,也就是原生js写法,到面向对象写法,可以看出首先通过变形,把局部的功能给拎出来,封装成一个函数,这个过程中尽量不要出现函数嵌套函数,因为this是指向是个令人头疼的问题,可以有全局变量,window.onload里面尽量是实例化对象,与对象的调用的方式,把不是赋值的语句单独放到一个函数当中(比如上文中的获取元素,给TabSelect添加自定义属性),最后就是改变this指向问题,事件或者定时器,让面向对象中的this指向该对象

`总结`:

本篇主要是本人对构造器函数与原型对象的一点点理解,new操作符调用的函数为构造函,功能上与内置的函数并没有多大的区别,构造函数首字母大写用来区分普通函数还是构造函数,构造函数中的this指向该实例化的构造函数,主要是创建多个共享特定属性和行为的对象,用于创建模板,作为饼干工具,而原型对象主要是改写构造函数(对象)下面的方法和属性,让公用方法或者属性在内存中存在一份,解决了当创建多个实例化对象时,重复的创建构造函数的过程,目的是减少内存开销,提高性能,还有就是原型在原有的对象基础上,通过prototype进行额外的,封装,拓展,原型是挂载在构造函数下面的,以及最后用面向对象写法实现了一个小实例,其实设计模式中的原型模式就是面向对象的写法,杀鸡焉用牛刀,适合自己的才是好方法,面向对象的写法,对于简单的实例,面向过程就可以了,对于复杂的实例,什么组件,插件,我觉得都是面向对象的应用,关于面向对象,我也只是略知皮毛,在不断的学习当中...

`以下是本篇提点概要`

* 什么是函数:function关键字声明,一独立封闭功能的代码块,也是对象
* 什么是构造函数:new关键字创建对象时调用的函数,用于创建模板,生成饼干工具
* 普通函数与构造函数的区别,有new无new区别,this的指向,普通函数,this指向全局window,而构造器函数this,指向该new 构造器函数调用
* 为何内置构造函数无new也能工作,因为那些内置系统构造函数,都被设计为作用域安全的构造函数,一个作用域安全的构造函数无new也可以工作,并返回同样类型的对象
* 原型对象,prototype,函数一旦声明,就有该属性,作用1:去改写对象下面公用的方法或者属性,让公用方法或者属性在内存中存在一份,可以看作是对象的基类
作用2:在原有的对象基础上上,通过prototype进行额外的,封装,拓展
* 区分构造函数自定义属性与原型属性,用in操作符,hasOwnProperty组合使用进行判断(见上示例代码)
* 使用对象字面量形式改写原型对象会改变构造函数的属性,指向问题,需手动的改写原型对象手动设置constructor属性
* 在原有的对象基础上,通过prototype进行额外的,封装,拓展
* 原型中的属性优先级,构造函数自定义的属性优先级优先于原型属性,查找变量的方式是沿着原型链逐级查找的,直至顶层Object,有则返回,无则返回undeinfed
* 面向对象小实例,普通写法,JQuery写法与面向对象选项卡写法















上一篇下一篇

猜你喜欢

热点阅读