JavaScript面向对象(1)

Javascript是一种基于对象(object-based)的语言,你遇到的所有东西几乎都是对象。但是,它又不是一种真正的面向对象编程(OOP)语言,因为它的语法中没有class(类)。那么,如果我们要把”属性”(property)和”方法”(method),封装成一个对象,甚至要从原型对象生成一个实例对象,我们应该怎么做呢?

作为多年使用Java的我,首先开始思考如下问题:

  1. 在JavaScript中,类如何定义,如何创建对象?
  2. 在JavaScript中,基本类型有对应的对象形式吗?
  3. 在JavaScript中,数组和函数是对象吗?
  4. 在JavaScript中,有继承和接口吗?
  5. 在JavaScript中,如何使用函数这种特殊的对象?
  6. 在JavaScript中,闭包是什么?
  7. 在JavaScript中,有哪些奇技淫巧是Java所没有的?

类和对象

在JavaScript中,类的定义只是一个Map键值对,没有其他。

1
2
3
4
var person = {
name : "xiaohong",
age : 18
}

那么JavaScript中的类中没有方法吗?有!但是也是键值对。

1
2
3
4
5
6
7
var person = {
name : "xiaohong",
age : 18,
eat : function(){
alert("i like eat meat");
}
}

上面的person实际上已近是一个对象实例,我们可以这样使用, 这种方式我们称它为字面量对象

1
2
person.name;
person.eat();

那么问题来了,如何才能像Java中一样定义一个JavaScript的类呢?在JavaScript中定义类的方法除了上面的方法还可以使用下面几种方法。

构造函数法

这个方法是最经典的方法,用构造函数模拟“类”,在其内部可以用this关键字指代实例对象。

1
2
3
4
5
6
7
function Person(){
this.name="xiaohong";
this.age=18;
this.eat=function(){
alert("i like eat meat");
}
}

这样我们就可以像Java中的一样,创建该类的实例对象了,这种方式我们称它为函数对象

1
2
var person = new Person();
person.eat();

Object.create()

1
2
3
4
5
6
7
var person = {
name : "xiaohong",
age : 18,
eat : function(){
alert("i like eat meat");
}
}

我们来思考一下这个问题,一个类其实定义的是一个可以创建多个实例的模板,那么,上面的person即是模板又是对象,如果我们不把上面的person看成一个类的实例对象的话,而是模拟成一个类,我们可以使用Object.create()方法来创建多个实例对象。(其实JavaScript中的面向对象本身就是一种模拟而已)

1
2
3
4
5
6
7
8
9
10
var Person = {
name : "xiaohong",
age : 18,
eat : function(){
alert("i like eat meat");
}
}

var person = Object.create(Person);
person.eat();

是不是看起来特别别扭,模拟面向对象就是这样,因为在JavaScript中没有定义类的方法,所以,所有JavaScript中的类的定义都是通过函数来模拟的。

事实上,在JavaScript中函数就是对象,上面提到了字面量对象是连接到 Object.prototype 的, 函数对象是连接到 Function.prototype 的(而Function对象本身也是连接到 Object.prototype 的)。

极简主义法

我们从上面类和对象的定义,已近知道JavaScript中可以通过var obj = {} 方法来定义一个对象,但是这个对象里面只能定义属性(方法也可以看成一种键值对属性),那么我们可以定义一个构造属性(构造方法)来创建这个对象的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var Person = {
createNew: function(){
var person = {}
person.name="xiaohong",
person.age=18,
person.eat=function(){
alert("i like eat meat");
}
return person;
}
}

var person = Person.createNew();
person.eat();

这种方法的好处是在实现面向对象的一些特征(比如,继承、封装、多态)方面能清晰表达。

基本类型的对象

JavaScript中的基本类型有6种:String,Number,Bolean,Symbol,undefined,null

Symbol很像Java中的UUID,Symbol()就相当于UUID.randomUUID()。

undefined和null这两种数据类型都只有一个值,就是它们的类型名。undefined用于表示变量未初始化,null用于表示对象为空。

Java中的基本数据类型都有对应的包装类,并可以自动装箱和拆箱,JavaScript也有类似的机制。

1
2
3
4
5
var str = new String("dp2px");
var num = new Number(3.1415);
var bool = new Boolean(true) ;

var sym = Symbol(); //特殊,不能 new

