面向对象编程(下)

104 阅读19分钟

六、Object对象中的相关方法

Object对象的原生方法分成两类:Object本身的方法Object实例方法

所谓本身的方法就是直接定义在当前构造函数对象Object对象上的

//例子
Object.xxx()	

Object实例方法就是定义在Object.prototype的方法。它可以被Object实例直接共享

Object.prototype.hello = function(){
    console.log('hello');
}
var obj = new Object();
obj.hello();//hello

1、Object的静态方法

所谓静态方法,是指定制在Object对象本身的方法

以下两个方法的作用都是用来遍历对象的属性

1、Object.keys()

Object.keys()方法的参数是一个对象,返回一个数组。该数组的成员都是对象自身的所有属性名

var arr = ['a','b','c'];
Object.keys(arr);//['0','1','2'];
var obj = {
    0:'a',
    1'b',
    2: 'c'
}
Object.keys(obj);//['0', '1', '2']

2、Object.getOwnPropertyNames()

Object.getOwnPropertyNames方法与Object.keys类似,也是接受一个对象作为参数,返回一个数组,包含了该对象自身的所有属性名。

var obj = {
    0:'a',
    1'b',
    2: 'c'
}
Object.getOwnPropertyNames(obj);// ["0", "1", "2"]

对于一般的对象来说,Object.keys()Object.getOwnPropertyNames()返回的结果是一样的。只有涉及不可枚举属性时,才会有不一样的结果。Object.keys方法只返回可枚举的属性,Object.getOwnPropertyNames方法还返回不可枚举的属性名。

var arr = ['a','b','c'];
Object.getOwnPropertyNames(arr);//['0','1','2','length'];

上面代码中,数组的length属性是不可枚举的属性,所以只出现在Object.getOwnPropertyNames方法的返回结果中。

由于 JavaScript 没有提供计算对象属性个数的方法,所以可以用这两个方法代替。

var obj = {
    0:'a',
    1'b',
    2: 'c'
}
Object.keys(obj).length //3
Object.getOwnPropertyNames(obj).length //3

一般情况下,使用Object.keys方法遍历对象的属性应用最多。

3、Object.getPrototypeOf()

Object.getPrototypeOf方法返回参数对象的原型。这是获取原型对象的标准方法

介绍几种特殊的对象原型:

       //参数是该对象,返回该对象的原型,也是获取原型对象的标准方法
        function Fn(){};
        var f1 = new Fn();
        console.log(Object.getPrototypeOf(f1) === Fn.prototype);//true
        //空对象的原型是Object.prototypeOf
        console.log(Object.getPrototypeOf({}) === Object.prototype);//true
        //Object.prototype的原型是null
        console.log(Object.getPrototypeOf(Object.prototype) === null);//true

         //函数的原型是Function.prototype
         function f(){};
         console.log(Object.getPrototypeOf(f) === Function.prototype);//true */

4、Object.setPrototypeOf()

Object.setPrototypeOf方法接收两个参数,第一个是现有对象,第二个是原型对象

var a = {};
var b = {x : 1};
Object.setPrototypeOf(a,b);
console.log(Object.getPrototypeOf(a));//{x:1}
a.x //1

new命令可以使用Object.setPrototypeOf方法模拟。

var F = function () {
  this.foo = 'bar';
};
//var f = new F();
// 等同于
var f = Object.setPrototypeOf({}, F.prototype);
F.call(f);

5、Object.create()

生成实例对象的常用方法是,使用new命令让构造函数返回一个实例。但是很多时候,只能拿到一个实例对象,它可能根本不是由构建函数生成的,那么能不能从一个实例对象,生成另一个实例对象呢?

JavaScript 提供了Object.create方法,用来满足这种需求。该方法接受一个对象作为参数,然后以它为原型,返回一个实例对象。该实例完全继承原型对象的属性。

// 原型对象
var A = {
  print: function () {
    console.log('hello');
  }
};
// 实例对象
var B = Object.create(A);
Object.getPrototypeOf(B) === A // true
B.print() // hello
B.print === A.print // true

