首页 > 代码库 > HTML 学习笔记 JavaScript(面向对象)

HTML 学习笔记 JavaScript(面向对象)

现在让我们继续跟着大神的脚步前进 学习一下JavaScript中的面向对象的思想,其实作为一个iOS开发者,对面向对象还是比较熟悉的,但是昨晚看了一下Js中的面向对象,妈蛋 一脸萌比啊。还好有大神。让我们跟着大神的思路在捋一下。(在这里更欢迎大家阅读原博 )原博地址:http://www.cnblogs.com/dolphinX/p/4385862.html

理解对象

对象这个词如雷贯耳,同样出名的一句话:XXX语言中一切皆为对象!

对象是什么?什么觉面向对象的编程?

对象(object),台湾译作物件,是面向对象(Object Oriented)中的术语,既表示客观世界问题空间(Namespace)中的某个具体的事物,又表示软件系统解空间中的基本元素。

在软件系统中,对象具有唯一的标识符,对象包括属性(Properties)和方法(Methods),属性就是需要记忆的信息,方法就是对象能够提供的服务。在面向对象(Object Oriented)的软件中,对象(Object)是某一个类(Class)的实例(Instance)。 —— 维基百科

对象是从我们现实生活中抽象出来的一个概念,俗话说物以类聚,人以群分,我们也经常说有一类人,他们专业给隔壁家制造惊喜,也就是我们说的老王

这里面就有两个重要概念

类:无论是物以类聚,还是有一类人,这里说的类并不是实际存在的事物,是一些特征、是一些规则等
老王:这是个实物,是现实存在,和类的关系就是符合类的描述
对应到计算机术语,类就是class,定义了一些特点(属性 property)和行为(方法 method),比如说给隔壁制造惊喜的这类人有几个特征

长相文质彬彬,为人和善
姓王
同时这些人还有技能(行为)

帮隔壁修下水道
亲切问候对方儿子
我们刚才就描述了一个类,用代码表示就是

class LaoWang{
    string name;
    string familyNmae = "wang";
    bool isKind = true;

    LaoWang(string name){
        this.name = name;
    }

    void fixPipe(){
        statement
    }

    void greetSon(){
        statement
    }
}

符合这些特点并且有上述行为能力的,我们称之为老王,从描述我们就可以看出来LaoWang不是指某个人,而是指一类人,符合上述描述的都可能是老王!用计算机术语说就是没个活蹦乱跳的老王都是类LaoWang的实例。用代码描述就是

LaoWang lw1 = new LaoWang("yi");
LaoWang lw2 = new LaoWang("er");
...
LaoWang lw1000000 = new LaoWang("baiwan");

可以看出我们能够根据类LaoWang实例化出成千百万个老王来,老王不是一个人在战斗!

封装

刚才我们说的已经涉及到了对象的一个重要特性——封装

以前我们可能会有这样的描述

王一长相文质彬彬,为人和善,姓王,有技能帮隔壁修下水道、亲切问候对方儿子
王二长相文质彬彬,为人和善,姓王,有技能帮隔壁修下水道、亲切问候对方儿子
王三长相文质彬彬,为人和善,姓王,有技能帮隔壁修下水道、亲切问候对方儿子
王四长相文质彬彬,为人和善,姓王,有技能帮隔壁修下水道、亲切问候对方儿子
...
王百万长相文质彬彬,为人和善,姓王,有技能帮隔壁修下水道、亲切问候对方儿子

有了对象的思想我们可以这样说了,首先定义一类人

有那么一类人

1. 长相文质彬彬,为人和善
2. 姓王

同时这些人还有技能(行为)

1. 帮隔壁修下水道
2. 亲切问候对方儿子

然后是实例化,也就是对号入座

王一是老王
王二是老王
...
王百万是老王

也就是说 我们通过类来描述一套规则,其中包括

1:属性

2:行为

对于这个类实例化出的对象,也就是副歌这个类描述的对象,不用去关心对象细节,我们认为符合类的描述,就会有类规定的属性和方法,至于每个方法具体实现细节不去关注,比如老王怎么给人修水管,我知道他有修水管的技能,然后用的时候让他去修就好了(只要不修我家的)

我们称这种隐藏细节的特征叫做封装

JavaScript 对象


因为JavaScript是基于原型(prototype)的,没有类的概念(ES6有了,这个暂且不谈),我们能接触到的都是对象,真正做到了一切皆为对象

