解决命名冲突

前提

在实际开发中,往往是由很多工程师共同完成一个项目。工程师各自负责不同的功能开发,最后整合代码到一起。

整合代码时,就有可能出现各种各样的冲突。比如 HTML 的冲突,CSS 的冲突,还有 JavaScript 功能的冲突。

其中,JavaScript 不同功能的代码块中各自定义的变量,由于整合到一起,所以会出现互相冲突或直接覆盖的问题。

为了避免变量命名冲突,出现了以下几种方法。

方法一:命名空间 Namespace

为项目或库创建一个全局对象,然后将所有功能添加到该全局变量中,减少程序中全局变量的数量,实现单全局变量。这样避免了在具有大量函数、对象和其他变量的情况下造成全局污染,也解决了命名冲突等问题。

定义一个全局对象,它在主域的 JavaScript 中。

1
var org = {}

全局对象里又包含多个对象。这些对象中存储每个功能的变量,这样就能区分开各功能模块的变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var org = {
department1: {
tuan: {
name: 'a'
},
dou: {
name: 'b'
}
},
department2: {
piao: {
name: 'c'
}
}
}

如果要使用这些变量:

1
2
3
var tuan = org.department1.tuan;		// 简化引用值名称

console.log(tuan.name); // a

这是个比较早期采用的方法,现在不再使用了。

方法二:闭包

运用闭包和立即执行函数。

把特定的功能写进去,然后留出一个接口,方便后续调用。

这个接口就是 return 出去的函数,这个函数里放入功能函数的调用。

功能函数会使用立即执行函数里的私有化变量。

return 的函数作为一个中转站存在。

好处:不污染全局变量,也不影响各自的功能实现。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var name = 'origin'; // 全局变量
var init = (function() {
var name = 'piao'; // 私有化变量,不污染全局变量
function callName() {
console.log(name);
}
function changeName(useName) {
name = useName;
console.log(name);
}
return function () { // 留出接口,调用上面特定的功能
callName(); // 功能函数调用
changeName('dou');
}
}());

init(); // piao dou

方法三:webpack

链式调用(模仿 jQuery)

jQuery 链式调用:

1
$('div').css('background-color', 'red').width(100).height(100).html(123).css('position', 'absolute').css('left', '100px').css('top', '100px');

对象内的方法调用:

  • 函数内没有return语句时,默认会返回undefined值。
  • tuan.eat()调用完成后,如果接着调用.drink(),就会报错:“没有在 undefined 上找到 drink 方法。”
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var tuan = {
eat : function () {
console.log('Fried chicken!');
// return undefined;
},
drink : function () {
console.log('Coke!');
},
sleep : function () {
console.log('Gnite~');
}
}

tuan.eat().drink(); // 报错,Cannot read property 'drink' of undefined
  • 在一个对象的函数(方法)里,this指向的是这个对象。
  • 所以,我们之间在方法(函数)里增加语句return this;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var tuan = {
eat : function () {
console.log('Fried chicken!')
return this;
},
drink : function () {
console.log('Coke!')
return this;
},
sleep : function () {
console.log('Gnite~')
return this;
}
}

tuan.eat().drink().sleep(); // Fried chicken! Coke! Gnite~

访问属性

成员操作符['属性名']或者.操作符可以访问到对象的属性,还可以给对象的属性赋值。

两者作用基本等同,每当你使用.操作符访问属性时,系统内部会自动转换成[]操作符。

但是以下情况需要使用[]

  • 属性名比较特殊,比如包含空格或者连字符。
  • 需要用变量做属性名。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var user = {
'user-name': 'cnl@outlook.com',
'nice name': 'Tuan',
password: 123456
};
var proto = 'nice name';

// 属性名特殊:

console.log(user['user-name']); // cnl@outlook.com

console.log(user.user-name); // 报错,name is not defined at Object

// 用变量做属性名:

console.log(user.proto); // undefined

console.log(user[proto]); // Tuan
  • 属性名拼接。因为[]操作符内填写的是字符串形式的属性名,所以可以通过+进行拼接。