上面代码中,Object.create方法以A对象为原型,生成了B对象。B继承了A的所有属性和方法。

2、其它方法

除了上面提到的两个方法,Object还有不少的其它静态方法,后文咱们会一一详细讲解

(1)对象属性模型的相关方法

  • Object.getOwnPropertyDescriptor():获取某个属性的描述对象。

  • Object.defineProperty():通过描述对象,定义某个属性。

  • Object.defineProperties():通过描述对象,定义多个属性。

(2)控制对象状态的方法(了解部分,自行查阅MDN)

  • Object.preventExtensions():防止对象扩展。

  • Object.isExtensible():判断对象是否可扩展。

  • Object.seal():禁止对象配置。

  • Object.isSealed():判断一个对象是否可配置。

  • Object.freeze():冻结一个对象。

  • Object.isFrozen():判断一个对象是否被冻结。

七、object的实例方法

  • Object.prototype.valueOf():返回当前对象对应的值。

  • Object.prototype.toString():返回当前对象对应的字符串形式。

  • Object.prototype.toLocaleString():返回当前对象对应的本地字符串形式。

  • Object.prototype.hasOwnProperty():判断某个属性是否为当前对象自身的属性,还是继承自原型对象的属性。

  • Object.prototype.isPrototypeOf():判断当前对象是否为另一个对象的原型。

  • Object.prototype.propertyIsEnumerable():判断某个属性是否可枚举。

1、Object.prototype.valueOf()

方法的作用是返回一个对象的,默认情况下返回对象本身

var obj = new Object();
obj.valueOf() === obj;//true

valueof方法的主要用途是,JavaScript自动类型转换时会默认调用这个方法

var obj = new Object();
//JavaScript就会默认调用valueOf()方法,求出obj的值再与1相加
console.log(1+obj);//"1[object Object]"

所以,如果自定义valueOf方法,就可以得到想要的结果

var obj = new Object();
obj.valueOf = function(){    
	return 2;
}
console.log(1 + obj);//3

原理:用自定义的Object.valueOf,覆盖Object.prototype.valueOf

2、Object.prototype.toString()

toString方法的作用是返回一个对象的字符串形式,默认返回类型字符串

var obj1 = new Object();
console.log(obj1.toString());//"[object Object]"
var obj2 = {a:1};
obj2.toString() // "[object Object]"

返回的这个结果说明了对象的类型。

字符串[object Object]本身没有太大的用处,但是通过自定义toString方法,可以让对象在自动类型转换时,得到想要的字符串形式。

var obj = new Object();
obj.toString = function(){    
    return 'hello';
}
console.log(obj + '' + 'world');//"hello world"

像数组、字符串、函数、Date对象都分别定义了自定义的toString方法,覆盖了Object.prototype.toString()方法

console.log([1, 2, 3].toString());  // "1,2,3"
console.log('123'.toString()); // "123"
console.log((function(){
    return 2;
}).toString());
//function(){
//    return 2;
//}
console.log((new Date).toString());//Tue Oct 06 2020 16:10:38 GMT+0800 (中国标准时间)

3、Object.prototype.toLocaleString()方法跟toString方法用法一致。

目前,主要有三个对象自定义了toLocaleString方法

  • Array.prototype.toLocaleString()

  • Number.prototype.toLocaleString()

  • Date.prototype.toLocaleString()

举例来说,日期的实例对象的toStringtoLocaleString返回值就不一样,而且toLocaleString的返回值跟用户设定的所在地域相关

var date = new Date();
console.log(date.toString());//Tue Oct 06 2020 16:10:38 GMT+0800 (中国标准时间)
console.log(date.toLocaleString());//2020/10/6 下午4:11:57

4、Object.prototype.hasOwnProperty()

判断某个属性是否为当前对象自身的属性,还是继承自原型对象的属性。

Object.prototype.hasOwnProperty方法接受一个字符串作为参数,返回一个布尔值,表示该实例对象自身是否具有该属性。

var obj = {
    a: 123
}
obj.hasOwnProperty('b');//false
obj.hasOwnProperty('a');//true
obj.hasOwnProperty('toString');//false

