JS原型与面向对象总结
ECMAScript有两种开发模式:1.函数式(过程化),2.面向对象(OOP)。面向对象的语言有一个标志,那就是类的概念,而通过类可以创建任意多个具有相同属性和方法的对象。但
是,ECMAScript没有类的概念,因此它的对象也与基于类的语言中的对象有所不同。
创建对象
创建一个对象,然后给这个对象新建属性和方法。
var box = new Object(); //创建一个 Object对象
box.name = 'Lee';//创建一个 name属性并赋值
box.age = 100;//创建一个 age属性并赋值
box.run = function () {//创建一个 run()方法并返回值
return this.name + this.age + '运行中...';
};
alert(box.run());//输出属性和方法的值
上面创建了一个对象,并且创建属性和方法,在 run()方法里的 this,就是代表 box对象
本身。这种是 JavaScript创建对象最基本的方法,但有个缺点,想创建一个类似的对象,就
会产生大量的代码。
var box2 = box;
//得到 box的引用
box2.name = 'Jack';
//直接改变了 name属性
alert(box2.run())
//用 box.run()发现 name也改变了
var box2 = new Object();
box2.name = 'Jack';
box2.age = 200;
box2.run = function () {
return this.name + this.age + '运行中...';
};
alert(box2.run());
//这样才避免和 box混淆,从而保持独立
为了解决多个类似对象声明的问题,我们可以使用一种叫做工厂模式的方法,这种方法
就是为了解决实例化对象产生大量重复的问题。
function createObject(name, age) { //集中实例化的函数
var obj = new Object();
obj.name = name;
obj.age = age;
obj.run = function () {
return this.name + this.age + '运行中...';
};
return obj;
}
var box1 = createObject('Lee', 100);//第一个实例
var box2 =createObject('Jack',200);//第二个实例
alert(box1.run()); //Lee100运行中...
alert(box2.run()); //Jack200运行中...
//保持独立
工厂模式解决了重复实例化的问题,但还有一个问题,那就是识别问题,因为根本无法
搞清楚他们到底是哪个对象的实例。
alert(typeof box1); //Object
alert(box1 instanceof Object);//true
ECMAScript中可以采用构造函数(构造方法)可用来创建特定的对象。类型于 Object对
象。
function Box(name, age) {//构造函数模式
this.name = name;
this.age = age;
this.run = function () {
return this.name + this.age + '运行中...';
};
}
var box1 = new Box('Lee', 100);
var box2 = new Box('Jack',200);
alert(box1.run());
alert(box1 instanceof Box); //很清晰的识别他从属于 Box
使用构造函数的方法,即解决了重复实例化的问题,又解决了对象识别的问题,但问题是,这里并没有 new Object(),为什么可以实例化 Box(),这个是哪里来的呢?
使用了构造函数的方法,和使用工厂模式的方法他们不同之处如下:
1.构造函数方法没有显示的创建对象(new Object());
2.直接将属性和方法赋值给 this 对象;
3.没有 renturn 语句。
构造函数的方法有一些规范:
1.函数名和实例化构造名相同且大写,(PS:非强制,但这么写有助于区分构造函数和
普通函数);
2.通过构造函数创建对象,必须使用 new 运算符。
既然通过构造函数可以创建对象,那么这个对象是哪里来的,new Object()在什么地方执行了?执行的过程如下:
1.当使用了构造函数,并且 new 构造函数(),那么就后台执行了 new Object();
2.将构造函数的作用域给新对象,(即 new Object()创建出的对象),而函数体内的 this 就
代表 new Object()出来的对象。
3.执行构造函数内的代码;
4.返回新对象(后台直接返回)。
关于 this 的使用,this 其实就是代表当前作用域对象的引用。如果在全局范围 this 就代表 window 对象,如果在构造函数体内,就代表当前的构造函数所声明的对象。
var box = 2;
alert(this.box); //全局,代表 window
构造函数和普通函数的唯一区别,就是他们调用的方式不同。只不过,构造函数也是函数,必须用 new 运算符来调用,否则就是普通函数。
var box = new Box('Lee', 100); //构造模式调用
alert(box.run());
Box('Lee', 20); //普通模式调用,无效
var o = new Object();
Box.call(o, 'Jack', 200) //对象冒充调用
alert(o.run());
探讨构造函数内部的方法(或函数)的问题,首先看下两个实例化后的属性或方法是否相
等。
var box1 = new Box('Lee', 100); //传递一致
var box2 = new Box('Lee', 100); //同上
alert(box1.name == box2.name); //true,属性的值相等
alert(box1.run == box2.run); //false,方法其实也是一种引用地址
alert(box1.run() == box2.run()); //true,方法的值相等,因为传参一致
可以把构造函数里的方法(或函数)用 new Function()方法来代替,得到一样的效果,更加证明,他们最终判断的是引用地址,唯一性。
function Box(name, age) { //new Function()唯一性
this.name = name;
this.age = age;
this.run = new Function("return this.name + this.age + '运行中...'");
}
我们可以通过构造函数外面绑定同一个函数的方法来保证引用地址的一致性,但这种做法没什么必要,只是加深学习了解:
function Box(name, age) {
this.name = name;
this.age = age;
this.run = run;
}
function run() { //通过外面调用,保证引用地址一致
return this.name + this.age + '运行中...';
}
虽然构造函数创建对象可以使用全局的函数 run()来解决了保证引用地址一致的问题,但这种方式又带来了一个新的问题,全局中的 this 在对象调用的时候是 Box 本身,而当作普通函数调用的时候 ,this 又代表 window。
三. 原型
我们创建的每个函数都有一个 prototype(原型)属性,这个属性是一个对象,它的用途是包含可以由特定类型的所有实例共享的属性和方法。逻辑上可以这么理解:prototype 通过调用构造函数而创建的那个对象的原型对象。使用原型的好处可以让所有对象实例共享它所包含的属性和方法。也就是说,不必在构造函数中定义对象信息,而是可以直接将这些信息添加到原型中。
function Box() {} //声明一个构造函数
Box.prototype.name = 'Lee'; //在原型里添加属性
Box.prototype.age = 100;
Box.prototype.run = function () { //在原型里添加方法
return this.name + this.age + '运行中...';
};
比较一下原型内的方法地址是否一致:
var box2 = new Box();
var box1 = new Box();
alert(box1.run == box2.run);//true,方法的引用地址保持一致
为了更进一步了解构造函数的声明方式和原型模式的声明方式,我们通过图示来了解一构造函数方式下:
原型模式方式:
在原型模式声明中,多了两个属性,这两个属性都是创建对象时自动生成的。__proto__属性是实例指向原型对象的一个指针,它的作用就是指向构造函数的原型属性 constructor。通过这两个属性,就可以访问到原型里的属性和方法了。PS:IE浏览器在脚本访问__proto__会不能识别,火狐和谷歌浏览器及其他某些浏览器均能识别。虽然可以输出,但无法获取内部信息。
alert(box1.__proto__);//[object Object]
判断一个对象是否指向了该构造函数的原型对象,可以使用 isPrototypeOf()方法来测试。
alert(Box.prototype.isPrototypeOf(box)); //只要实例化对象,即都会指向
原型模式的执行流程:
1.先查找构造函数实例里的属性或方法,如果有,立刻返回;
2.如果构造函数实例里没有,则去它的原型对象里找,如果有,就返回;
虽然我们可以通过对象实例访问保存在原型中的值,但却不能访问通过对象实例重写原型中的值。
var box1 = new Box();
alert(box1.name);//Lee,原型里的值
box1.name = 'Jack';
alert(box.1name);//Jack,就近原则
var box2 = new Box();
alert(box2.name);//Lee,原型里的值,没有被 box1修改
如果想要 box1也能在后面继续访问到原型里的值,可以把构造函数里的属性删除即可,
具体如下:
delete box1.name;//删除属性
alert(box1.name);
如何判断属性是在构造函数的实例里,还是在原型里?可以使用 hasOwnProperty()函数来验证:
alert(box.hasOwnProperty('name'));//实例里有返回 true,否则返回 false
构造函数实例属性和原型属性示意图
in 操作符会在通过对象能够访问给定属性时返回 true,无论该属性存在于实例中还是原
型中。
alert('name' in box); //true,存在实例中或原型中
我们可以通过 hasOwnProperty()方法检测属性是否存在实例中,也可以通过 in 来判断
实例或原型中是否存在属性。那么结合这两种方法,可以判断原型中是否存在属性。
function isProperty(object, property) { //判断原型中是否存在属性
return !object.hasOwnProperty(property) && (property in object);
}
var box = new Box();
alert(isProperty(box, 'name')) //true,如果原型有
为了让属性和方法更好的体现封装的效果,并且减少不必要的输入,原型的创建可以使用字面量的方式:
function Box() {};
Box.prototype = { //使用字面量的方式
name : 'Lee',
age : 100,
run : function () {
return this.name + this.age + '运行中...';
}
};
使用构造函数创建原型对象和使用字面量创建对象在使用上基本相同,但还是有一些区别,字面量创建的方式使用 constructor 属性不会指向实例,而会指向 Object,构造函数创建的方式则相反。
var box = new Box();
alert(box instanceof Box);
alert(box instanceof Object);
alert(box.constructor == Box); //字面量方式,返回 false,否则,true
alert(box.constructor == Object); //字面量方式,返回 true,否则,false
如果想让字面量方式的 constructor 指向实例对象,那么可以这么做:
Box.prototype = {
constructor : Box, //直接强制指向即可
};
PS:字面量方式为什么 constructor 会指向 Object?因为 Box.prototype={};这种写法其实就是创建了一个新对象。而每创建一个函数,就会同时创建它 prototype,这个对象也会自动获取 constructor 属性。所以,新对象的 constructor 重写了 Box 原来的 constructor,因此会指向新对象,那个新对象没有指定构造函数,那么就默认为 Object。原型的声明是有先后顺序的,所以,重写的原型会切断之前的原型。
function Box() {};
Box.prototype = { //原型被重写了
constructor : Box,
name : 'Lee',
age : 100,
run : function () {
return this.name + this.age + '运行中...';
}
};
Box.prototype = {
age = 200
};
var box = new Box(); //在这里声明
alert(box.run()); //box 只是最初声明的原型
原型对象不仅仅可以在自定义对象的情况下使用,而 ECMAScript 内置的引用类型都可以使用这种方式,并且内置的引用类型本身也使用了原型。
alert(Array.prototype.sort); //sort 就是 Array 类型的原型方法
alert(String.prototype.substring); //substring 就是 String 类型的原型方法
String.prototype.addstring = function () { //给 String 类型添加一个方法
return this + ',被添加了!'; //this 代表调用的字符串
};
alert('Lee'.addstring()); //使用这个方法
PS:尽管给原生的内置引用类型添加方法使用起来特别方便,但我们不推荐使用这种方法。因为它可能会导致命名冲突,不利于代码维护。
原型模式创建对象也有自己的缺点,它省略了构造函数传参初始化这一过程,带来的缺点就是初始化的值都是一致的。而原型最大的缺点就是它最大的优点,那就是共享。原型中所有属性是被很多实例共享的,共享对于函数非常合适,对于包含基本值的属性也还可以。但如果属性包含引用类型,就存在一定的问题:
function Box() {};
Box.prototype = {
constructor : Box,
name : 'Lee',
age : 100,
family : ['父亲', '母亲', '妹妹'], //添加了一个数组属性
run : function () {
return this.name + this.age + this.family;
}
};
var box1 = new Box();
box1.family.push('哥哥'); //在实例中添加'哥哥'
alert(box1.run());
var box2 = new Box();
alert(box2.run()); //共享带来的麻烦,也有'哥哥'了
PS:数据共享的缘故,导致很多开发者放弃使用原型,因为每次实例化出的数据需要保留自己的特性,而不能共享。
为了解决构造传参和共享问题,可以 组合构造函数+ 原型模式:
function Box(name, age) { //不共享的使用构造函数
this.name = name;
this.age = age;
this. family = ['父亲', '母亲', '妹妹'];
};
Box.prototype = { //共享的使用原型模式
constructor : Box,
run : function () {
return this.name + this.age + this.family;
}
};
PS:这种混合模式很好的解决了传参和引用共享的大难题。是创建对象比较好的方法。原型模式,不管你是否调用了原型中的共享方法,它都会初始化原型中的方法,并且在
声明一个对象时,构造函数+原型部分让人感觉又很怪异,最好就是把构造函数和原型封装到一起。为了解决这个问题,我们可以使用 动态原型模式。
function Box(name ,age) { //将所有信息封装到函数体内
this.name = name;
this.age = age;
if (typeof this.run != 'function') { //仅在第一次调用的初始化
Box.prototype.run = function () {
return this.name + this.age + '运行中...';
};
}
}
var box = new Box('Lee', 100);
alert(box.run());
当第一次调用构造函数时,run()方法发现不存在,然后初始化原型。当第二次调用,就不会初始化,并且第二次创建新对象,原型也不会再初始化了。这样及得到了封装,又实现
了原型方法共享,并且属性都保持独立。
if (typeof this.run != 'function') {
alert('第一次初始化'); //测试用
Box.prototype.run = function () {
return this.name + this.age + '运行中...';
};
}
var box = new Box('Lee', 100); //第一次创建对象
alert(box.run()); //第一次调用
alert(box.run()); //第二次调用
var box2 = new Box('Jack', 200); //第二次创建对象
alert(box2.run());
alert(box2.run());
PS:使用动态原型模式,要注意一点,不可以再使用字面量的方式重写原型,因为会切断实例和新原型之间的联系。
以上讲解了各种方式对象创建的方法,如果这几种方式都不能满足需求,可以使用一开始那种模式:寄生构造函数。
function Box(name, age) {
var obj = new Object();
obj.name = name;
obj.age = age;
obj.run = function () {
return this.name + this.age + '运行中...';
};
return obj;
}
寄生构造函数,其实就是工厂模式+构造函数模式。这种模式比较通用,但不能确定对象关系,所以,在可以使用之前所说的模式时,不建议使用此模式。
在什么情况下使用寄生构造函数比较合适呢?假设要创建一个具有额外方法的引用类型。由于之前说明不建议直接 String.prototype.addstring,可以通过寄生构造的方式添加。
function myString(string) {
var str = new String(string);
str.addstring = function () {
return this + ',被添加了!';
};
return str;
}
var box = new myString('Lee'); //比直接在引用原型添加要繁琐好多
alert(box.addstring());
在一些安全的环境中,比如禁止使用 this 和 new,这里的 this 是构造函数里不使用 this ,这里的 new 是在外部实例化构造函数时不使用 new。这种创建方式叫做稳妥构造函数。
function Box(name , age) {
var obj = new Object();
obj.run = function () {
return name + age + '运行中...'; //直接打印参数即可
};
return obj;
}
var box = Box('Lee', 100); //直接调用函数
alert(box.run());
PS:稳妥构造函数和寄生类似。
JS原型与面向对象总结的更多相关文章
- JavaScript面向对象(二)——成员属性、静态属性、原型属性与JS原型链
前 言 JRedu 上一篇博客中,我们介绍了JS中的面向对象,并详细的解释了this的指向问题.本篇博客,我们继续来学习JS的面向对象.来深入理解一下JavaScript OOP中的成员属性/方 ...
- 简单粗暴地理解js原型链–js面向对象编程
简单粗暴地理解js原型链–js面向对象编程 作者:茄果 链接:http://www.cnblogs.com/qieguo/archive/2016/05/03/5451626.html 原型链理解起来 ...
- 深入分析JS原型链以及为什么不能在原型链上使用对象
在刚刚接触JS原型链的时候都会接触到一个熟悉的名词:prototype:如果你曾经深入过prototype,你会接触到另一个名词:__proto__(注意:两边各有两条下划线,不是一条).以下将会围绕 ...
- js原型对象与Java类的比较
在我学习Javascript的原型对象时,我总是不自觉地拿Java的类思想来做比较,时间久了,我渐渐认识到二者有巨大的不同. 首先,类是不能直接当方法来运行,它最简便的方式是调用其静态方法:而原型对象 ...
- 深入理解JS原型链与继承
我 觉得阅读精彩的文章是提升自己最快的方法,而且我发现人在不同阶段看待同样的东西都会有不同的收获,有一天你看到一本好书或者好的文章,请记得收藏起来, 隔断时间再去看看,我想应该会有很大的收获.其实今天 ...
- Day046--JavaScript-- DOM操作, js中的面向对象, 定时
一. DOM的操作(创建,追加,删除) parentNode 获取父级标签 nextElementSibling 获取下一个兄弟节点 children 获取所有的子标签 <!DOCTYPEhtm ...
- 前端 ---JS中的面向对象
JS中的面向对象 创建对象的几种常用方式 1.使用Object或对象字面量创建对象 2.工厂模式创建对象 3.构造函数模式创建对象 4.原型模式创建对象 1.使用Object或对象字面量创建对象 ...
- python 全栈开发,Day52(关于DOM操作的相关案例,JS中的面向对象,定时器,BOM,client、offset、scroll系列)
昨日作业讲解: 京东购物车 京东购物车效果: 实现原理: 用2个盒子,就可以完整效果. 先让上面的小盒子向下移动1px,此时就出现了压盖效果.小盒子设置z-index压盖大盒子,将小盒子的下边框去掉, ...
- 37.js----浅谈js原型的理解
浅谈Js原型的理解 一.js中的原型毫无疑问一个难点,学习如果不深入很容易就晕了! 在参考了多方面的资料后,发现解释都太过专业,对于很多还没有接触过面向对象 语言的小白来说,有理解不了里面 ...
随机推荐
- javascript中的栈堆内存
<--------栈内存---------> 俗称叫做作用域(全局作用域/私有作用域) >为js代码提供的执行环境(执行js代码的地方) >基本数据内省是直接存放在栈内存中的 ...
- HTML5页面CSS Reset
/*------------------*//*reset*//*------------------*/* {box-sizing: border-box; -webkit-tap-highligh ...
- python__基础 : 多继承中方法的调用顺序 __mro__方法
在多继承中,如果一个子类继承了两个平级的父类,而这两个父类有两个相同名字的方法,那么一般先继承谁,调用方法就调用先继承的那个父类的方法.如: class A: def test(self): prin ...
- thinkphp 5.1/tp5.1 route路由bug
tp5.1下面RuleItem类中,match方法. 如果同一个控制器下面,写了两个路由,后一个路由比包含前一个路由,则访问后一个路由地址的时候,会跳转到前面定义的那个路由
- 【NXP开发板应用—智能插排】1.如何使用scp传输文件
首先感谢深圳市米尔科技有限公司举办的这次活动并予以本人参加这次活动的机会,以往接触过嵌入式,但那都是皮毛,最多刷个系统之类的,可以说对于嵌入式系统开发这件事情是相当非常陌生的,这次活动为我提供了一个非 ...
- Rsync+inotify实现文件实时同步#附shell脚本
强烈推荐先仔细看此文 https://segmentfault.com/a/1190000002427568 实验环境 centos 7.3 vm2:192.168.221.128 同步服务器 vm1 ...
- python 新手函数基础(函数定义调用值传递等)
1.编程的集中主要方式: 面向过程 >类 >>关键字class 面向函数>函数 >> 关键字def 面向过程> 过程 >> 关键字def 2.py ...
- 转载:git和github新手安装使用教程(三步入门)
转载防止以后电脑重装,找不到记录. 教程地址:https://www.cnblogs.com/ttjsndx/p/7943444.html
- C++ 数组复制
参考:https://blog.csdn.net/huangxiaohui123/article/details/81984175 补充: int tu[N][N],dis[N][N]; memcpy ...
- nmap教程(上)
一.nmap的主要功能 1.端口扫描 2.主机探测:查找目标网络的在线主机 3.服务/版本检测:发现开放端口后,进一步检测目标主机的检测服务协议.应用程序名称.版本号等信息 4.操作系统检测 5.网络 ...