1
2
3
4
5
6
7
8
9
10
11
12
13
var tuan = {
card1: {name: 'Visa'},
card2: {name: 'Master'},
card3: {name: 'American Express'},
card4: {name: 'Apple Card'},
useCard: function(num) {
return this['card' + num]; // 字符串拼接
}
}

console.log(tuan.useCard(1)); // { name: 'Visa' }
console.log(tuan.useCard(2)); // { name: 'Master' }
console.log(tuan.useCard(4)); // { name: 'Apple Card' }

对象枚举

对象枚举(enumeration),又叫遍历,就是将对象的每个属性(属性名+属性值)挨个访问一遍。
for循环语句可以遍历数组。

for…in 循环语句

对象的遍历则需要使用for...in循环语句。它的功能只有一个,就是遍历对象。

  • 「键名」可以是任意合法的标识符,它在循环的过程中会逐个被赋值为「对象」的属性名。
  • 对象[键名]为对象的属性值。
1
2
3
for (var 键名 in 对象) {
循环体
}

特点:通过对象属性的个数来控制循环圈数。

数组算特殊类型的对象,所以也可以使用for...in语句。

1
2
3
4
5
var arr = ['a', 'b', 'c'];

for(var prop in arr) {
console.log(arr[prop]); // a b c
}

输出属性名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var tuan = {
name : '团',
age : 3,
sex : 'female',
weight : 45
}

for (var key in tuan) {
console.log(typeof(key), key);
}

// 输出结果:
// string name
// string age
// string sex
// string weight

输出属性值:(注意[].操作符的差异)

  • tuan.key-> 系统内部自动转换成tuan['key'],导致的结果:系统是把 key 当做 tuan 的一个属性,因为它没有这个属性,所以会输出 undefined。
1
2
3
4
5
6
for (var key in tuan) {
console.log(tuan.key);
}

// 输出结果:
// undefined undefined undefined undefined
  • 所以需要使用[]操作符,这样tuan[key]中的key才是变量:
1
2
3
4
5
6
for (var key in tuan) {
console.log(tuan[key]);
}

// 输出结果:
// 团 3 female 45

for...in语句在遍历时,会将对象原型上的属性也访问一遍。不过,它不会遍历到原型链终端Object.prototype系统自带的属性。如果手动给它增加属性,是可以遍历到的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var tuan = {
name: '团',
age: 3,
sex: 'female',
weight: 45,
__proto__: {
lastName: '刘'
}
}

Object.prototype.height = 160;

for(var prop in tuan) {
console.log(prop + ': ' + tuan[prop]);
}

// 输出结果:
// name: 团
// age: 3
// sex: female
// weight: 45
// lastName: 刘
// height: 160

hasOwnProperty() 方法

任何对象都有hasOwnProperty()方法,它可以判断属性是否属于这个对象。括号内传入要判断的属性名(字符串形式)。如果是,它会返回布尔值true

1
2
3
4
5
6
7
8
9
10
11
12
var tuan = {
name : '团',
age : 3,
sex : 'female',
weight : 45,
__proto__ : {
lastName : '刘'
}
}

console.log(tuan.hasOwnProperty('name')); // true
console.log(tuan.hasOwnProperty('height')); // false

增加到for...in语句中,可以过滤掉原型上的属性,只留下自己的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var tuan = {
name: '团',
age: 3,
sex: 'female',
weight: 45,
__proto__: {
lastName: '刘'
}
}

for (var prop in tuan) {
if (tuan.hasOwnProperty(prop)){
console.log(prop + ': ' + tuan[prop]);
}
}

// 输出结果:
// name: 团
// age: 3
// sex: female
// weight: 45

判断是不是自己的属性,不包括原型链上的。

in 操作符

in 操作符可以判断这个对象能不能访问到这个属性 ,包括原型上的。操作符前面要用属性名(字符串形式)。

1
2
3
4
5
6
7
8
9
10
11
12
var tuan = {
name : '团',
age : 3,
sex : 'female',
weight : 45,
__proto__ : {
lastName : '刘'
}
}

console.log('name' in tuan); // true
console.log('height' in tuan); // false