上面代码中,对象obj自身具有a属性,所以返回truetoString属性是继承的,所以返回false

5、Object.prototype.isPrototypeOf()

判断当前对象是否为另一个对象的原型。

        //Object.prototype.isPrototypeOf():判断当前对象是否为另一个对象的原型。
         //验证改对象是否为另一个对象的原型
            var o1 = {};
            var o2 = Object.create(o1);
            var o3 = Object.create(o2);
            console.log(o2.isPrototypeOf(o3));//true
            console.log(o1.isPrototypeOf(o3));//true
            console.log(o3);//{}
            console.log(Object.prototype.isPrototypeOf({}));//true
            console.log(Object.prototype.isPrototypeOf([]));//true
            console.log(Object.prototype.isPrototypeOf(null));//false

上面代码中,由于Object.prototype处于原型链的最顶端,所以对各种实例都返回true,只有直接继承自null的对象除外。

Object.prototype.proto

实例对象的__proto__属性(前后各两个下划线),返回该对象的原型。该属性可读写。

var obj = {};
var p = {};
obj.__proto__ = p;
Object.getPrototypeOf(obj) === p // true

上面代码通过__proto__属性,将p对象设为obj对象的原型。

根据语言标准,__proto__属性只有浏览器才需要部署,其他环境可以没有这个属性。它前后的两根下划线,表明它本质是一个内部属性,不应该对使用者暴露。因此,应该尽量少用这个属性,而是用Object.getPrototypeOf()Object.setPrototypeOf(),进行原型对象的读写操作。

6、Object.prototype.propertyIsEnumerable()

实例对象的propertyIsEnumerable()方法返回一个布尔值,用来判断某个属性是否可遍历。注意,这个方法只能用于判断对象自身的属性,对于继承的属性一律返回false

判断属性的可枚举性(可枚举:可以便利的属性),属性可以被便利返回true

var obj = {};
obj.p = 123;
obj.propertyIsEnumerable('p') // true
obj.propertyIsEnumerable('toString') // false

八、属性描述对象

JavaScript提供了一个内部数据结构,用来描述对象的属性,控制它的行为,比如该属性是否可写、可遍历等等。这个内部数据结构称之为”属性描述对象”。每个属性都有自己对应的属性描述对象,保存该属性的一些元信息

下面是属性描述对象的一个例子

{
  value: 123,
  writable: false,
  enumerable: true,
  configurable: false,
  get: undefined,
  set: undefined
}

属性描述对象提供了6个元属性

属性含义
value`value`是该属性的属性值,默认为`undefined`。
writable`writable`是一个布尔值,表示属性值(value)是否可改变(即是否可写),默认为`true`。
enumerable`enumerable`是一个布尔值,表示该属性是否可遍历,默认为`true`。如果设为`false`,会使得某些操作(比如`for...in`循环、`Object.keys()`)跳过该属性。
configurable`configurable`是一个布尔值,表示可配置性,默认为`true`。如果设为`false`,将阻止某些操作改写该属性,比如无法删除该属性,也不得改变该属性的属性描述对象(`value`属性除外)。也就是说,`configurable`属性控制了属性描述对象的可写性。
get`get`是一个函数,表示该属性的取值函数(getter),默认为`undefined`。
set`set`是一个函数,表示该属性的存值函数(setter),默认为`undefined`。

1、Object.getOwnPropertyDescriptor()

Object.getOwnPropertyDescriptor()方法可以获取属性描述对象。它的第一个参数是目标对象,第二个参数是一个字符串,对应目标对象的某个属性名。

var obj = {name:'MJJ'};
Object.getOwnPropertyDescriptor(obj,'name');
/*
{
configurable: true
enumerable: true
value: "MJJ"
writable: true
}
*/
//toString为继承来的属性,无法获取
Object.getOwnPropertyDescriptor(obj,'toString');//undefined
注意:Object.getOwnPropertyDescriptor()方法只能用于对象自身的属性,不能用于继承的属性

2、Object.defineProperty()

Object.defineProperty方法允许通过属性描述对象,定义或修改一个属性,然后返回修改后的对象。

语法如下:

