Javascript的原型链模式的设计,让它隐藏了一些有趣的功能.

This

this是在运行时进行绑定的.this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。

当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this就是记录的其中一个属性,会在函数执行的过程中用到。

全局环境

无论是否在严格模式下,在全局执行环境中(在任何函数体外部)this 都指向全局对象。

1
2
3
4
5
6
7
8
9
// 在浏览器中, window 对象同时也是全局对象:
console.log(this === window); // true

a = 37;
console.log(window.a); // 37

this.b = "MDN";
console.log(window.b) // "MDN"
console.log(b) // "MDN"

函数(运行内)环境

简单调用

因为下面的代码不在严格模式下,且 this 的值不是由该调用设置的,所以 this 的值默认指向全局对象。

1
2
3
4
5
6
7
8
function f1(){
return this;
}
//在浏览器中:
f1() === window; //在浏览器中,全局对象是window

//在Node中:
f1() === global;

然而,在严格模式下,this将保持他进入执行环境时的值,所以下面的this将会默认为undefined

1
2
3
4
5
6
function f2(){
"use strict"; // 这里是严格模式
return this;
}

f2() === undefined; // true

call&apply

如果要想把 this 的值从一个环境传到另一个,就要用 call 或者apply 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
// 将一个对象作为call和apply的第一个参数,this会被绑定到这个对象。
var obj = {a: 'Custom'};

// 这个属性是在global对象定义的。
var a = 'Global';

function whatsThis(arg) {
return this.a; // this的值取决于函数的调用方式
}

whatsThis(); // 'Global'
whatsThis.call(obj); // 'Custom'
whatsThis.apply(obj); // 'Custom'

bind

ECMAScript 5 引入了 Function.prototype.bind()。调用f.bind(someObject)会创建一个与f具有相同函数体和作用域的函数,但是在这个新函数中,this将永久地被绑定到了bind的第一个参数,无论这个函数是如何被调用的。

1
2
3
4
5
6
7
8
9
10
11
12
function f(){
return this.a;
}

var g = f.bind({a:"azerty"});
console.log(g()); // azerty

var h = g.bind({a:'yoo'}); // bind只生效一次!
console.log(h()); // azerty

var o = {a:37, f:f, g:g, h:h};
console.log(o.a, o.f(), o.g(), o.h()); // 37, 37, azerty, azerty

箭头函数

箭头函数中,this与封闭词法环境的this保持一致。在全局代码中,它将被设置为全局对象:

1
2
3
var globalObject = this;
var foo = (() => this);
console.log(foo() === globalObject); // true

作为对象的方法

当函数作为对象里的方法被调用时,它们的 this 是调用该函数的对象。

下面的例子中,当 o.f()被调用时,函数内的this将绑定到o对象。

1
2
3
4
5
6
7
8
var o = {
prop: 37,
f: function() {
return this.prop;
}
};

console.log(o.f()); // 37

作为构造函数

当一个函数用作构造函数时(使用new关键字),它的this被绑定到正在构造的新对象。

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
/*
* 构造函数这样工作:
*
* function MyConstructor(){
* // 函数实体写在这里
* // 根据需要在this上创建属性,然后赋值给它们,比如:
* this.fum = "nom";
* // 等等...
*
* // 如果函数具有返回对象的return语句,
* // 则该对象将是 new 表达式的结果。
* // 否则,表达式的结果是当前绑定到 this 的对象。
* //(即通常看到的常见情况)。
* }
*/

function C(){
this.a = 37;
}

var o = new C();
console.log(o.a); // logs 37


function C2(){
this.a = 37;
return {a:38};
}

o = new C2();
console.log(o.a); // logs 38

闭包

闭包是基于词法作用域书写代码时所产生的自然结果.

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。

1
2
3
4
5
6
7
8
9
10
function foo(){
var a=2;
function bar(){
console.log(a);
}
return bar;
}
var baz=foo();
baz();//2————朋友,这就是闭包的效果。

函数bar可以访问函数foo的作用域.然后我们将bar()函数本身当作一个值类型进行传递。bar()所声明的位置所赐,它拥有涵盖foo()内部作用域的闭包,使得该作用域能够一直存活,以供bar()在之后任何时间进行引用。

bar()依然持有对该作用域的引用,而这个引用就叫作闭包。

String

String(..)

String函数可以将任意类型的值转化成字符串,转换规则如下。

  • 数值:转为相应的字符串。
  • 字符串:转换后还是原来的值。
  • 布尔值true转为字符串"true"false转为字符串"false"
  • undefined:转为字符串"undefined"
  • null:转为字符串"null"