我们可以用typeof()来判断他们是不是对象。

那么JavaScript中的自动装箱和拆箱的过程是怎样的呢?

1
2
var hello ='hello';
var str = hello.slice(1); //str 的值是'ello'

我们知道字符串有一些操作函数,其实这个过程是对hello字符串进行了自动装箱,使用的是一个临时的String对象来操作的。

数组函数是对象吗

在JavaScript中我们通常会这样定义一个数组

1
var arr = ["one", "two", "three"];

实际上JavaScript会把上面的代码转换为键值的形式

1
var arr = {"0":"one", "1":"two", "2", "three"}

那么函数是对象吗?

JavaScript中定义函数的方式有两种,一种是函数声明,一种是函数表达式

1
2
3
4
5
6
7
8
9
//函数声明,不会立即执行
function dowork(){
alert("i am work")
}

//函数表达式,会立即执行
var dowork = function(){
alert("i am work")
}

你可以发现两个函数都可以通过dowork()来执行,但是函数声明和函数表达式还是有很大区别的。

函数表达式就有点像Java中的匿名函数,声明的方式会在执行程序前加载,程序文件的任何地方都可以调用声明方式定义的函数,但是表达式方式定义的函数只有在执行到定义的地方才会加载,才能在之后来调用该函数。

1
2
3
4
5
var dowork = function work(){
alert("i am work");
}

dowork();

这个函数是函数表达式,dowork是它的函数名,如果函数内部需要递归调用自己就需要用到work这个名字了。

事实上在JavaScript中函数就相当于一个特殊的对象,可以作为对象的一个属性,而且这个对象可以直接被调用执行。这个特点在纯面向对象的Java语言中是不存在的。

JavaScript中的this

在JavaScript中除了函数声明的参数之外,还有两个附加参数 thisarguments, 而这个 this 参数和 Java 中的不太一样,在JavaScript中函数一共有四种调用模式,在每个调用模式下 this 存在很大差异。

方法调用模式

当一个函数被定义为一个对象的属性时,这个函数被称为方法(注意方法和函数的区别),当一个方法被调用的时候,this被绑定到这个方法所在的对象上,通常使用 . 语法来调用方法。

1
2
3
4
5
6
7
8
9
10
11
12
var obj = {
value : '0',
increment: function(inc){
this.value += typeof inc === 'number' ? inc : 1;
}
}

obj.increment();
document.writeln(obj.value); //结果为 1

obj.increment(2);
document.writeln(obj.value); //结果为 2

这个 this 的绑定过程发生在实际调用方法的时候 obj.increment() , 这里的 increment() 方法可以访问所属对象的上下文,因此被称为公共方法

函数调用模式

当一个函数直接被调用(而不是作为对象的方法)时, this 被绑定到全局对象(这是语言设计上的一个错误),这样就导致了一个问题,问题如下:

1
2
3
4
5
6
7
8
9
10
var obj = {
value : '0';
}

obj.increment = function(inc){

var helper = function(){
this.value += typeof inc === 'number' ? inc : 1; //this.value 不能访问obj的value
}
}

如上代码中 obj 对象绑定了一个方法 increment, 在 increment 方法内有一个函数 helper, 此时 helper 的 this并不是绑定的obj对象而是 helper自己。 所以上面的 this.value 实际上是 helper 的 value。如果要访问上面的 obj 的 value 要通过如下方式(注意下面的that是约定的名称)

1
2
3
4
5
6
7
8
9
10
11
12
var obj = {
value : '0'
}
obj.increment = function(inc){
that = this;
var helper = function(){
that.value += typeof inc === 'number' ? inc : 1;
}
helper();
}
obj.increment(2);
document.writeln(obj.value);

构造器调用模式

JavaScript是一门基于原型继承的语言。Java的继承实际上是一种类型的扩展,但是JavaScript不存在直接扩展一个Class,因为不存在Class这种类型。

JavaScript 只有一种结构:对象。每个实例对象( object )都有一个私有属性(称之为 __proto__ )指向它的构造函数的原型对象(prototype )。该原型对象也有一个自己的原型对象( __proto__ ) ,层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。

如果一个函数通过 new 关键字调用,将创建一个隐藏连接到该函数的 prototype 成员的新对象,同时 this 将会绑定在这个新对象上。