但是 in 操作符有个问题,它不能判断这个属性是属于对象自己的,还是原型上的。

1
2
3
4
5
6
7
8
9
10
11
var tuan = {
name : '团',
age : 3,
sex : 'female',
weight : 45,
__proto__ : {
lastName : '刘'
}
}

console.log('lastName' in tuan); // true

判断某个属性能否访问到,包括原型链上的。

instanceof 操作符

格式:A instanceof B。判断对象 A 是不是构造函数 B 生产出来的。

判断方法:看对象 A 的原型链上有没有构造函数 B 的原型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 对象的原型链终端:Object.prototype

function Person() {

}

var person = new Person();

console.log(person instanceof Person); // true
console.log(person instanceof Object); // true
console.log(person instanceof Array); // false

// 对象字面量:
console.log({} instanceof Object); // true

判断某个对象是否是这个构造函数生产出来的。

区分数组和对象

如果有一个数据,需要用方法去判别它是对象还是数组。

1
var arr = [] | {};

构造器 constructor

如下:

1
2
3
console.log([].constructor); // [Function: Array]

console.log({}.constructor); // [Function: Object]

instanceof 操作符

如下:

1
2
3
console.log([] instanceof Array); // true

console.log({} instanceof Array); // false

toString() 方法

toString()是定义在Object.prototype上的实例方法,所有实例对象继承了该方法,其可以返回一个对象的字符串形式。

1
2
3
{}.toString(); // [object Object]

{a:1}.toString(); // [object Object]

对于空对象和一般对象,toString()方法会继承于Object.prototype.toString(),默认返回[object Object],其中第二个 Object 是该对象的构造函数,那么根据这个值可以判断数据类型。

然而,字符串、数组、Date 等对象拥有自定义的toString()方法,会覆盖Object.prototype.toString()方法。

1
2
3
4
5
'abc'.toString(); // "abc"

[1,2].toString(); // "1,2"

new Date().toString(); // "Sun Mar 24 2019 16:08:58 GMT+0800"

可以直接使用Object.prototype.toString()方法来获得类型,但由于是在 Object 对象环境中使用方法,所有对象都会显示[object Object]

配合call(),改变toString()方法执行时的所在的环境,可以得到对象的精确类型。

  • Object.prototype里的toString()方法,正常情况下,这个方法被谁调用,那么它里面的 this 就是谁。
  • toString()方法是为了处理前面的调用者,对象调用了它,那么这个 this 就是对象。它将前面的参数识别出来,然后返回字符串形式。
1
2
3
4
5
6
Object.prototype.toString = function () {
// 猜测这个方法中一定使用了 this
// 执行过程:
// 识别 this
// 返回相应的结果
}
  • 原来是谁调用,this 就是谁。现在直接用call()方法把 this 指向改掉。
1
2
3
console.log(Object.prototype.toString.call([])); // [object Array]

console.log(Object.prototype.toString.call({})); // [object Object]

this

函数预编译过程中,除了之前说过的形参、变量和函数声明,还包括argumentsthis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function test(target) {
var a = 123;
function b() {}
}

AO = {
arguments: [1],
this: window,
target: 1,
a: undefined,
b: function b() {}
}

test(1);

这里的this指向 window(即 GO,全局)。

1
2
3
4
5
function test() {
console.log(this);
}

test(); // Window {......}

全局作用域里的this也指向 window。

1
console.log(this); // Window {......}

call()apply()方法可以改变函数运行时this的指向。

在对象里,谁调用了方法,这个方法里面的 this 就是谁。

  • 对象的方法执行时同样会走预编译,预编译完成后,this 会根据调用者来改变指向。
  • 对象 tuan 调用了 test 方法,那么它里面的 this 就指向 tuan 这个对象。
1
2
3
4
5
6
7
8
var tuan = {
test: function() {
console.log(this.name); // 团
},
name: '团'
}

tuan.test();
  • 如果没人调用它,直接空执行,那么this就是指向 window 的。

