JavaScript中的面向对象(一)

面向对象编程

面向对象英文全称叫做ObjectOriented,简称 OO。OO其实包括 OOA(ObjectOrientedAnalysis,面向对象分析 ) 、OOD(ObjectOrientedDesign,面向对象设计 )和 OOP(ObjectOrientedProgramming,面向对象的程序设计 ) 。
面向对象的语法只对应 OOP,只是 OO的一部分 。

一个典型的 OO编程过程应该是先整理需求 ,根据需求进行 OOA ,将真实世界的客观物件抽象成程序中的类或对象 ,这个过程经也称建模 , OOA的输出结果是一个个类或对象的模型图 。接下来要进行 OOD ,这一步的目的是处理类之间的耦合关系 ,设计类或对象的接口 ,此时会用到各种设计模式 ,例如观察者模式 、责任链模式等 。 OOA和 OOD是个反复迭代的过程 ,它们本身也没有非常清晰的边界 ,是相互影响 、制约的 。等 OOA和OOD结束之后 ,才到 OOP ,进行实际的编码工作 。OOA和 OOD是面向对象编程的思想和具体语言无关 ,而 OOP是面向对象编程的工具 ,和选用的语言相关 。 OOP是 OOA和 OOD的底层 ,不同语言的语法不同 ,所以 OOP不同 ,但 OOA和 OOD与具体要求语言无关 ,一般情况下可以轻易跨语言重用 。比如 ,我们经常会看到设计模式方面的书会声明是基于 Java或者基于 C #描述的 ,设计模式和具体语言无关 ,无论哪种支持面向对象的语言都可以使用设计模式 。

OOP是使用面向对象技术的基础 ,面向对象的思维最后是要通过 OOP来实施的 。但 OOP并不等于 OO ,甚至并不是 OO。OOA才是要重要 、最关键的部分 ,绝大多数情况下 ,我们的程序可能用不上很复杂的 OOD设计 , OOA会占据大部分的开发时间 , OOP反而只占很少量的时间 。OOA的熟练度直接关系到开发的速度 ,而 OOA能力的提升很需要经验的积累 。

面向过程和面向对象编程

写一个简单的电话本程序,实现简单的增删查改

//定义电话本
var phonebook = [
    {name : "张三" , tel : 123456} ,
    {name : "李四" , tel : 234561} ,
    {name : "王五" , tel : 345612} ,
    {name : "赵六" , tel : 456123} ,
    {name : "小七" , tel : 561234} 
]
//增
function add(oBook,oName,oTel) {
    oBook.push({name : oName , tel : oTel})
}
//删
function remove(oBook,oName,oTel) {
    var n;
    for (var i=0; i<oBook.length; i++) {
        if(oBook[i].name == oName) {
            n = i;
            break;
        }
    }
    if (n != undefined) {
        oBook.splice(n,1)
    }
}
//查
function getTel(oBook,oName) {
    var tel = "";
    for (var i=0; i<oBook.length; i++) {
        if(oBook[i].name == oName) {
            tel = oBook[i].tel;
            break;
        }
    }
    return tel;
}
//改
function modify(oBook,oName,oTel) {
    for (var i=0; i<oBook.length; i++) {
        if(oBook[i].name == oName) {
            oBook[i].tel = oTel;
            break;
        }
    }
}

//使用程序
//从电话本中找到张三的号码
var str = getTel(phonebook,"张三");
console.log(str)

这种编程方式就是一种面向过程编程。程序由数据和函数组成,如果要执行什么操作
就把数据传给相应的’处理函数’,经过处理之后返回我们想要的结果。

面向过程存在的问题:
(1)数据和处理函数没有直接的联系,在执行操作时,我们不但要选择相应的处理函数还要自己准备处理函数需要的数据,也就是说需要同时关注处理函数和数据。
(2) 数据和函数都暴露在同一个作用域里面,没有私有和共有的概念,整个程序中所有的数据和处理函数都可以互相访问,在开发阶段的初期也许开发速度很快但是到后期的维护阶段,由于整个程序的耦合非常紧,任何一个处理函数和数据都有可能关联到其他地方,容易牵一发而动全身,加大维护难度。
(3) 其实面向过程的思维方式是典型的计算机思维方式———输入数据给处理器,
处理器内部执行运算,处理器返回结果。在计算机的世界里只能识别0和1,而现实生活中,我们思路并不是这样的简单,所有的东西在我们脑海中都是有状态有动作的物件。用面向过程的思维我们无法描绘客观世界的事物,因为无法直接使用生活中的思维方式。

面向过程的方式描述一个人与面向对象的方式描述一个人。

面向过程
var name = "Haha";
var state = "awake";
var say = function(oName) {
    console.log("I'm " + oName);
};
var sleep = function(oState) {
    oState = "asleep"
};
say(name);
sleep(state);

面向对象
var person = {
    name : "Haha",
    state : "awake",
    say : function(oName) {
        console.log("I'm " + this.name)
    },
    sleep = function() {
        this.state = "asleep"
    }
};
person.say();
person.sleep();

