进阶之对象继承这个容易忽略(二)
4、多重继承
JavaScript 不提供多重继承功能,即不允许一个对象同时继承多个对象。但是,可以通过变通方法,实现这个功能。
function M1() { // 构造函数M1
this.hello = 'hello';
}
function M2() { // 构造函数M2
this.world = 'world';
}
function S() { // 子类构造函数S
M1.call(this);
M2.call(this);
}
// 继承 M1
S.prototype = Object.create(M1.prototype);
// 继承链上加入 M2
Object.assign(S.prototype, M2.prototype);
// 指定构造函数
S.prototype.constructor = S;
var s = new S();
s.hello // 'hello'
s.world // 'world'
上面代码中,子类S
同时继承了父类M1
和M2
。这种模式又称为 Mixin(混入)。
5、模块
随着网站逐渐变成“互联网应用程序”,嵌入网页的 JavaScript 代码越来越庞大,越来越复杂。网页越来越像桌面程序,需要一个团队分工协作、进度管理、单元测试等等……开发者必须使用软件工程的方法,管理网页的业务逻辑。
JavaScript 模块化编程,已经成为一个迫切的需求。理想情况下,开发者只需要实现核心的业务逻辑,其他都可以加载别人已经写好的模块。
但是,JavaScript 不是一种模块化编程语言,ES6 才开始支持“类”和“模块”。下面介绍传统的做法,如何利用对象实现模块的效果。
(1)基本的实现方法
模块是实现特定功能的一组属性和方法的封装。
简单的做法是把模块写成一个对象,所有的模块成员都放到这个对象里面。
var module1 = new Object({
_count : 0,
m1 : function (){
//...
},
m2 : function (){
//...
}
});
上面的函数m1
和m2
,都封装在module1
对象里。使用的时候,就是调用这个对象的属性。
// 使用
module1.m1();
但是,这样的写法会暴露所有模块成员,内部状态可以被外部改写。比如,外部代码可以直接改变内部计数器的值。
module1._count = 5;
(2)封装私有变量
(2-1)构造函数的写法
我们可以利用构造函数,封装私有变量。
function StringBuilder() {
var buffer = []; // 模块的私有变量
this.add = function (str) {
buffer.push(str);
};
this.toString = function () {
return buffer.join('');
};
}
上面代码中,buffer
是模块的私有变量。一旦生成实例对象,外部是无法直接访问buffer
的。但是,这种方法将私有变量封装在构造函数中,导致构造函数与实例对象是一体的,总是存在于内存之中,无法在使用完成后清除。这意味着,构造函数有双重作用,既用来塑造实例对象,又用来保存实例对象的数据,违背了构造函数与实例对象在数据上相分离的原则(即实例对象的数据,不应该保存在实例对象以外)。同时,非常耗费内存。
function StringBuilder() {
this._buffer = [];
}
StringBuilder.prototype = {
constructor: StringBuilder,
add: function (str) {
this._buffer.push(str);
},
toString: function () {
return this._buffer.join('');
}
};
这种方法将私有变量放入实例对象中,好处是看上去更自然,但是它的私有变量可以从外部读写,不是很安全。
(2-2)立即执行函数的写法
另一种做法是使用“立即执行函数”(Immediately-Invoked Function Expression,IIFE),将相关的属性和方法封装在一个函数作用域里面,可以达到不暴露私有成员的目的。
var module1 = (function () {
var _count = 0;
var m1 = function () {
//...
};
var m2 = function () {
//...
};
return {
m1 : m1,
m2 : m2
};
})();
使用上面的写法,外部代码无法读取内部的_count
变量。
console.info(module1._count); //undefined
上面的module1
就是 JavaScript 模块的基本写法。下面,再对这种写法进行加工。
(3)模块的放大模式(向模块添加新方法)
如果一个模块很大,必须分成几个部分,或者一个模块需要继承另一个模块,这时就有必要采用“放大模式”(augmentation)。
var module1 = (function (mod){
mod.m3 = function () {
//...
};
return mod;
}) (module1);
上面的代码为module1
模块添加了一个新方法m3()
,然后返回新的module1
模块。
在浏览器环境中,模块的各个部分通常都是从网上获取的,有时无法知道哪个部分会先加载。如果采用上面的写法,第一个执行的部分有可能加载一个不存在空对象,这时就要采用"宽放大模式"(Loose augmentation)。
var module1 = (function (mod) {
//...
return mod;
})(window.module1 || {});
与"放大模式"相比,“宽放大模式”就是“立即执行函数”的参数可以是空对象。
(4)输入全局变量(保证独立性)
独立性是模块的重要特点,模块内部最好不与程序的其他部分直接交互。
为了在模块内部调用全局变量,必须显式地将其他变量输入模块。
var module1 = (function ($, YAHOO) {
//...
})(jQuery, YAHOO); // 向模块内部传入全局变量
上面的module1
模块需要使用 jQuery 库和 YUI 库,就把这两个库(其实是两个模块)当作参数输入module1
。这样做除了保证模块的独立性,还使得模块之间的依赖关系变得明显。
立即执行函数还可以起到命名空间的作用。
(function($, window, document) {
function go(num) {
}
function handleEvents() {
}
function initialize() {
}
function dieCarouselDie() {
}
//attach to the global scope
window.finalCarousel = { // 对外暴露接口
init : initialize,
destroy : dieCarouselDie
}
})( jQuery, window, document );
上面代码中,finalCarousel
对象输出到全局,对外暴露init
和destroy
接口,内部方法go
、handleEvents
、initialize
、dieCarouselDie
都是外部无法调用的。
四、Object 对象的相关方法
JavaScript 在Object
对象上面,提供了很多相关方法,处理面向对象编程的相关操作。本章介绍这些方法。
1、Object.getPrototypeOf() 获取原型对象
Object.getPrototypeOf
方法返回参数对象的原型。这是获取原型对象的标准方法。
var F = function () {};
var f = new F();
Object.getPrototypeOf(f) === F.prototype // true
上面代码中,实例对象f
的原型是F.prototype
。
下面是几种特殊对象的原型。
// 空对象的原型是 Object.prototype
Object.getPrototypeOf({}) === Object.prototype // true
// Object.prototype 的原型是 null
Object.getPrototypeOf(Object.prototype) === null // true
// 函数的原型是 Function.prototype
function f() {}
Object.getPrototypeOf(f) === Function.prototype // true
2、Object.setPrototypeOf() 设置原型对象
Object.setPrototypeOf
方法为参数对象设置原型,返回该参数对象。它接受两个参数,第一个是现有对象,第二个是原型对象。
var a = {};
var b = {x: 1};
Object.setPrototypeOf(a, b);
Object.getPrototypeOf(a) === b // true
a.x // 1 a对象共享b对象的属性
上面代码中,Object.setPrototypeOf
方法将对象a
的原型,设置为对象b
,因此a
可以共享b
的属性。
使用Object.setPrototypeOf
方法模拟new
命令
var F = function () {
this.foo = 'bar';
};
var f = new F();
// 等同于
var f = Object.setPrototypeOf({}, F.prototype); // 模拟new命令
F.call(f);
上面代码中,new
命令新建实例对象,其实可以分成两步。第一步,将一个空对象的原型设为构造函数的prototype
属性(上例是F.prototype
);第二步,将构造函数内部的this
绑定这个空对象,然后执行构造函数,使得定义在this
上面的方法和属性(上例是this.foo
),都转移到这个空对象上。
3、 Object.create() 创建实例对象,指向目标对象的原型
生成实例对象的常用方法是,使用new
命令让构造函数返回一个实例。但是很多时候,只能拿到一个实例对象,它可能根本不是由构建函数生成的,那么能不能从一个实例对象,生成另一个实例对象呢?
JavaScript 提供了Object.create
方法,用来满足这种需求。**该方法接受一个对象作为参数,然后以它为原型,返回一个实例对象。**该实例完全继承原型对象的属性。
// 原型对象
var A = {
print: function () {
console.log('hello');
}
};
// 实例对象
var B = Object.create(A); // 以A为原型,创建了B实例对象,使B继承了A的属性
Object.getPrototypeOf(B) === A // true
B.print() // hello
B.print === A.print // true
上面代码中,Object.create
方法以A
对象为原型,生成了B
对象。B
继承了A
的所有属性和方法。
实际上,Object.create
方法可以用下面的代码代替。
内部实现原理
if (typeof Object.create !== 'function') {
Object.create = function (obj) { // 模拟Object.create方法
function F() {} // 创建一个空构造函数F
F.prototype = obj; // 让F的原型 指向参数obj(obj为传入的原型对象)
return new F(); // 返回一个F的实例
};
}
上面代码表明,Object.create
方法的实质是新建一个空的构造函数F
,然后让F.prototype
属性指向参数对象obj
,最后返回一个F
的实例,从而实现让该实例继承obj
的属性。
下面三种方式生成的新对象是等价的。
var obj1 = Object.create({});
var obj2 = Object.create(Object.prototype);
var obj3 = new Object();
如果想要生成一个不继承任何属性(比如没有toString
和valueOf
方法)的对象,可以将Object.create
的参数设为null
。
var obj = Object.create(null); // 不继承Object的toString和valueOf方法的一个对象
obj.valueOf()
// TypeError: Object [object Object] has no method 'valueOf'
上面代码中,对象obj
的原型是null
,它就不具备一些定义在Object.prototype
对象上面的属性,比如valueOf
方法。
使用Object.create
方法的时候,必须提供对象原型,即参数不能为空,或者不是对象,否则会报错。
Object.create()
// TypeError: Object prototype may only be an Object or null
Object.create(123)
// TypeError: Object prototype may only be an Object or null
Object.create
方法生成的新对象,动态继承了原型。在原型上添加或修改任何方法,会立刻反映在新对象之上。
var obj1 = { p: 1 };
var obj2 = Object.create(obj1);
obj1.p = 2;
obj2.p // 2 obj2的原型指向obj1,当访问obj2上的p属性时,js引擎会先在obj2本身上找,没找到会去原型上找
上面代码中,修改对象原型obj1
会影响到实例对象obj2
。
除了对象的原型,Object.create
方法还可以接受第二个参数。该参数是一个属性描述对象,它所描述的对象属性,会添加到实例对象,作为该对象自身的属性。
var obj = Object.create({}, {
p1: { // p1为添加到obj实例对象自身的属性
value: 123,
enumerable: true,
configurable: true,
writable: true,
},
p2: {
value: 'abc',
enumerable: true,
configurable: true,
writable: true,
}
});
// 等同于
var obj = Object.create({});
obj.p1 = 123;
obj.p2 = 'abc';
Object.create
方法生成的对象,继承了它的原型对象的构造函数。
function A() {}
var a = new A();
var b = Object.create(a);
b.constructor === A // true
b instanceof A // true
上面代码中,b
对象的原型是a
对象,因此继承了a
对象的构造函数A
。
4、Object.prototype.isPrototypeOf()判断某个对象是否为参数对象的原型
实例对象的isPrototypeOf
方法,用来判断该对象是否为参数对象的原型。
var o1 = {};
var o2 = Object.create(o1);
var o3 = Object.create(o2);
o2.isPrototypeOf(o3) // true 判断o2是否为o3的原型
o1.isPrototypeOf(o3) // true 判断o1是否为o3的原型
上面代码中,o1
和o2
都是o3
的原型。这表明只要实例对象处在参数对象的原型链上,isPrototypeOf
方法都返回true
。
Object.prototype.isPrototypeOf({}) // true
Object.prototype.isPrototypeOf([]) // true
Object.prototype.isPrototypeOf(/xyz/) // true
Object.prototype.isPrototypeOf(Object.create(null)) // false
上面代码中,由于Object.prototype
处于原型链的最顶端,所以对各种实例都返回true
,只有直接继承自null
的对象除外。
5、Object.prototype.__proto__ 返回该对象的原型,可读写
实例对象的__proto__
属性(前后各两个下划线),返回该对象的原型。该属性可读写。
var obj = Object.create({x:1}) // 创建实例对象obj,其原型指定为{x:1}
obj.__proto__ // {x: 1} 实例对象obj的__proto__属性,返回obj的原型
Object.getPrototypeOf(obj) // {x: 1}
上面代码通过Object.create
创建实例对象obj,指定其原型为{x:1}
,访问obj对象的__proto__
属性,返回其原型。
var obj = {};
var p = {};
obj.__proto__ = p; // 原型属性可读写
Object.getPrototypeOf(obj) === p // true
obj.__proto__ === Object.getPrototypeOf(obj) //true
上面代码通过__proto__
属性,将p
对象设为obj
对象的原型。
根据语言标准,__proto__
属性只有浏览器才需要部署,其他环境可以没有这个属性。它前后的两根下划线,表明它本质是一个内部属性,不应该对使用者暴露。因此,应该尽量少用这个属性,而是用Object.getPrototypeOf()
和Object.setPrototypeOf()
,进行原型对象的读写操作。
原型链可以用__proto__
很直观地表示。
var A = {
name: '张三'
};
var B = {
name: '李四'
};
var proto = {
print: function () {
console.log(this.name);
}
};
A.__proto__ = proto; // 将A的原型指向proto对象
B.__proto__ = proto; // 将B的原型指向proto对象
// 共享print方法,都是在调用proto对象内的print方法
A.print() // 张三
B.print() // 李四
A.print === B.print // true
A.print === proto.print // true
B.print === proto.print // true
上面代码中,A
对象和B
对象的原型都是proto
对象,它们都共享proto
对象的print
方法。也就是说,A
和B
的print
方法,都是在调用proto
对象的print
方法。
6、获取原型对象方法的比较
如前所述,__proto__
属性指向当前对象的原型对象,即构造函数的prototype
属性。
var obj = new Object();
obj.__proto__ === Object.prototype
// true
obj.__proto__ === obj.constructor.prototype
// true
上面代码首先新建了一个对象obj
,它的__proto__
属性,指向构造函数(Object
或obj.constructor
)的prototype
属性。
因此,获取实例对象obj
的原型对象,有三种方法。
obj.__proto__
obj.constructor.prototype
Object.getPrototypeOf(obj)
上面三种方法之中,前两种都不是很可靠。__proto__
属性只有浏览器才需要部署,其他环境可以不部署。而obj.constructor.prototype
在手动改变原型对象时,可能会失效。
var P = function () {};
var p = new P();
var C = function () {};
C.prototype = p;
var c = new C();
c.constructor.prototype === p // false
上面代码中,构造函数C
的原型对象被改成了p
,但是实例对象的c.constructor.prototype
却没有指向p
。所以,在改变原型对象时,一般要同时设置constructor
属性。
C.prototype = p;
C.prototype.constructor = C; // 如在构造函数的继承中就使用到这个操作
var c = new C();
c.constructor.prototype === p // true
因此,推荐使用第三种Object.getPrototypeOf
方法,获取原型对象。
7、Object.getOwnPropertyNames()
Object.getOwnPropertyNames
方法返回一个数组,成员是参数对象本身的所有属性的键名,不包含继承的属性键名。
Object.getOwnPropertyNames(Date)
// ["parse", "arguments", "UTC", "caller", "name", "prototype", "now", "length"]
上面代码中,Object.getOwnPropertyNames
方法返回Date
所有自身的属性名。
对象本身的属性之中,有的是可以遍历的(enumerable),有的是不可以遍历的。Object.getOwnPropertyNames
方法返回所有键名,不管是否可以遍历。只获取那些可以遍历的属性,使用Object.keys
方法。
Object.keys(Date) // []
上面代码表明,Date
对象所有自身的属性,都是不可以遍历的。
8、Object.prototype.hasOwnProperty()
对象实例的hasOwnProperty
方法返回一个布尔值,用于判断某个属性定义在对象自身,还是定义在原型链上。
Date.hasOwnProperty('length') // true
Date.hasOwnProperty('toString') // false
上面代码表明,Date.length
(构造函数Date
可以接受多少个参数)是Date
自身的属性,Date.toString
是继承的属性。
另外,hasOwnProperty
方法是 JavaScript 之中唯一一个处理对象属性时,不会遍历原型链的方法。
9、in 运算符和 for...in 循环
in
运算符返回一个布尔值,表示一个对象是否具有某个属性。它不区分该属性是对象自身的属性,还是继承的属性。
'length' in Date // true
'toString' in Date // true
in
运算符常用于检查一个属性是否存在。
获得对象的所有可遍历属性(不管是自身的还是继承的),可以使用for...in
循环。
var o1 = { p1: 123 };
var o2 = Object.create(o1, { // o2的原型指向o1,并且在o2上定义一个属性p2
p2: { value: "abc", enumerable: true }
});
for (p in o2) {
console.info(p);
}
// p2
// p1 继承的属性
上面代码中,对象o2
的p2
属性是自身的,p1
属性是继承的。这两个属性都会被for...in
循环遍历。
为了在for...in
循环中获得对象自身的属性,可以采用hasOwnProperty
方法判断一下。
for ( var name in object ) {
if ( object.hasOwnProperty(name) ) { // 过滤掉非自身的属性
/* loop code */
}
}
获得对象的所有属性(不管是自身的还是继承的,也不管是否可枚举),可以使用下面的函数。
function inheritedPropertyNames(obj) {
var props = {};
while(obj) {
// 获取obj对象的所有属性,包括不可枚举的,
Object.getOwnPropertyNames(obj).forEach(function(p) {
props[p] = true;
});
obj = Object.getPrototypeOf(obj); // 获取对象的原型
}
console.log(props)
return Object.getOwnPropertyNames(props);
}
上面代码依次获取obj
对象的每一级原型对象“自身”的属性,从而获取obj
对象的“所有”属性,不管是否可遍历。
下面是一个例子,列出Date
对象的所有属性。
inheritedPropertyNames(Date)
// [
// "caller",
// "constructor",
// "toString",
// "UTC",
// ...
// ]
10、对象的拷贝
如果要拷贝一个对象,需要做到下面两件事情。
- 确保拷贝后的对象,与原对象具有同样的原型。
- 确保拷贝后的对象,与原对象具有同样的实例属性。
下面就是根据上面两点,实现的对象拷贝函数。
function 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);
// 定义属性,给target对象定义propKey属性,其属性描述对象是desc
Object.defineProperty(target, propKey, desc);
});
return target;
}
另一种更简单的写法,是利用 ES2017 才引入标准的Object.getOwnPropertyDescriptors
方法。
function copyObject(orig) {
return Object.create(
Object.getPrototypeOf(orig),
Object.getOwnPropertyDescriptors(orig)
);
}
五、严格模式
除了正常的运行模式,JavaScript 还有第二种运行模式:严格模式(strict mode)。顾名思义,这种模式采用更加严格的 JavaScript 语法。
同样的代码,在正常模式和严格模式中,可能会有不一样的运行结果。一些在正常模式下可以运行的语句,在严格模式下将不能运行。
1、设计目的
早期的 JavaScript 语言有很多设计不合理的地方,但是为了兼容以前的代码,又不能改变老的语法,只能不断添加新的语法,引导程序员使用新语法。
严格模式是从 ES5 进入标准的,主要目的有以下几个。
- 明确禁止一些不合理、不严谨的语法,减少 JavaScript 语言的一些怪异行为。
- 增加更多报错的场合,消除代码运行的一些不安全之处,保证代码运行的安全。
- 提高编译器效率,增加运行速度。
- 为未来新版本的 JavaScript 语法做好铺垫。
总之,严格模式体现了 JavaScript 更合理、更安全、更严谨的发展方向。
2、启用方法
'use strict';
- 点赞
- 收藏
- 关注作者
评论(0)