注意,如果在对象方法里再执行一个函数,这个函数里面的 this 没人调用的话,那么它同样是指向 window 的。

  • 对象 b 调用 say 方法,里面的 this 指向 b
  • 把 a.say 的函数引用当作实际参数传入 b 的 say 方法中
  • 相当于fun = function () {console.log(this.name)};
  • 函数 fun 在 b 的 say 方法里执行,没有人调用它
  • 所以按照预编译,函数 fun 里的 this 指向 window
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var name = '2';

var a = {
name : '1',
say : function () {
console.log(this.name);
}
}

var b = {
name : '3',
say : function (fun) {
fun();
}
}

b.say(a.say); // 2

arguments

arguments是一个对应于传递给函数的参数的类数组对象。

属性有:arguments.calleearguments.length

arguments.callee指向当前执行的函数。

1
2
3
4
5
6
function test () {
console.log(arguments.callee); // [Function: test]
console.log(arguments.callee == test); // true
}

test();

用处:比如需要初始化工具时,立即执行函数没有函数名,而在函数内又需要用到它自身,就可以用来arguments.callee来代替。

1
2
3
4
5
6
7
8
var num = (function(n) {
if (n === 1) {
return 1;
}
return n * arguments.callee(n - 1);
}(4));

console.log(num); // 24

arguments.length指向传递给当前函数的参数数量。

function.caller返回调用函数。

  • 如果一个函数f是在全局作用域内被调用的,则f.callernull
1
2
3
4
5
function piao () {
console.log(piao.caller); // null
}

piao();
  • 如果一个函数是在另外一个函数作用域内被调用的,则f.caller指向调用它的那个函数。
1
2
3
4
5
6
7
8
9
// demo 在 test 函数作用域内被调用
function test () {
demo();
}
function demo () {
console.log(demo.caller); // [Function: test]
}

test();

克隆

浅层克隆

浅克隆的对象的引用值是==拷贝==对象里的引用,这两个对象里面的引用(如对象里的数组或者内嵌对象)指向的地方是一致的。

原始值是值拷贝,各自改各自的,没有影响。

例如:

  • 1# 处声明一个 target 变量,是为了保证用户在没有传入第二个变量时,程序依然可以正常运行。
  • 因为对象cat2的引用值是直接拷贝cat的引用,它俩指向同一个地址,所以一个改动,另一个也会改动。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var cat = {
name: '票',
age: 1,
card: ['Visa', 'Master']
}

var cat2 = {};

function clone(origin, target) {
var target = target || {}; // 1#
for (var prop in origin) {
target[prop] = origin[prop];
}
return target;
}

console.log(clone(cat, cat2)); // { name: '票', age: 1, card: [ 'Visa', 'Master' ] }

cat2.card.push('Apple Card');

console.log(cat2.card); // [ 'Visa', 'Master', 'Apple Card' ]
console.log(cat.card); // [ 'Visa', 'Master', 'Apple Card' ]

深层克隆

两个对象里的引用独立拷贝,不指向同一个地方。

步骤如下:

遍历每个属性。

  • for(var prop in obj){}
  • 遍历时需注意,for...in会将对象的原型链上的属性也拿出来,所以需要hasOwnProperty()方法验证该属性是否属于这个对象。

判断属性是不是原始值,原始值直接进行值拷贝。

  • typeof()
  • null的数据类型也被识别为 object,所以需要把它去掉。

如果是引用值,判断引用值是数组还是对象。

  • toString()instanceofconstructor

  • 考虑到父子域跨域访问的问题,建议使用toString()

建立相应的空引用值(数组或对象)。

  • {}[]

把要拷贝的引用值当作一个新的对象或数组,然后从第一步开始循环。

  • 递归

规律:判断引用值里的属性是原始值还是引用值……

  • 出口:当这个属性是原始值时else {target[prop] = origin[prop]}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
var tuan = {
name: '团',
age: 3,
sex: 'female',
card: ['Visa', 'Master', 'Apple Card'],
prefer: {
name: 'dou',
cat: {
name: 'piao'
}
}
}
var tuan2 = {};

