面向对象编程 -- 笔记
构造函数与 new 命令
1. 对象是什么
- 面向对象编程(Object Oriented Programming,缩写为 OOP)
- “对象”(object)到底是什么
- 对象是单个实物的抽象
- 对象是一个容器,封装了属性(property)和方法(method)
2. 构造函数
- 典型的面向对象编程语言(比如 C++ 和 Java),都有“类”(class)这个概念。所谓“类”就是对象的模板,对象就是“类”的实例。但是,JavaScript 语言的对象体系,不是基于“类”的,而是基于构造函数(constructor)和原型链(prototype)。
- JavaScript 语言使用构造函数(constructor)作为对象的模板。所谓”构造函数”,就是专门用来生成实例对象的函数。它就是对象的模板,描述实例对象的基本结构。一个构造函数,可以生成多个实例对象,这些实例对象都有相同的结构。
- 构造函数就是一个普通的函数,为了与普通函数区别,构造函数名字的第一个字母通常大写
3. new 命令
- 基本用法
- new 命令的作用,就是执行构造函数,返回一个实例对象
- 使用 new 命令时,根据需要,构造函数也可以接受参数
- new 命令本身就可以执行构造函数,所以后面的构造函数可以带括号,也可以不带括号。但是为了表示这里是函数调用,推荐使用括号
- 如果忘了使用 new 命令,直接调用构造函数,这种情况下,构造函数就变成了普通函数,并不会生成实例对象。
- 为了保证构造函数必须与 new 命令一起使用
- 构造函数内部使用严格模式,即第一行加上 use strict。这样的话,一旦忘了使用 new 命令,直接调用构造函数,由于严格模式中,函数内部的 this 不能指向全局对象,默认等于 undefined,导致不加 new 调用会报错(JavaScript 不允许对 undefined 添加属性)。
- 构造函数内部判断是否使用 new 命令,如果发现没有使用,则直接返回一个实例对象
new 命令的原理
- 使用 new 命令时,它后面的函数依次执行下面的步骤。
- 创建一个空对象,作为将要返回的对象实例。
- 将这个空对象的原型,指向构造函数的 prototype 属性,将这个空对象赋值给函数内部的 this 关键字。
- 开始执行构造函数内部的代码。
- return 对象
- 如果构造函数内部有 return 语句,而且 return 后面跟着一个对象,new 命令会返回 return 语句指定的对象;否则,就会不管 return 语句,返回 this 对象
- 如果对普通函数(内部没有 this 关键字的函数)使用 new 命令,则会返回一个空对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function _new (constructor, params) {
// 将 arguments 对象转为数组
var args = [].slice.call(arguments);
// 取出构造函数
var constructor = args.shift();
// 创建一个空对象,继承构造函数的 prototype 属性
var context = Object.create(constructor.prototype);
// 执行构造函数
var result = constructor.apply(context, args);
// 如果返回结果是对象,就直接返回,否则返回 context 对象
return (typeof result === 'object' && result != null) ? result : context;
}
// 实例
var actor = _new(Person, '张三', 28);new.target
- 函数内部可以使用 new.target 属性。如果当前函数是 new 命令调用,new.target 指向当前函数,否则为 undefined
- 使用这个属性,可以判断函数调用的时候,是否使用 new 命令。
- 使用 new 命令时,它后面的函数依次执行下面的步骤。
4. Object.create() 创建实例对象
- 以一个现有的对象作为模板,生成新的实例对象
绑定 this 的方法
- Function.prototype.call()
- call 方法的参数,应该是一个对象。如果参数为空、null 和 undefined,则默认传入全局对象
- 如果 call 方法的参数是一个原始值,那么这个原始值会自动转成对应的包装对象,然后传入 call 方法
- call 方法还可以接受多个参数,第一个参数就是 this 所要指向的那个对象,后面的参数则是函数调用时所需的参数
- Function.prototype.apply()
- apply 方法的作用与 call 方法类似,也是改变 this 指向,然后再调用该函数。唯一的区别就是,它接收一个数组作为函数执行时的参数
- Function.prototype.bind()
- bind 方法用于将函数体内的 this 绑定到某个对象,然后返回一个新函数
- bind 还可以接受更多的参数,将这些参数绑定原函数的参数。
- 如果 bind 方法的第一个参数是 null 或 undefined,等于将 this 绑定到全局对象,函数运行时 this 指向顶层对象(浏览器为 window)。
- bind 方法每运行一次,就返回一个新函数,这会产生一些问题
prototype 对象
1. 原型对象概述
- 构造函数的缺点
- 通过构造函数为实例对象定义属性,虽然很方便,但是同一个构造函数的多个实例之间,无法共享属性,从而造成对系统资源的浪费。
- prototype 属性的作用
- JavaScript 继承机制的设计思想就是,原型对象的所有属性和方法,都能被实例对象共享。也就是说,如果属性和方法定义在原型上,那么所有实例对象就能共享,不仅节省了内存,还体现了实例对象之间的联系
- JavaScript 规定,每个函数都有一个 prototype 属性,指向一个对象, 对于构造函数来说,生成实例的时候,该属性会自动成为实例对象的原型
- 原型对象的属性不是实例对象自身的属性。只要修改原型对象,变动就立刻会体现在所有实例对象上。
- 当实例对象本身没有某个属性或方法的时候,它会到原型对象去寻找该属性或方法。如果实例对象自身就有某个属性或方法,它就不会再去原型对象寻找这个属性或方法
- 原型对象的作用,就是定义所有实例对象共享的属性和方法。这也是它被称为原型对象的原因,而实例对象可以视作从原型对象衍生出来的子对象
- 原型链
- JavaScript 规定,所有对象都有自己的原型对象(prototype)。一方面,任何一个对象,都可以充当其他对象的原型;另一方面,由于原型对象也是对象,所以它也有自己的原型。因此,就会形成一个“原型链”(prototype chain):对象到原型,再到原型的原型……
- 如果一层层地上溯,所有对象的原型最终都可以上溯到 Object.prototype,即 Object 构造函数的 prototype 属性。也就是说,所有对象都继承了 Object.prototype 的属性。这就是所有对象都有 valueOf 和 toString 方法的原因,因为这是从 Object.prototype 继承的。
- Object.prototype 的原型是 null。null 没有任何属性和方法,也没有自己的原型。因此,原型链的尽头就是 null。
- 读取对象的某个属性时,JavaScript 引擎先寻找对象本身的属性,如果找不到,就到它的原型去找,如果还是找不到,就到原型的原型去找。如果直到最顶层的 Object.prototype 还是找不到,则返回 undefined。如果对象自身和它的原型,都定义了一个同名属性,那么优先读取对象自身的属性,这叫做“覆盖”(overriding)。
- constructor 属性
- prototype 对象有一个 constructor 属性,默认指向 prototype 对象所在的构造函数。
- constructor 属性的作用是,可以得知某个实例对象,到底是哪一个构造函数产生的
- constructor 属性表示原型对象与构造函数之间的关联关系,如果修改了原型对象,一般会同时修改 constructor 属性,防止引用的时候出错
2. instanceof 运算符
- instanceof 运算符返回一个布尔值,表示对象是否为某个构造函数的实例。
- instanceof 运算符的左边是实例对象,右边是构造函数。它会检查右边构建函数的原型对象(prototype),是否在左边对象的原型链上
- 由于 instanceof 检查整个原型链,因此同一个实例对象,可能会对多个构造函数都返回 true
- instanceof 运算符的一个用处,是判断对象的类型
- 利用 instanceof 运算符,还可以巧妙地解决,调用构造函数时,忘了加 new 命令的问题(无 new 调用)
1
2
3
4
5
6
7
8function Fubar (foo, bar) {
if (this instanceof Fubar) {
this._foo = foo;
this._bar = bar;
} else {
return new Fubar(foo, bar);
}
}
Object 对象的相关方法
- Object.getPrototypeOf()
- 返回参数对象的原型
- Object.setPrototypeOf()
- 为参数对象设置原型,返回该参数对象。它接受两个参数,第一个是现有对象,第二个是原型对象。
Object.create()
- 该方法接受一个对象作为参数,然后以它为原型,返回一个实例对象。该实例完全继承原型对象的属性。
1
2
3
4
5
6
7if (typeof Object.create !== 'function') {
Object.create = function (obj) {
function F() {}
F.prototype = obj;
return new F();
};
}如果想要生成一个不继承任何属性(比如没有toString和valueOf方法)的对象,可以将Object.create的参数设为null。
- 使用Object.create方法的时候,必须提供对象原型,即参数不能为空,或者不是对象,否则会报错。
- object.create方法生成的新对象,动态继承了原型。在原型上添加或修改任何方法,会立刻反映在新对象之上。
- Object.prototype.isPrototypeOf()
- 实例对象的isPrototypeOf方法,用来判断该对象是否为参数对象的原型。
- Object.prototype.proto
- 实例对象的proto属性(前后各两个下划线),返回该对象的原型。该属性可读写。
- 获取原型对象方法的比较
- obj.proto
- 只有浏览器才需要部署,其他环境可以不部署
- obj.constructor.prototype
- 在手动改变原型对象时,可能会失效, 一般要同时设置constructor属性
- Object.getPrototypeOf(obj)
- obj.proto
- Object.getOwnPropertyNames()
- 返回一个数组,成员是参数对象本身的所有属性的键名,不包含继承的属性键名, 不管键名是否可以遍历
- Object.prototype.hasOwnProperty()
- 对象实例的hasOwnProperty方法返回一个布尔值,用于判断某个属性定义在对象自身,还是定义在原型链上
- in 运算符和 for…in 循环
- in运算符返回一个布尔值,表示一个对象是否具有某个属性。它不区分该属性是对象自身的属性,还是继承的属性
- 获得对象的所有可遍历属性(不管是自身的还是继承的),可以使用for…in循环
- 为了在for…in循环中获得对象自身的属性,可以采用hasOwnProperty方法判断一下。
- 对象的拷贝
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function copyObject(orig) {
var copy = Object.create(Object.getPrototypeOf(orig));
copyOwnPropertiesFrom(copy, orig);
return copy;
}
function copyOwnPropertiesFrom(target, source) {
Object
.getOwnPropertyNames(source)
.forEach(function (propKey) {
var desc = Object.getOwnPropertyDescriptor(source, propKey);
Object.defineProperty(target, propKey, desc);
});
return target;
}