首页 > 代码库 > 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(面向对象)