function deepClone(origin, target) {
var target = target || {},
toStr = Object.prototype.toString,
arrStr = '[object Array]';
for (var prop in origin) {
if (origin.hasOwnProperty(prop)) {
if (origin[prop] !== 'null' && typeof(origin[prop]) == 'object') {
if (toStr.call(origin[prop]) == arrStr) {
target[prop] = [];
} else {
target[prop] = {};
}
deepClone(origin[prop], target[prop]);
} else {
target[prop] = origin[prop];
}
}
}
return target;
}

deepClone(tuan, tuan2);

tuan.card.push('American Express');

console.log(tuan.card); // [ 'Visa', 'Master', 'Apple Card', 'American Express' ]
console.log(tuan2.card); // [ 'Visa', 'Master', 'Apple Card' ]

tuan.prefer.cat.toy = 'ball';

console.log(tuan.prefer); // { name: 'dou', cat: { name: 'piao', toy: 'ball' } }
console.log(tuan2.prefer); // { name: 'dou', cat: { name: 'piao' } }

三目运算符

又叫条件运算符、三元运算符。它是 JavaScript 中唯一需要三个操作数的运算符。运算的结果根据给定条件在两个值中取其一。语法为:

1
条件 ? 值1 : 值2

如果条件为真,则结果取值1。否则为值2。你能够在任何允许使用标准运算符的地方使用条件运算符。

功能类似if...else语句。它还自带return返回值。

例:

  • 有括号先看括号
  • 字符串之间的比较是比较 asc 码,字符串’10’(一零)和字符串’9’逐位比较,'10' < '9'
1
2
3
var num = 4 > 3 ? ('10' > '9' ? 1 : 0) : 2;

console.log(num); // 0

可以使用三目运算符简化深层克隆里的部分步骤。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function deepClone(origin, target) {   
var target = target || {},
toStr = Object.prototype.toString,
arrStr = '[object Array]';
for(var prop in origin) {
if(origin.hasOwnProperty(prop)) {
if(origin[prop] !=='null' && typeof(origin[prop]) == 'object') {
target[prop] = (toStr.call(origin[prop]) == arrStr) ? [] : {};
deepClone(origin[prop], target[prop]);
}else{
target[prop] = origin[prop];
}
}
}
return target;
}

字符串 String

字符串原始值无法拥有属性和方法,但是字符串对象可以有。

length属性:返回字符串的长度。

indexOf('文本', 检索起始位置)方法:返回字符串中指定文本首次出现的位置(索引)。未找到会返回 -1。

lastIndexOf():返回指定文本最后一次出现的位置。未找到会返回 -1。注意:该方法向后进行检索(从尾到头),这意味着:假如第二个参数是 50,则从位置 50 开始检索,直到字符串的起点。

search():搜索特定值的字符串,并返回匹配的位置。

区别:

  • search() 方法无法设置第二个开始位置参数。
  • indexOf() 方法无法设置更强大的搜索值(正则表达式)。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
var str = 'hhh略略略啦啦啦xfsdsdfcdeewruiggvsfasdfghj';

console.log(str.length); // 36

console.log(str.indexOf('f')); // 10
console.log(str.indexOf('f', 11)); // 15
console.log(str.indexOf('z')); // -1

console.log(str.lastIndexOf('f')); // 32
console.log(str.lastIndexOf('f', 11)); // 10
console.log(str.lastIndexOf('z')); // -1

console.log(str.search('f')); // 10

slice(从该位开始截取,截取到该位之前):截取字符串的某部分并返回该部分。

补充

任何使用 var 声明的属性不能从全局作用域或函数的作用域中删除。

通过 var 操作给 window 上增加的属性,这种属性叫做不可配置的属性。不可配置的属性无法进行 delete 操作。

1
2
3
4
5
6
7
8
9
var num = 123;

console.log(delete num); // false
console.log(num); // 123

num1 = 123;

console.log(delete num1); // true
console.log(num1); // 报错, num1 is not defined

引用值没必要研究隐式类型转换,如果一定要转换也是可以的。

1
2
3
4
// 空数组不等于空数组,引用值地址不一样

console.log([] == []); // false
console.log([] === []); // false