所以我们再说对象就有些模糊了,很多同学会搞混类型的对象和对象本身这个概念,我们在接下来的术语中不提对象,我们使用和Java类似的方式,方便理解

function People(name){
    this.name = name;

    this.printName = function(){
        console.log(name);
    };
}

这是一个函数,也是对象,我们称之为类

var p1 = new People(Byron);

p1是People类new出来的对象,我们称之为实例

类和实例的关系用我们码农的专业眼光看起来是这样的

技术分享

类就是搬砖的模具,实例就是根据模具印出来的砖块,一个模具可以印出(实例化)多个实例,每个实例都符合类的特征,这个例子和我们JavaScript中概念很像

在Java中类不能称之为对象,如同老王是一个概念、规则的集合,但是在JavaScript中,本身没有类的概念,我们需要用对象模拟出类,然后用类去创建对象
我们的例子中模具虽然是“类”,但同时也是个存在的实物,是个对象,我们为了方便理解,称之为类

Object


 

我们知道JavaScript有null、undefined、number、boolean、string五种简单类型,null和undefined分别表示没有声明和声明后没有初始化的变量、对象,是两个简单的值,其余三个有对应的包装对象Number、Boolean、String

其它的就是object 类型了,比如常用的Array Date RegExp等,我们常用的Function也是一个对象 虽然

typeof function(){}; // "function"

但是Function实例和其他类型的实例没有什么区别,都是对象,只不过 typeof操作符对其作了处理。

在JavaScript中使用对象很简单,使用new操作符执行Obejct函数就可以构建一个最基本的对象

var obj = new Object();

我们称new调用的函数为构造函数,构造函数和普通函数的区别仅仅在于是否使用了new来调用,它们的返回值也会不同

所谓“构造函数”,就是专门用来生成“对象”的函数。它提供模板,作为对象的基本结构。一个构造函数,可以生成多个对象,这些对象都有相同的结构

我们可以通过.来位对象添加属性和方法

obj.name = Byron;
obj.printName = function(){
    console.log(obj.name);
};

这么写比较麻烦,我们可以使用字面量来创建一个对象,下面的写法和上面等价

var obj = {
    name: Byron,
    printNmae: function(){
        console.log(obj.name);
    }
}

构造对象

我们可以抛开类 使用字面量来构造一个对象

var obj1 = {
    nick: Byron,
    age: 20,
    printName: function(){
        console.log(obj1.nick);
    }
}
var obj2 = {
    nick: Casper,
    age: 25,
    printName: function(){
        console.log(obj2.nick);
    }
}

问题

这样构造有两个明显问题

太麻烦了,每次构建一个对象都是复制一遍代码
如果想个性化,只能通过手工赋值,使用者必需了解对象详细
这两个问题其实也是我们不能抛开类的重要原因,也是类的作用

使用函数做自动化

function createObj(nick, age){
  var obj = {
    nick: nick,
    age: age,
    printName: function(){
            console.log(this.nick);
        }
  };
  return obj;
}

var obj3 = createObj(Byron, 30);
obj3.printName();

我们通过创建一个函数来实现自动创建对象的过程,至于个性化通过参数实现,开发者不必关注细节,只需要传入指定参数即可
问题
这种方法解决了构造过程复杂,需要了解细节的问题,但是构造出来的对象类型都是Object,没有识别度

有型一些


要想让我们构造出的函数有型一些 我们需要了解一些额外的知识

function作为构造函数(通过new操作符调用) 的时候会返回一个类型为function的name对象

function 可以接受参数,可以根据参数创建相同类型的不同值对象

function 实例作用域内有一个constructor属性,这个属性就可以指示其构造器

new

new运算符接受一个函数F及其参数 new F(arguments...)。这一过程分为三步:

1.创建类的实例 这步是把一个空的对象的 proto 属性设置为 F.prototype 。
2.初始化实力 函数F被传入参数并调用,关键字this被设定为该实例
3.返回实例

根据这几个特性 我们来改造一下创建对象的方式

function Person(nick, age){
    this.nick = nick;
    this.age = age;
    this.sayName = function(){
            console.log(this.nick);
    }
}
var p1 = new Person();

instanceof

instanceof 是一个操作符 可以判断对象是否为某个类型的实例

p1 instanceof Person; // true
p1 instanceof Object;// true

instanceof 判断的是对象

1 instanceof Number; // false

问题

构造函数在解决了上面所有问题,同时为实例带来了类型,但可以注意到每个实例printName方法实际上作用一样,但是每个实例要重复一遍,大量对象存在的时候是浪费内存