Object.defineProperty(object, propertyName, attributesObject)

Object.defineProperty方法接受三个参数,依次如下。

  • object:属性所在的对象

  • propertyName:字符串,表示属性名

  • attributesObject:属性描述对象

举例说明,定义obj.name可以写成下面这样

var obj = Object.defineProperty({},'name',{
    value:'mjj',
    writable:false,//定义可写性,false为不可写的
    enumerable:true,//定义可枚举性为可枚举,true是可以被遍历
    configurable:false//定义是否能删除,false为不可删除
})
console.log(obj.name);//mjj
obj.name = 'alex';
console.log(obj.name);//mjj

上面代码中,Object.defineProperty()方法定义了obj.p属性。由于属性描述对象的writable属性为false,所以obj.p属性不可写。注意,这里的Object.defineProperty方法的第一个参数是{}(一个新建的空对象),p属性直接定义在这个空对象上面,然后返回这个对象,这是Object.defineProperty()的常见用法。

如果属性已经存在,Object.defineProperty()方法相当于更新该属性的属性描述对象。

3、Object.defineProperties()

如果一次性定义或修改多个属性,可以使用Object.defineProperties()方法

var obj = Object.defineProperties({}, {
  p1: { value: 123, enumerable: true },
  p2: { value: 'abc', enumerable: true },
  p3: { 
    get: function () { 
        return this.p1 + this.p2 
    },
    enumerable:true,
    configurable:true
  }
});
console.log(obj.p1);//123
console.log(obj.p2);//"abc"
console.log(obj.p3);//"123abc"

上面代码中,Object.defineProperties()同时定义了obj对象的三个属性。其中,p3属性定义了取值函数get,即每次读取该属性,都会调用这个取值函数。

注意,一旦定义了取值函数get(或存值函数set),就不能将writable属性设为true,或者同时定义value属性,否则会报错

var obj = {};
Object.defineProperty(obj, 'p', {
  value: 123,
  get: function() { return 456; }
});
// TypeError: Invalid property.
// A property cannot both have accessors and be writable or have a value
Object.defineProperty(obj, 'p', {
  writable: true,
  get: function() { return 456; }
});
// TypeError: Invalid property descriptor.
// Cannot both specify accessors and a value or writable attribute

上面代码中,同时定义了get属性和value属性,以及将writable属性设为true,就会报错。

Object.defineProperty()Object.defineProperties()参数里面的属性描述对象,writableconfigurableenumerable这三个属性的默认值都为false

var obj = {};
Object.defineProperty(obj, 'foo', {});
Object.getOwnPropertyDescriptor(obj, 'foo')
/*
{
configurable: false,
enumerable: false,
value: undefined,
writable: false,
}
*/

上面代码中,定义obj.foo时用了一个空的属性描述对象,就可以看到各个元属性的默认值。

4、元属性

value

value属性是目标属性的值

var obj = {};
obj.p = 123;
Object.getOwnPropertyDescriptor(obj, 'p').value //123
Object.defineProperty(obj, 'p', { value: 246 });
obj.p // 246

上面代码是通过value属性,读取或改写obj.p的例子。

writable

writable属性是一个布尔值,决定了目标属性的值(value)是否可以被改变。

var obj = {};
Object.defineProperty(obj, 'a', {
  value: 37,
  writable: false
});
obj.a // 37
obj.a = 25;
obj.a // 37

上面代码中,obj.awritable属性是false。然后,改变obj.a的值,不会有任何效果。

注意,正常模式下,对writablefalse的属性赋值不会报错,只会默默失败。但是,严格模式下会报错,即使对a属性重新赋予一个同样的值。

'use strict';
var obj = {};
Object.defineProperty(obj, 'a', {
  value: 37,
  writable: false
});
obj.a = 37;
// Uncaught TypeError: Cannot assign to read only property 'a' of object

上面代码是严格模式,对obj.a任何赋值行为都会报错。

如果原型对象的某个属性的writablefalse,那么子对象将无法自定义这个属性。

var proto = Object.defineProperty({}, 'foo', {
  value: 'a',
  writable: false
});
var obj = Object.create(proto);
obj.foo = 'b';
obj.foo; // 'a'