以上代码中,程序由变量和函数组成,而变量和函数无法联系起来共同描述同一个物件。
用计算机的思维进行编程其实是很难描述的,因为面向过程的思维方式是在描述一个个”动作”,
有动作的起点(初始数据),有动作的过程(初始数据传给处理函数进行处理),
有动作的终点(处理函数返回处理结果),而客观世界中存在的是一个个”物件”,
物件有状态,有动作,物件本身只是一个客观存在,它没有起点,没有终点。
能用面向过程思维描述的只是物件的动作,例如我开始睡觉(起点),意识逐渐模糊(过程),
睡着了(终点)。用面向过程的思维方式是无法描绘客观世界的事物的,
因为编程时无法直接使用生活中的思维方式。

而面向对象编程就是抛开计算机思维,使用生活中的思维进行编程的编程方式。
面向过程的思维就是描述一个个”动作”,面向对象的思维则是描述一个个”物件”,
客观生活中的物件,都可以通过面向对象思维映射到程序中。
在程序中我们管”物件”叫做”对象”,对象由两部分组成:”属性”和”行为”,
对应客观世界中物件的”状态”和”动作”。属性本质其实是个变量相当于面向过程中的数据,
而行为的本质其实是函数相当于面向过程中的处理函数。

面向对象改写电话本程序
//定义电话本类
function Phonebook(opt){
    this._phonebook = opt;
}
PhoneBook.prototype = {
    //增
    add : function(oName,oTel) {
        this._phonebook.push({name : oName , tel : oTel})
    },
    //删
    remove : function(oName) {
        var n;
        for (var i=0; i<this._phonebook.length; i++) {
            if(this._phonebook[i].name == oName) {
                n = i;
                break;
            }
        }
        if (n != undefined) {
            this._phonebook.splice(n,1)
        }
    },
    //查
    getTel : function(oName) {
        var tel = "";
        for (var i=0; i<this._phonebook.length; i++) {
            if(this._phonebook[i].name == oName) {
                tel = this._phonebook[i].tel;
                break;
            }
        }
        return tel;
    },
    //改
    modify : function(oName,oTel) {
        for (var i=0; i<this._phonebook.length; i++) {
            if(this._phonebook[i].name == oName) {
                this._phonebook[i].tel = oTel;
                break;
            }
        }
    }

}

//使用
var book1 = new Phonebook([
    {name : "张三" , tel : 123456} ,
    {name : "李四" , tel : 234561} ,
    {name : "王五" , tel : 345612} ,
    {name : "赵六" , tel : 456123} ,
    {name : "小七" , tel : 561234} 
]);

var result = book1.getTel("张三");
console.log(result)

ECMAScript三种开发模式

  • 面向过程
  • 面向对象
  • 函数式编程

javascript中的面向对象

和java这种基于类(class-base)的面向对象的编程语言不同,
javascript没有类这样的概念,但是javascript也是面向对象的语言,
这种面向对象的方式成为 基于原型(prototype-base)的面向对象。
虽然说ES6已经引入了类的概念来作为模板,通过关键字 “class” 可以定义类,
但ES6的这种写法可以理解为一种语法糖,它的绝大部分功能,ES5都可以做到,
新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
如果要理解基于原型实现面向对象的思想,那么理解javascript中的
三个重要概念: 构造函数(constructor)、原型(prototype)、原型链(prototype chain)
对帮助理解基于原型的面向对象思想就显得尤为重要。

构造函数

function Student(name, age){
    this.name = name;
    this.age = age;
    this.sayName = function(){alert(this.name);};
}
var s1 = new Student("王铁锤", 18);

这种方式调用构造函数实际上会经历以下4个步骤:

  • 创建一个新对象( 在内部隐式调用了new Object() )
  • 将构造函数的作用域赋给新对象(把this绑定到实例对象)
  • 执行构造函数中的代码(为这个新对象添加属性)
  • 返回新对象。

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

唯一区别:调用方式不同。构造函数首字母一定要大写。

任何函数,只要通过new操作符来调用,它就可以作为构造函数;
而任何构造函数,如果不通过new 操作符来调用,那它跟普通函数无区别。

this的指向

  • 用new关键字执行:this指向生成的实例对象
  • 普通函数执行:this指向调用函数的对象

原型对象prototype

  • 我们创建的每个函数都有一个prototype属性,这个属性值指向原型对象
  • 原型对象默认包含一个constructor属性,指向构造函数
  • 任何写在原型对象中的属性和方法都可以让所有对象实例共享

实例

用new关键字生成的对象称为实例,实例会复制构造函数内所有的属性和方法。
当调用构造函数创建一个新实例后,该实例的内部将包含一个内部属性[[prototype]],指向原型对象。
这个内部属性可以通过以下方式获取(通过实例得到原型对象):
    在FF,Chrome等浏览器可以通过私有属性__proto__得到;
    通过ES5方式去获取:Object.getPrototypeOf(实例);