1
2
3
4
5
String(123) // "123"
String('abc') // "abc"
String(true) // "true"
String(undefined) // "undefined"
String(null) // "null"

String方法的参数如果是对象,返回一个类型字符串;如果是数组,返回该数组的字符串形式。

1
2
String({a: 1}) // "[object Object]"
String([1, 2, 3]) // "1,2,3"

String方法背后的转换规则,与Number方法基本相同,只是互换了valueOf方法和toString方法的执行顺序。

  1. 先调用对象自身的toString方法。如果返回原始类型的值,则对该值使用String函数,不再进行以下步骤。
  2. 如果toString方法返回的是对象,再调用原对象的valueOf方法。如果valueOf方法返回原始类型的值,则对该值使用String函数,不再进行以下步骤。
  3. 如果valueOf方法返回的是对象,就报错。
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
47
String({
toString: function () {
return 3;
}
})
// "3"

String({
valueOf: function () {
return 2;
}
})
// "[object Object]"

String({
valueOf: function () {
return 2;
},
toString: function () {
return 3;
}
})
// "3"

var obj = {
valueOf: function () {
return 2;
},
toString: function () {
return {};
}
};

String(obj)
// '2'

var obj = {
valueOf: function () {
return {};
},
toString: function () {
return {};
}
};

String(obj)
// TypeError: Cannot convert object to primitive value

Number

Number(..)

使用Number函数,可以将任意类型的值转化成数值。

下面分成两种情况讨论,一种是参数是原始类型的值,另一种是参数是对象。

原始类型值的转换规则如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 数值:转换后还是原来的值
Number(324) // 324

// 字符串:如果可以被解析为数值,则转换为相应的数值
Number('324') // 324

// 字符串:如果不可以被解析为数值,返回 NaN
Number('324abc') // NaN

// 空字符串转为0
Number('') // 0

// 布尔值:true 转成 1,false 转成 0
Number(true) // 1
Number(false) // 0

// undefined:转成 NaN
Number(undefined) // NaN

// null:转成0
Number(null) // 0

Number函数将字符串转为数值,要比parseInt函数严格很多。基本上,只要有一个字符无法转成数值,整个字符串就会被转为NaN

1
2
parseInt('42 cats') // 42
Number('42 cats') // NaN

上面代码中,parseInt逐个解析字符,而Number函数整体转换字符串的类型。

另外,parseIntNumber函数都会自动过滤一个字符串前导和后缀的空格。

简单的规则是,Number方法的参数是对象时,将返回NaN,除非是包含单个数值的数组。

1
2
3
Number({a: 1}) // NaN
Number([1, 2, 3]) // NaN
Number([5]) // 5

Number处理对象执行顺序:

  1. 调用对象自身的valueOf方法。如果返回原始类型的值,则直接对该值使用Number函数,不再进行后续步骤。

  2. 如果valueOf方法返回的还是对象,则改为调用对象自身的toString方法。如果toString方法返回原始类型的值,则对该值使用Number函数,不再进行后续步骤。

  3. 如果toString方法返回的是对象,就报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Number({
valueOf: function () {
return 2;
}
})
// 2

Number({
toString: function () {
return 3;
}
})
// 3

Number({
valueOf: function () {
return 2;
},
toString: function () {
return 3;
}
})
// 2

上面代码对三个对象使用Number函数。第一个对象返回valueOf方法的值,第二个对象返回toString方法的值,第三个对象表示valueOf方法先于toString方法执行。

Number.prototype.toFixed()

toFixed方法先将一个数转为指定位数的小数,然后返回这个小数对应的字符串。在处理数值计算精度不对时经常用到.

1
2
(10).toFixed(2) // "10.00"
10.005.toFixed(2) // "10.01"

toFixed方法的参数为小数位数,有效范围为0到20,超出这个范围将抛出 RangeError 错误。

Number.prototype.toExponential()

toExponential方法用于将一个数转为科学计数法形式。

1
2
3
4
5
6
7
(10).toExponential()  // "1e+1"
(10).toExponential(1) // "1.0e+1"
(10).toExponential(2) // "1.00e+1"

(1234).toExponential() // "1.234e+3"
(1234).toExponential(1) // "1.2e+3"
(1234).toExponential(2) // "1.23e+3"

toExponential方法的参数是小数点后有效数字的位数,范围为0到20,超出这个范围,会抛出一个 RangeError 错误。

Number.prototype.toPrecision()

toPrecision方法用于将一个数转为指定位数的有效数字。