1
2
3
4
5
6
7
8
9
10
var Quo = function(string){
this.status = string;
};

Quo.prototype.get_status = function(){
return this.status; // this 绑定到了新对象,可以访问Quo的属性
};

var myQuo = new Quo("confused");
document.writeln(myQuo.get_status());

apply和call调用模式

apply和call方法的第一个参数都是this的指向对象,第二个参数有些差别:

call 的参数是直接放进去的,第二第三第 n 个参数全都用逗号分隔,直接放到后面 obj.myFun.call(db,'成都', ... ,'string' )
apply 的所有参数都必须放在一个数组里面传进去 obj.myFun.apply(db,['成都', ..., 'string' ])

例如下面代码:

1
2
3
4
5
6
7
8
9
10
11
var name = 'DP2PX', age = 28;
var obj = {
name: '水寒',
objAge: this.age,
myFun: function(){
console.log(this.name + "年龄" + this.age);
}
}

obj.objAge; // 28
obj.myFun(); // 水寒年龄 undefined

上面已经分析了原因, objAge: this.age 这里的 this 指向 window, 而 console.log(this.name + "年龄" + this.age) 这里的 this 指向 obj.

接下来我们使用 apply 和 call 方法来将 this 指向另一个对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var name = 'DP2PX', age = 28;
var obj = {
name: '水寒',
objAge: this.age,
myFun: function(){
console.log(this.name + "年龄" + this.age, "来自" + fm + "去往" + t);
}
}

var db = {
name = "小强",
age = 18
}

obj.objAge; // 28
obj.myFun(); // 水寒年龄 undefined
obj.myFun.call(db, '西安', '北京'); //小强18来自西安去往北京
obj.myFun.apply(db, ['西安', '北京']); //小强18来自西安去往北京
obj.myFun.bind(db, '西安', '北京')(); //小强18来自西安去往北京 bind返回的是一个函数,必须调用

有继承和接口吗

说到这个继承,我们首先得看一下JavaScript的prototype属性是什么。

1
2
3
4
5
6
7
8
9
function Person(name){
this.name=name;
}

var p1 = new Person('xiaoming');
var p2 = new Person('xiaohong');

alert(p1.name);
alert(p2.name);

我们发现,每一个实例对象,都有自己的属性和方法,假设我们在JavaScript中要在类中共享数据,就很难做到了。

上面已经提到过了,在JavaScript中有一个 prototype 属性, 我们可以将所有需要共享的属性和方法放到prototype对象中。将不需要共享的属性和方法放在构造函数中。

1
2
3
4
5
6
7
8
9
10
function Person(name){
this.name=name;
}
Person.prototype={age:28}

var p1 = new Person('xiaoming');
var p2 = new Person('xiaohong');

alert(p1.age);
alert(p2.age);

我们会发现p1.age和p2.age都是28.

由于所有实体类对象都共享了一个prototype对象,而实例对象就好像继承了prototype对象一样,这就是JavaScript继承机制的设计思想。

继承的五种方法

现在有一个动物类和一个猫类,如何来使猫类继承动物类呢?

1
2
3
4
5
6
7
8
function Animal(){
this.species="动物"
}

function Cat(name, color){
this.name=name;
this.color=color;
}

构造函数绑定

第一种方法也是最简单的方法,使用call或apply方法,将父对象的构造函数绑定在子对象上,即在子对象构造函数中加一行:

1
2
3
4
5
6
7
8
function Cat(name, color){
Animal.apply(this, arguments); //注意这里
this.name=name;
this.color=color;
}

var cat1 = new Cat("大黄", "yellow");
alert(cat1.species); //动物

prototype模式

第二种方法更常见,使用prototype属性。当一个函数对象被创建时,Function 构造器产生的函数对象会运行类似这样一些代码:

1
this.prototype = {constructor: this};

新函数对象被赋予一个 prototype 属性,其值是包含一个 constructor 属性且属性值为该新函数对象。

如果 Cat 的 prototype 对象,指向一个 Animal 的实例,那么所有 Cat 的实例,就能继承Animal了。

1
2
3
4
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
var cat1 = new Cat("大黄", "yellow");
alert(cat1.species);

任何一个对象的 prototype 对象都有一个 constructor 属性,指向它的构造函数。如果没有 Cat.prototype = new Animal(); 这一行,Cat.prototype.constructor 是指向 Cat 的, 加了这一行以后,Cat.prototype.constructor 指向 Animal。