构造函数

任何函数只要使用new表达式就是构造函数

每个函数都自动添加一个名称为prototype属性,这是一个对象

每个对象(实例)都有一个内部属性__proto__(规范中没有指定这个名称,但是浏览器中都是这么实现的)指向其类型的prototype属性,类的实例也是对象,其proto属性指向“类”的prototype

prototype

技术分享

通过图我们可以看出一些端倪,实例可以通过__proto__访问到其类型的prototype属性,这就意味着类的prototype对象可以作为一个公共容器,供所有实例访问。

抽象重复


我们刚才的问题可以通过这个手段解决

所有实例都会通过原型链引用到类型的prototype

prototype相当于特定类型所有实例都可以访问到的一个公共容器

重复的东西移到公共容器里放一份就可以了

看下代码

function Person(nick, age){
    this.nick = nick;
    this.age = age;
}
Person.prototype.sayName = function(){
    console.log(this.nick);
}

var p1 = new Person();
p1.sayName();

这个时候我们对应的关系是这样的

技术分享

终于有个靠谱的构建对象的方式了

What‘s this?

由于运行期绑定的特性 JavaScript中的this含义非常多,它可以是全局对象,当前对象或任意对象,这完全取决于函数的调用方式

随着函数使用场合的不同 this的值也会放声变化,但是又一个总的原则,就是this指的是 调用函数的那个对象。

作为函数调用


在函数被直接调用的时候,this绑定到全局对象 在浏览器中 window就是该全局对象。

console.log(this);//window

function fn1(){
    console.log(this);
}

fn1();//window

内部函数

函数嵌套产生的内部函数的this 不是其父函数 仍然是全局变量

console.log(this);//window
function fn0() {
    function fn() {
        alert(this);//window
    }
    fn();
}
fn0();

setTimeout setInterval

这两个方法执行的时候 this也是全局对象

document.addEventListener("click",function(e) {
    console.log(this);//HTMLDocument
    setTimeout(function(){
        console.log(this);//window
    },200);
});

作为构造函数调用


所谓构造函数,就是通过这个函数生成一个新对象(object)。这时,this就指这个新对象

new 运算符接受一个函数 F 及其参数:new F(arguments...)。这一过程分为三步:

创建类的实例。这步是把一个空的对象的 proto 属性设置为 F.prototype 。
初始化实例。函数 F 被传入参数并调用,关键字 this 被设定为该实例。
返回实例。

看例子

function Person(name){
    this.name = name;
}
Person.prototype.printName = function(){
    console.log(this.name);
};

var p1 = new Person(Byron);
var p2 = new Person(Casper);
var p3 = new Person(Vincent);

p1.printName();//Byron
p2.printName();//Casper
p3.printName();//Vincent

作为对象方法调用


在JavaScript中 函数也是对象,因此函数可以作为一个对象的属性,此时函数被称为该对象的方法,在使用这种方式调用时 this被自然绑定到该对象

var obj1 = {
    name: ‘Byron‘,
    fn : function(){
        alert(typeof this);//object
    }
};

obj1.fn();

小陷阱

var fn2 = obj1.fn;

fn2();//window

DOM对象绑定事件


在事件处理程序中this代表事件源DOM对象(低版本IE有bug,指向了window)

document.addEventListener(click, function(e){
    console.log(this);//HTMLDocument
    var _document = this;
    setTimeout(function(){
        console.log(this);//window
        console.log(_document);//HTMLDocument
    }, 200);
}, false);

Function.prototype.bind


 

bind 返回一个新函数 并且使函数内部的this为传入的第一个参数

var obj1 = {
    name: Byron,
    fn : function(){
        console.log(this);//object
    }
};
var fun3 = obj1.fn.bind(obj1);
fun3();//Object { name: "Byron", fn: obj1.fn() }

使用call和apply设置this


call apply,调用一个函数,传入函数执行上下文及参数

fn.call(context, param1, param2...)

fn.apply(context, paramArray)

语法很简单,第一个参数就是希望蛇者的this对象,不同之处在于call方法接收参数列表 而apply接收参数数组。

fn2.call(obj1);
fn2.apply(obj1);

caller


在函数A调用函数B的时候,被调用函数B会自动生成一个caller属性 指向调用他的函数对象,如果函数当前未被调用或并非被其他函数调用,则caller为Null

function fn4(){ console.log(fn4.caller); function fn(){ console.log(fn.caller); } fn(); } fn4();

函数的执行环境