function Student(name, age){
    this.name = name;
    this.age = age;
    this.sayName = function(){alert(this.name);};
}
var s1 = new Student("王铁锤", 18);
s1就称作构造函数Student的实例

构造函数、原型对象和实例的关系

1,每个构造函数都有一个原型对象(prototype)

2,原型对象都包含一个指向构造函数的指针(constructor)

3,而实例都包含一个指向原型对象的内部指针([[prototype]])

判断原型和实例的关系(返回布尔值)
constructor: 得到构造函数的引用,一般用于判断该实例是否由某一构造函数生成
    实例.constructor == Student //true
instanceof: 检测某个对象是不是某一构造函数的实例,适用于原型链中的所有实例
    实例 instanceof Student //true
    实例 instanceof Object //true
isPrototypeOf: 判断当前对象是否为实例的原型
    原型对象.isPrototypeOf(实例) //true

构造函数方法很好用,但是单独使用存在一个浪费内存的问题(所有的实例会复制所有构造函数中的属性/方法)。
这样既不环保,也缺乏效率。通常的做法是:使用构造函数添加私有属性和方法,使用原型添加共享的属性和方法。

原型链

实例与Object原型对象之间的链条称为原型链

原型链搜索机制

  • 读取实例对象的属性时,先从实例对象本身开始搜索。如果在实例中找到了这个属性,则返回该属性的值
  • 如果没有找到,则继续搜索实例的原型对象,如果在原型对象中找到了这个属性,则返回该属性的值
  • 如果还是没找到,则向原型对象的原型对象查找,依此类推,直到Object的原型对象(最顶层对象)
  • 如果再Object的原型对象中还搜索不到,则抛出错误

重置原型对象

重置原型对象,可以一次性给原型对象添加多个方法

function Popover(){}
Popover.prototype = {
    constructor:Popover,
    show:function(){},
    hide:function(){}
}
记得要加这一个属性constructor:Popover来解决原型对象与构造函数之家的识别问题。

内置原型对象

使用内置原型可以给已有构造函数添加方法

例如:给所有的字符串添加一个获取ascii码的方法
String.prototype.toascii = function() {
    var result = [];
    for (var i=0; i<this.length; i++) {
        result.push(this[i].charCodeAt())
    }
    return result;
}

var str = 'abcd';
str = str.toascii();
console.log(str);// [97, 98, 99, 100]

使用new实例化的原型

每个被new实例化的对象都会包含一个proto 属性,它是对构造函数 prototype 的引用。

  function Foo(){};
  var foo = new Foo();
  console.log(foo.__proto__ === Foo.prototype); // ture

  function Foo(){};
  console.log(Foo.prototype.__proto__ === Object.prototype); // true

上面返回true 的原因是Foo.prototype 是Object预创建的一个对象,是Object创建的一个实例,
所以,Foo.prototype._proto 是Object.prototype的引用。

函数(function)对象的原型

在javascript中,函数是一种特殊的对象,所有的函数都是构造函数 Function 的实例。
所以,函数的原型链与new操作符实例化对象的原型链会不同。

function Foo() {}
console.log(Foo.__proto__ === Object.prototype); // false
console.log(Foo.__proto__ === Function.prototype); // true

从上面代码可以看出,函数Foo的proto属性并不是指向到Object.prototype,
而是指向到Function.prototype,这就说明函数Foo是Function的一个实例。

console.log(Function.__proto__ === Function.prototype); // true
console.log(Function.prototype.__proto__ === Object.prototype); // true

上面代码可以看出,函数Function自己本身也是构造函数Function的一个实例,这段读起来非常拗口,看下面的图:
这里写图片描述
由此可见,Object、Function、Array等等这些函数,都是构造函数Function的实例。

instanceof运算符

instanceof运算符返回一个指定的对象是否一个类的实例,格式如:A instanceof B。其中,左操作数必须是一个对象,右操作数必须是一个类(构造函数)。判断过程:如果函数B在对象A的原型链(prototype chain)中被发现,那么instanceof操作符将返回true,否则返回false。
对照上文中的原型链图,看下面的代码:

function Foo() {}
var foo = new Foo();
console.log(foo instanceof Foo); // true
console.log(foo instanceof Object); // true
console.log(foo instanceof Function); // false,foo原型链中没有Function.prototype
console.log(Foo instanceof Function); // true
console.log(Foo instanceof Object); // true
console.log(Function instanceof Function); // true
console.log(Object instanceof Function); // true
console.log(Function instanceof Object); // true

javascript对象结构图

这里写图片描述
通过图片可以看出,原型链的搜索机制。首先定义一个实例var s1 = new Foo(),s1在带调用某个方法时首先会在本身查找调用的方法,如果自身没有定义则通过proto 找到Foo的原型对象;Foo的原型对象包含着所有构造函数function Foo()的方法,如果在Foo的原型对象没有找到该方法则继续向原型对象的原型对象查找,依此类推,直到Object的原型对象,如果找到了则返回,否则抛出错误。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值