每一个实例也有一个constructor属性,默认调用prototype的constructor属性。

1
alert(cat1.constructor == Cat.prototype.constructor); // true

所以如果替换了prototype对象就要纠正constructor的指向。

直接继承prototype

第三种方法是对第二种方法的改进。由于 Animal 对象中,不变的属性都可以直接写入 Animal.prototype 。所以,我们也可以让 Cat() 跳过 Animal(),直接继承 Animal.prototype。

1
2
3
4
5
6
7
8
function Animal(){}
Animal.prototype.species="动物";

Cat.prototype=Animal.prototype;
Cat.prototype.constructor=Cat;

var cat1 = new Cat("大黄", "yellow");
alert(cat1.species);

与前一种方法相比,这样做的优点是效率比较高(不用执行和建立Animal的实例了),比较省内存。缺点是 Cat.prototype 和 Animal.prototype 现在指向了同一个对象,那么任何对 Cat.prototype 的修改,都会反映到 Animal.prototype。

所以,上面这一段代码其实是有问题的。请看第 5 行

1
Cat.prototype.constructor = Cat;

这一句实际上把 Animal.prototype 对象的 constructor 属性也改掉了!

利用空对象作为中介

由于”直接继承 prototype “存在上述的缺点,所以就有第四种方法,利用一个空对象作为中间继承对象。

1
2
3
4
var F = function(){}
F.prototype = Animal.prototype;
Cat.prototype = new F();
Cat.prototype.constructor = Cat;

F 是空对象,所以几乎不占内存。这时,修改 Cat 的 prototype 对象,就不会影响到 Animal 的 prototype 对象。

我们将上面的方法,封装成一个函数,便于使用。

1
2
3
4
5
6
7
function extend(Child, Parent){
var F = function(){};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
Child.uber = Parent.prototype;
}

使用的时候,方法如下

1
2
3
extend(Cat, Animal);
var cat1 = new Cat("大黄", "yellow");
alert(cat1.species);

另外

1
Child.uber = Parent.prototype;

意思是为子对象设一个 uber 属性,这个属性直接指向父对象的 prototype 属性。(uber是一个德语词,意思是”向上”、”上一层”。)这等于在子对象上打开一条通道,可以直接调用父对象的方法。这一行放在这里,只是为了实现继承的完备性,纯属备用性质。

拷贝继承

上面是采用 prototype 对象,实现继承。我们也可以换一种思路,纯粹采用”拷贝”方法实现继承。简单说,如果把父对象的所有属性和方法,拷贝进子对象,不也能够实现继承吗?这样我们就有了第五种方法。

首先,还是把 Animal 的所有不变属性,都放到它的 prototype 对象上。

1
2
function Animal(){}
Animal.prototype.species="动物";

然后,再写一个函数,实现属性拷贝的目的

1
2
3
4
5
6
7
8
function extend(Child, Parent){
var p = Parent.prototype;
var c = Child.prototype;
for(var i in p){
c[i] = p[i];
}
c.uber = p;
}

这个函数的作用,就是将父对象的 prototype 对象中的属性,一一拷贝给 Child 对象的 prototype 对象。

1
2
3
extend(Cat, Animal);
var cat1 = new Cat("大黄", "yellow");
alert(cat1.species);

前面我们看到了5种构造函数实现继承,对于非构造函数对象如何继承呢?

1
2
3
4
5
6
7
var Chiness={
nation:'中国'
}

var Doctor{
career:'医生'
}

怎样才能让”医生”去继承”中国人”

object方法

1
2
3
4
5
6
7
8
function object(o){
function F(){}
F.prototype = o;
return new F();
}

var Doctor = object(Chinese);
Doctor.career = '医生';

这个object()函数,其实只做一件事,就是把子对象的prototype属性,指向父对象,从而使得子对象与父对象连在一起。

浅拷贝

除了使用”prototype链”以外,还有另一种思路:把父对象的属性,全部拷贝给子对象,也能实现继承。

1
2
3
4
5
6
7
8
9
10
11
12
function extendCopy(p){
var c = {};
for(var i in p){
c[i] = p[i];
}
c.uber = p;
return c;
}

var Doctor = extendCopy(Chinese);
Doctor.career = '医生';
alert(Doctor.nation);