JavaScript中的函数既可以被当作普通的函数执行 也可以作为对象的方法执行,这是导致this含义如此丰富的主要原因

一个函数被执行时,会创建一个执行环境(ExecutionContext),函数的所有行为均发生在此执行环境中,构建该执行环境时,JavaScript首先会创建arguments变量,其中包含调用函数时传入的参数

接下来时创建作用域链,然后初始化变量,首先初始化函数的形参表,值为arguments变量中对应的值,如果 arguments变量中没有对应值,则该形参初始化为 undefined。

如果该函数中含有内部函数,则初始化这些内部函数,如果没有,继续初始化该函数内定义的局部变量,需要注意的是此时的这些变量初始化为undefined,其赋值操作在执行环境创建成功后,函数执行时才会执行,这点对于我们理解JavaScript中的变量作用域非常重要,最后为this变量赋值,会根据函数调用方式的不同,赋给this对象,当前对象等。

至此函数的执行环境(ExecutionContext)创建成功,函数开始逐行执行,所需变量均从之前构建好的执行环境(ExecutionContext)中读取

三种变量

实例变量:(this) 类的实例才能访问到的变量

静态变量:(属性)直接类型对象能访问到的变量

私有变量:(局部变量)当前作用域内有效的变量

看个例子:

function ClassA(){
    var a = 1; //私有变量,只有函数内部可以访问
    this.b = 2; //实例变量,只有实例可以访问
}

ClassA.c = 3; // 静态变量,也就是属性,类型访问

console.log(a); // error
console.log(ClassA.b) // undefined
console.log(ClassA.c) //3

var classa = new ClassA();
console.log(classa.a);//undefined
console.log(classa.b);// 2
console.log(classa.c);//undefined

原型连接和继承


在一切开始之前回顾一下类 实例 prototype  __proto__的关系

function Person(nick, age){
    this.nick = nick;
    this.age = age;
}
Person.prototype.sayName = function(){
    console.log(this.nick);
}

var p1 = new Person();
p1.sayName();

1.我们通过函数定义了"类":Person,函数自动获得prototype

2.每个类的实例都会有一个内部属性__proto__ 指向类的prototype属性。

有趣的现象


我们定义一个数组,调用其valueof()的方法

var numbers = [1,2,3];

alert(numbers.valueOf());//[1,2,3]

很奇怪的是我们在数组的类型Array中并不能找到valueOf的定义,根据之前的理论那么极有可能定义在了Array的prototype中,用于实例共享方法。查看一下

技术分享

我们发现Array的prototype里面并未包含valueOf等定义,那么valueOf是哪里来的呢?

一个有趣的现象是我们在Object实例的__proto__属性(也就是Object的prototype属性)中找到了找到了这个方法

技术分享

那么Array的实例为什么同样可以查找到Object的prototype里面定义的方法呢?

查找valueof过程


因为任何类型的prototype本质上也是个类Object的实例,所以prototype也和其他实例一样有个__proto__内部属性,指向Object的prototype

我们大概知道为什么了,自己类的prototype找不到的话,还会找prototype的类型的prototype((类似于父类的概念))属性,这样层层向上查找

大概过程是这样的

1>查找当前对象obj 查找obj属性 方法 找到后返回

2>没有找到 通过obj的__proto__属性,找到其类型Array的prototype属性(记为prop)继续查找,找到后返回

3>没有找到,把prop记为obj做递归重复步骤一,通过类方法找到prop的类型Object的prototype进行查找,找到后返回。

技术分享

这就是传说中的原型链,层层向上查找,最后还没有就返回undefined

用iOS中面向对象的思想来说的话 就是Array继承与Object,当实例化一个Array对象,当这个实例调用函数时,先查找Array中有没有这个方法,有调用,没有在查找其父类Object中的方法(函数) 如果有返回调用 没有 在找Object的父类,最后找不到的话 报错崩溃。

类型

我们之前介绍过instanceof操作符,判断对象是不是某个类型的实例

[1, 2, 3] instanceof Array; //true

可以看到[1,2,3]是类型Array的实例

[1, 2, 3] instanceof Object; //true

这个结果有些非议所思,怎么又是Array的实例,又是Object的实例,这不是乱套了

其实这个现象在日常生活中很常见 比如我们有两种类型

1类人猿
2动物

我们发现黑猩猩即是类人猿这个类的物种(实例),也是动物的实例

是不是悟出其中的门道了,类人猿是动物的一种,也就是说我们的两个类型之间有一种父子关系