1
2
3
4
5
(12.34).toPrecision(1) // "1e+1"
(12.34).toPrecision(2) // "12"
(12.34).toPrecision(3) // "12.3"
(12.34).toPrecision(4) // "12.34"
(12.34).toPrecision(5) // "12.340"

toPrecision方法的参数为有效数字的位数,范围是1到21,超出这个范围会抛出 RangeError 错误。

Number.prototype

所有的数字的原型链都指向Number.protytype,可以在 Number.prototype上写入一些常用的计算相关的方法,方便程序快速调用.

1
2
3
4
5
6
7
8
9
Number.prototype.add = function (x) {
return this + x;
};
Number.prototype.subtract = function (x) {
return this - x;
};

(8).add(2).subtract(4)
// 6

Array

@@iterator

for..of循环首先会向被访问对象请求一个迭代器(@@iterator)对象,然后通过调用迭代对象的next()方法来遍历所有返回值.

因为数值有内置的迭代器@@iterator,因此for..of可以直接应用在数组上.也可以手动调用迭代器遍历数组.

1
2
3
4
5
6
var myArray=[1,2,3];
var it=myArray[Symbol.iterator](); //
it.next();//{value:1,done:false}
it.next();//{value:2,done:false}
it.next();//{value:3,done:false}
it.next();//{done:true}

我们使用ES6中的符号Symbol.iterator来获取对象的@@iterator内部属性。