上面代码中,proto是原型对象,它的foo属性不可写。obj对象继承proto,也不可以再自定义这个属性了。如果是严格模式,这样做还会抛出一个错误。

但是,有一个规避方法,就是通过覆盖属性描述对象,绕过这个限制。原因是这种情况下,原型链会被完全忽视。

var proto = Object.defineProperty({}, 'foo', {
  value: 'a',
  writable: false
});
var obj = Object.create(proto);
Object.defineProperty(obj, 'foo', {
  value: 'b'
});
obj.foo // "b"
enumerable

enumerable(可遍历性)返回一个布尔值,表示目标属性是否可遍历。

如果一个属性的enumerable为false,下面三个操作不会取到该属性

  • for...in循环

  • Object.key方法

  • JSON.stringify方法

因此,enumerable可以用来设置“秘密”属性。

var obj = {};
Object.defineProperty(obj, 'x', {
  value: 123,
  enumerable: false
});
obj.x // 123
for (var key in obj) {
  console.log(key);
}
// undefined
Object.keys(obj)  // []
JSON.stringify(obj) // "{}"

上面代码中,obj.x属性的enumerablefalse,所以一般的遍历操作都无法获取该属性,使得它有点像“秘密”属性,但不是真正的私有属性,还是可以直接获取它的值。

注意,for...in循环包括继承的属性Object.keys方法不包括继承的属性如果需要获取对象自身的所有属性,不管是否可遍历,可以使用Object.getOwnPropertyNames方法。

另外,JSON.stringify方法会排除enumerablefalse的属性,有时可以利用这一点。如果对象的 JSON 格式输出要排除某些属性,就可以把这些属性的enumerable设为false

configurable

configurable(可配置性)返回一个布尔值,决定了是否可以修改属性描述对象。也就是说,

1、configurablefalse时,valuewritableenumerableconfigurable都不能被修改了。

var obj = Object.defineProperty({}, 'p', {
  value: 1,
  writable: false,
  enumerable: false,
  configurable: false
});
Object.defineProperty(obj, 'p', {value: 2})
// TypeError: Cannot redefine property: p
Object.defineProperty(obj, 'p', {writable: true})
// TypeError: Cannot redefine property: p
Object.defineProperty(obj, 'p', {enumerable: true})
// TypeError: Cannot redefine property: p
Object.defineProperty(obj, 'p', {configurable: true})
// TypeError: Cannot redefine property: p

上面代码中,obj.pconfigurablefalse。然后,改动valuewritableenumerableconfigurable,结果都报错。

2、注意,writable只有在false改为true会报错,true改为false是允许的。

var obj = Object.defineProperty({}, 'p', {
  writable: true,
  configurable: false
});
Object.defineProperty(obj, 'p', {writable: false})
// 修改成功

3、至于value,只要writableconfigurable有一个为true,就允许改动。

var o1 = Object.defineProperty({}, 'p', {
  value: 1,
  writable: true,
  configurable: false
});
Object.defineProperty(o1, 'p', {value: 2})
// 修改成功
var o2 = Object.defineProperty({}, 'p', {
  value: 1,
  writable: false,
  configurable: true
});
Object.defineProperty(o2, 'p', {value: 2})
// 修改成功

3、另外,writablefalse时,直接目标属性赋值,不报错,但不会成功。

var obj = Object.defineProperty({}, 'p', {
  value: 1,
  writable: false,
  configurable: false
});
obj.p = 2;
obj.p // 1var obj = Object.defineProperty({}, 'p', {  value: 1,  writable: false,  configurable: false});obj.p = 2;obj.p // 1

上面代码中,obj.pwritablefalse,对obj.p直接赋值不会生效。如果是严格模式,还会报错。

4、可配置性决定了目标属性是否可以被删除(delete)。

var obj = Object.defineProperties({}, {
  p1: { value: 1, configurable: true },
  p2: { value: 2, configurable: false }
});
delete obj.p1 // true
delete obj.p2 // false
obj.p1 // undefined
obj.p2 // 2