但是,这样的拷贝有一个问题。那就是,如果父对象的属性等于数组或另一个对象,那么实际上,子对象获得的只是一个内存地址,而不是真正拷贝,因此存在父对象被篡改的可能。

深拷贝

所谓”深拷贝”,就是能够实现真正意义上的数组和对象的拷贝。它的实现并不难,只要递归调用”浅拷贝”就行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function deepCopy(p, c){
var c = c||{};
for(var i in p){
if(typeof p[i] == 'object'){
c[i] = (p[i].constructor===Array)?[]:{};
deepCopy(p[i], c[i]);
}else{
c[i] = p[i];
}
}
return c;
}

var Doctor = deepCopy(Chinese);

评论

Ajax Android AndroidStudio Animation Anroid Studio AppBarLayout Banner Buffer Bulma ByteBuffer C++ C11 C89 C99 CDN CMYK COM1 COM2 CSS Camera Raw, 直方图 Chrome ContentProvider CoordinatorLayout C语言 DML DOM Dagger Dagger2 Darktable Demo Document DownloadManage Element Error Exception Extensions File FileProvider Fresco GCC Git GitHub GitLab Gradle Groovy HTML5 Handler HandlerThread Hexo Hybrid I/O IDEA IO ImageMagick IntelliJ Intellij Interpolator JCenter JNI JS Java JavaScript JsBridge Kotlin Lab Lambda Lifecycle Lint Linux Looper MQTT MVC MVP Maven MessageQueue Modbus Momentum MySQL NDK NIO NexT Next Nodejs ObjectAnimator Oracle VM Permission PhotoShop Physics Python RGB RS-232 RTU Remote-SSH Retrofit Runnable RxAndroid RxJava SE0 SSH Spring SpringBoot Statubar Task Theme Thread Tkinter UI UIKit UML VM virtualBox VS Code ValueAnimator ViewPropertyAnimator Web Web前端 Workbench api apk bookmark by关键字 compileOnly css c语言 databases demo hexo hotfix html iOS icarus implementation init jQuery javascript launchModel logo merge mvp offset photos pug query rxjava2 scss servlet shell svg tkinter tomcat transition unicode utf-8 vector virtual box vscode 七牛 下载 中介者模式 串口 临潼石榴 主题 书签 事件 享元模式 仓库 代理模式 位运算 依赖注入 修改,tables 光和色 内存 内核 内部分享 函数 函数式编程 分支 分析 创建 删除 动画 单例模式 压缩图片 发布 可空性 合并 同向性 后期 启动模式 命令 命令模式 响应式 响应式编程 图层 图床 图片压缩 图片处理 图片轮播 地球 域名 基础 增加 备忘录模式 外观模式 多线程 大爆炸 天气APP 太白山 头文件 奇点 字符串 字符集 存储引擎 宇宙 宏定义 实践 属性 属性动画 岐山擀面皮 岐山肉臊子 岐山香醋 工具 工厂模式 年终总结 开发技巧 异常 弱引用 恒星 打包 技巧 指针 插件 摄影 操作系统 攻略 故事 数据库 数据类型 数组 文件 新功能 旅行 旋转木马 时序图 时空 时间简史 曲线 杂谈 权限 枚举 架构 查询 标准库 标签选择器 样式 核心 框架 案例 桥接模式 检测工具 模块化 模板引擎 模板方法模式 油泼辣子 泛型 洛川苹果 浅色状态栏 源码 瀑布流 热修复 版本 版本控制 状态栏 状态模式 生活 留言板 相册 相对论 眉县猕猴桃 知识点 码云 磁盘 科学 笔记 策略模式 类图 系统,发行版, GNU 索引 组件 组合模式 结构 结构体 编码 网易云信 网格布局 网站广播 网站通知 网络 美化 联合 膨胀的宇宙 自定义 自定义View 自定义插件 蒙版 虚拟 虚拟机 补码 补齐 表单 表达式 装饰模式 西安 观察者模式 规范 视图 视频 解耦器模式 设计 设计原则 设计模式 访问者模式 语法 责任链模式 贪吃蛇 转换 软件工程 软引用 运算符 迭代子模式 适配器模式 选择器 通信 通道 配置 链表 锐化 错误 键盘 闭包 降噪 陕西地方特产 面向对象 项目优化 项目构建 黑洞
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×