这就是传说中的继承,JavaScript正是通过原型链实现继承机制的

继承


继承是指一个对象直接使用另外一个对象的属性和方法。

JavaScript并不提供原生的继承机制,我们自己实现的方式很多,介绍一种最为通用的。

通过上面的描述,我们可以看出我们如果实现了以下两点就可以说我们实现了继承

1.得到一个类的属性

2.得到一个类的方法。

我们分开讨论一下,先定义两个类

function Person(name,sex) {
    this.name = name;
    this.sex = sex;
}
Person.prototype.printName = function () {
    console.log(this.name);
}

function Male(gae) {
    this.age = age;
}
Male.prototype.printAge = function() {
    console.log(this.age);
}

属性获取

对象属性的获取是通过构造函数的执行,我们在一个类中执行另外一个类的构造函数,就可以把属性赋值到自己的内部,但是我们需要把环境改变到自己的作用域内,这就要借助我们讲过的函数call了

改造一下 Male

function Male(name,sex,age) {
    Person.call(this,name,sex);
    this.age = age;
}
Male.prototype.printAge = function() {
    console.log(this.age);
}

实例化 看看结果

var m = new Male("Byron","man",26);
console.log(m.sex);//max

那么我们类比一下iOS中的面向对象  Person.call()到底做了什么呢?仔细想一想是不是类似于子类实现自己构造函数的时候 写的一句[super init]呢,调用父类的方法完成继承的属性的初始化,再在自己的构造方法内完成拓展属性的初始化。总的来说,我感觉Person.Call().完成的工作就相当于[super init]。但是在JavaScript中他的具体作用,我作为初学者还是不太清楚,以后,我会专门写一遍博客介绍一下。

方法获取

我们知道类的方法都定义在了prototype里面,所以只要我们把子类的prototype改为父类的prototype的备份就好了。

Male.prototype = Object.create(Person.prototype);

这里我们通过Object.create() clone了一个新的prototype而不是直接把Person.prototype直接赋值,因为引用关系,这样会导致后续修改了子类的prototype也修改了父类的prototype,因为修改的是一个值。

另外 Object.create 是ES5方法,之前版本通过遍历属性也可以实现浅拷贝。

这样做需要注意的一点就是对子类添加方法,必须在修改其prototype之后,如果在之前会被覆盖掉。

Male.prototype.printAge = function(){
    console.log(this.age);
};

Male.prototype = Object.create(Person.prototype);

这样的话,printAge方法在赋值后就没了,因此得这么写

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

Male.prototype = Object.create(Person.prototype);

Male.prototype.printAge = function(){
    console.log(this.age);
};

这样写貌似没什么问题了,但是有个问题就是我们知道prototype对象有一个属性constructor 指向其类型,因此我们复制的父元素的prototype,这个时候constructor属性指向是不对的,导致我们判断类型出错。

console.log(Male.prototype.constructor);//function Person()

因此我们需要再重新制定一下constructor属性到自己的类型

最终方案

我们可以通过一个函数实现刚才说的内容

function inherit(superType,subType) {
    var _prototype = Object.create(superType.prototype);
    _prototype.constructor = subType;
    subType.prototype = _prototype;
}

使用方式

function inherit(superType,subType) {
    var _prototype = Object.create(superType.prototype);
    _prototype.constructor = subType;
    subType.prototype = _prototype;
}

function Person(name,sex) {
    this.name = name;
    this.sex = sex;
}
Person.prototype.printName = function () {
    console.log(this.name);
}

function Male(name,sex,age) {
    Person.call(this,name,sex);
    this.age = age;
}
inherit(Person,Male);
//Male.prototype = Object.create(Person.prototype);
Male.prototype.printAge = function() {
    console.log(this.age);
}

var m = new Male("Byron","man",26);
console.log(m.sex);//max
m.printName();
console.log(Male.prototype.constructor);//function Male()

这样 我们就在JavaScript中实现了继承。

hasOwnProperty


继承之后Male的实例也有了Person的方法,那么怎么判断某个是自己的还是父类的?

hasOwnProperty 是Object.prototype的一个方法,可以判断一个对象是否包含自定义属性而不是原型链上的属性。hasOwnProperty 是Js中唯一一个处理属性但不是查找原型链的函数

m.hasOwnProperty(‘name‘); // true
m.hasOwnProperty(‘printName‘); // false
Male.prototype.hasOwnProperty(‘printAge‘); // true

 

HTML 学习笔记 JavaScript(面向对象)