5、上面代码中,obj.p1configurabletrue,所以可以被删除,obj.p2就无法删除

九、存取器

除了直接定义以外,属性还可以用存取器定义。其中,存值函数称为setter,使用属性描述对象的set属性;取值函数称为getter,使用属性描述对象的get属性

一旦对目标属性定义了存取器,那么存取的时候,都将执行对应的函数。利用这个功能,可以实现许多高级特性,比如某个属性禁止赋值

var obj = Object.defineProperty({},'p',{
    get:function(){
        return 'getter';
    },
    set:function(value){
        console.log('setter:'+value);
    }
})
obj.p //"getter"
obj.p = 123;//"setter:123"

上面代码中,obj.p定义了getset属性。obj.p取值时,就会调用get;赋值时,就会调用set

JavaScript还提供了存取器的另一种写法。

var obj = {
    get p(){
        return 'getter';
    },
    set p(value){
        console.log('setter:'+ value);
    }
}

上面的写法与定义属性描述对象是等价的,而且使用更广泛。

注意,取值函数get不能接受参数,存值函数set只能接受一个参数(即属性的值)。

存取器往往用于,属性的值依赖对象内部数据的场合

var obj = {
    $n : 5,
    get next(){
        return this.$n++;
    },
    set next(value){
        if(value >= this.$n){
            this.$n = value;
        }else{
            throw new Error('新的值必须大于当前值');
        }
    }
};
obj.next //5
obj.next = 10;
obj.next //10
obj.next = 5;
// Uncaught Error: 新的值必须大于当前值

上面代码中,next属性的存值函数和取值函数,都必须依赖内部属性

十、深浅拷贝

1、基本类型的拷贝

先开看一段非常经典的代码

var a = 1;
var b = a;
a = 200;
console.log(a);//200
console.log(b);//1

基本类型是按值传递,引用类型按引用传递,数值作为基本类型是保存在栈内存中,可以直接拿来用的,赋值时什么就是什么,不会受到传递元素的改变带来影响。

2、引用类型的拷贝

简单说,引用类型是生成一个指针保存在堆内存中,当给引用类型赋值时,我们的写的是在栈内存中,也就是说我们拿到的其实是一个指针。这个指针是指向了栈内存中这个引用类型的代码。

提到拷贝涉及到的两种拷贝类型:深拷贝浅拷贝

//操作拷贝之后的数据不会影响到原数据的值拷贝,就是深拷贝,反正,有影响则为浅拷贝

浅拷贝

var a = {
    name:'mjj',
    age:20,
    hobby:'eat',
    friend:{
        name:'alex',
        age:38
    }
}
function shadowCopy(to,from){
    for(var key in from){
        to[key] = from[key]
    }
    return to;
}
var newObj = shadowCopy({},a);
newObj.age = 18;
newObj.friend.name = '阿黄';
/*
{
    age: 20,//没被改变
    friend: {
        name: "阿黄",//同时被改变,说明是同一个引用
        age: 38
    },
    hobby: "eat"
    name: "mjj
}
*/

我们发现,首先,浅拷贝不是直接赋值,浅拷贝新建了一个对象,然后将源对象的属性都一一复制过来,复制的是值,而不是引用。

我们知道,对象都是按地址引用进行访问的,浅拷贝的复制只复制了第一层的属性,并没有递归将所有的值复制过来,所以,操作拷贝数据,对原数据产生了影响,故而为浅拷贝。

深拷贝

深拷贝就是对目标的完全拷贝,不像浅拷贝那样只是复制了一层引用,就连值也都复制了。

只要进行了深拷贝,它们老死不相往来,谁也不会影响谁。