普通的对象没有内置的@@iterator,无法直接用for..of遍历.但我们可以手动给一个对象定义@@iterator.

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
var myObject={a:2,b:3};
Object.defineProperty(
myObject,
Symbol.iterator,
{
enumerable:false,
writable:false,
configurable:true,
value:function(){
var o=this;var idx=0;var ks=Object.keys(o);
return{
next:function(){
return{
value:o[ks[idx++]],
done:(idx>ks.length)};
}
};
}
});
//手动遍历myObject
var it=myObject[Symbol.iterator();
it.next();//{value:2,done:false}
it.next();//{value:3,done:false}
it.next();//{value:undefined,done:true}
//用for..of遍历myObjectfor
(var v of myObject){
console.log(v);
}
//2
//3

Object

浅拷贝

ES6中定义了Object.assign(..)方法实现浅拷贝. 第一个参数是目标对象,之后还可以跟一个或多个源对象.

它会遍历一个或多个源对象的所有可枚举(enumerable,参见下面的代码)的自有键(ownedkey,很快会介绍)并把它们复制(使用=操作符赋值)到目标对象,最后返回目标对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function anotherFunction(){/*..*/}
var anotherObject={c:true};
var anotherArray=[];
var myObject={
a:2,
b:anotherObject,//引用,不是复本!
c:anotherArray,//另一个引用!
d:anotherFunction
};


var new Obj= Object.assign({},myObject);
newObj.a; //2
newObj.b ===anotherObject;//true
newObj.c===anotherArray;//true
newObj.d===anotherFunction;//true

属性描述

ES5提供了Object.defineProperty(..)对对象的属性进行描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Object.defineProperty(myObject,"a",{value:2,
writable:true, // 可写
configurable:true, // 可配置
enumerable:true // 可枚举
});
myObject.a;//2

Object.getOwnPropertyDescriptor(myObject,"a"); // 获取对象中某个属性的描述信息
// {
//value:2,
//writable:true,
//enumerable:true,
//configurable:true
//}

⚠️在configurable:false情况下,我们可以将writable状态从true改为false.但不能从false改为true.

configurable:false还不会禁止删除该熟悉.

禁止扩展对象

Object.preventExtensions(..)可以禁止对象进行扩展.

1
2
3
4
5
6
7
var myObject={a:2};

Object.preventExtensions(myObject)

myObject.b = 3;
myObject.b; // undefined

Object.seal(..)

Object.seal(..)会创建一个“密封”的对象,这个方法实际上会在一个现有对象上调用Object.preventExtensions(..)并把所有现有属性标记为configurable:false。所以,密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性(虽然可以修改属性的值)。

Object.freeze(..)

Object.freeze(..)会创建一个冻结对象,这个方法实际上会在一个现有对象上调用Object.seal(..)并把所有“数据访问”属性标记为writable:false,这样就无法修改它们的值。

这个方法是你可以应用在对象上的级别最高的不可变性,它会禁止对于对象本身及其任意直接属性的修改(不过就像我们之前说过的,这个对象引用的其他对象是不受影响的)。

设置setter和getter操作符

1
2
3
4
5
6
7
8
9
10
11
12
13
var myObject = {
// 给 a属性定义 getter
get a () {
return this.__a__;
}.

// 给 a 定义一个 setter
set a(val) {
this.__a__ = val*2
}
}
myObject.a = 2;
myObject.a;// 4

inhasOwnProperty区别

in操作符会检查属性是否在对象及其[[Prototype]]原型链中(参见第5章)。相比之下,hasOwnProperty(..)只会检查属性是否在myObject对象中,不会检查[[Prototype]]链。

1
2
3
4
5
6
7
8
var otherObject={b:1};
var myObject={a:2};
Object.setPrototypeOf(myObject, otherObject); //
("a"in myObject);//true
("b"in myObject);//true
myObject.hasOwnProperty("a");//true
myObject.hasOwnProperty("b");//false

object.keys(..)object.getOwnPropertyNames(..)

Object.keys(..)会返回一个数组,包含所有可枚举属性,Object.getOwnPropertyNames(..)会返回一个数组,包含所有属性,无论它们是否可枚举。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var myObject={};
Object.defineProperty(
myObject,
"a",//让a像普通属性一样可以枚举
{enumerable:true,value:2}
);
Object.defineProperty(
myObject,
"b",//让b不可枚举
{enumerable:false,value:3}
);
myObject.propertyIsEnumerable("a");//true
myObject.propertyIsEnumerable("b");//false

Object.keys(myObject);//["a"]
Object.getOwnPropertyNames(myObject);//["a","b"]

修改对象的原型链方式

有两种较安全修改一个对象原型链的方式:Object.create(proto,[propertiesObject])Object.setPrototypeOf(obj,prototype)

1
2
3
4
5
6
7
// Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
Bar.prototype=Object.create(Foo.prototype);

//ES6开始可以直接修改现有的
// Object.setPrototypeOf方法设置一个指定的对象的原型 ( 即, 内部[[Prototype]]属性)到另一个对象或 null。
Object.setPrototypeOf(Bar.prototype,Foo.prototype);

Object.prototype.valueOf()

valueOf是对象默认拥有的方法,作用是返回一个对象的“值”,默认情况下返回对象本身。

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

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

1
2
3
4
5
6
7
8
9
var obj = new Object();
1 + obj // "1[object Object]"

var obj = new Object();
obj.valueOf = function () {
return 2;
};

1 + obj // 3

Object.prototype.toString()

toString是对象默认拥有的方法,它的作用是对象的字符串形式.

1
2
3
4
5
6
7
8
9
10
11
12
13
var o1 = new Object();
o1.toString() // "[object Object]"

var o2 = {a:1};
o2.toString() // "[object Object]"

var obj = new Object();

obj.toString = function () {
return 'hello';
};

obj + ' ' + 'world' // "hello world"

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

1
2
3
4
5
6
7
8
9
10
11
12
13
[1, 2, 3].toString() // "1,2,3"

'123'.toString() // "123"

(function () {
return 123;
}).toString()
// "function () {
// return 123;
// }"

(new Date()).toString()
// "Tue May 10 2016 09:11:31 GMT+0800 (CST)"

上面代码中,数组、字符串、函数、Date 对象调用toString方法,并不会返回[object Object],因为它们都自定义了toString方法,覆盖原始方法。

所有的js对象都有toString方法,并且调用它获得字符串值会不一样,可以它来作为对象类型的判断.

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
Object.prototype.toString.call(2) // "[object Number]"
Object.prototype.toString.call('') // "[object String]"
Object.prototype.toString.call(true) // "[object Boolean]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(Math) // "[object Math]"
Object.prototype.toString.call({}) // "[object Object]"
Object.prototype.toString.call([]) // "[object Array]"

var type = function (o){
var s = Object.prototype.toString.call(o);
return s.match(/\[object (.*?)\]/)[1].toLowerCase();
};

['Null',
'Undefined',
'Object',
'Array',
'String',
'Number',
'Boolean',
'Function',
'RegExp'
].forEach(function (t) {
type['is' + t] = function (o) {
return type(o) === t.toLowerCase();
};
});

type.isObject({}) // true
type.isNumber(NaN) // true
type.isRegExp(/abc/) // true

Function

判断函数是否是构造函数执行

function函数在JS可以作为普通的函数执行,也可以作为面向对象中的构造函数(构造器模式)执行。可以在函数体内通过new.target方式判断当前执行的类型

1
2
3
4
5
6
7
function fn() {
console.log(new.target)
}
// 不带 "new":
fn(); // undefined

new fn(); // function fn(){....}