使用深拷贝可以使新创建的对象和原来的完全脱离关系

 var a = {
            name: 'mjj',
            age: 20,
            hobby: ['eat','paly'],
            friend: {
                name: 'alex',
                age: 38,
                friend:{
                    name:'阿黄',
                    age:20,
                    friend:{
                        name:'小胡',
                        age:10
                    }
                }
            }
        }

        function deepCopy(to,from){
            //遍历from里面的所有属性,拷贝到to对象中
            for(var key in from){
                //不遍历原型链上的属性
                if(from.hasOwnProperty(key)){
                    //如果值是对象并且有值,再遍历对象
                    if(from[key] && typeof from[key] === 'object' ){
                        //在这一步还要考虑一下上面friend这个属性的值是不是数组
                        to[key] = from[key].constructor === Array? []:{};
                        to[key] = deepCopy(to[key],from[key]);
                    }else{
                        //如果不是,直接复制
                        to[key] = from[key];
                    }
                }
            }
            return to;
        }

        var newObj = deepCopy({},a);
        newObj.friend.name = '小红';
        newObj.age = 20;
        console.log(newObj);
        console.log(a);

十一、模块化

1、基本的实现方法(字面量方式)

模块是实现特地功能的一组属性和方法的封装

简答的做法就是把模块写成一个对象,所有的模块成员都放到这个对象里面。

var module1 = new Object({
    count:0,
    m1:function(){
        console.log('m1');
    },
    m2:function(){
        console.log('m1');
    }
})

上面的函数m1m2,都封装在module1对象里。使用的时候,就是调用这个对象的属性。

module1.m1();//"m1"

但是,这样的写法会暴露所有模块成员,内部状态可以被外部改写。比如,外部代码可以直接改变内部计数器的值。

module1.count = 10;

2、封装私有变量:构造函数的写法

我们可以利用构造函数,封装私有变量。

function StringFun(){
    var arrStrings = [];
    this.add = function(str){
        arrStrings.push(str);
    };
    this.toString = function(){
        return arrStrings.join('');
    }
}
var str = new StringFun();

上面代码中,arrStrings是模块的私有变量。一旦生成实例对象,外部是无法直接访问arrStrings的。但是,这种方法将私有变量封装在构造函数中,导致构造函数与实例对象是一体的,总是存在于内存之中,无法在使用完成后清除。非常耗费内存

function StringFun() {
  this.arrStrings = [];
}
StringFun.prototype = {
  constructor: StringFun,
  add: function (str) {
    this.arrStrings.push(str);
  },
  toString: function () {
    return this.arrStrings.join('');
  }
};

这种方法将私有变量放入实例对象中,好处是看上去更自然,但是它的私有变量可以从外部读写,不是很安全。

3、封装私有变量:立即执行函数的写法

另一种做法是使用“立即执行函数”(Immediately-Invoked Function Expression,IIFE),将相关的属性和方法封装在一个函数作用域里面,可以达到不暴露私有成员的目的。

var module1 = (function(){
    var count = 0;
    var m1 =function(){
        console.log('m1');
    };
    var m2 = funcyion(){
        console.log('m2')
    };
    return{
        m1:m1,
        m2:m2
    }
})()

console.log(module1.m1);//m1
console.log(module1.m2);//m2
console.log(module1.count);//undefined

使用上面的写法,外部代码无法读取内部的count变量。

console.info(module1.count); //undefined

上面的module1就是 JavaScript 模块的基本写法。下面,再对这种写法进行加工。

4、模块放大模式

如果一个模块很大,必须分成几个部分,或者一个模块需要继承另一个模块,这时就有必要采用“放大模式”(augmentation)。

var module1 = (function (mod){
 mod.m3 = function () {
  //...
 };
 return mod;
})(module1);

上面的代码为module1模块添加了一个新方法m3(),然后返回新的module1模块。

在浏览器环境中,模块的各个部分通常都是从网上获取的,有时无法知道哪个部分会先加载。如果采用上面的写法,第一个执行的部分有可能加载一个不存在空对象,这时就要采用”宽放大模式”(Loose augmentation)。

var module1 = (function (mod) {
 //...
 return mod;
})(window.module1 || {});

与”放大模式”相比,“宽放大模式”就是“立即执行函数”的参数可以是空对象。

5、输入全局变量

独立性是模块的重要特点,模块内部最高不与程序的其它部分直接交互

为了在模块内部调用全局变量,必须显式地将其他变量输入模块。

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对象输出到全局,对外暴露initdestroy接口,内部方法gohandleEventsinitializedieCarouselDie都是外部无法调用的。