第1章 关于this
this
关键字是Javascript中最复杂的机制之一。到现在我也是对this一知半解,完全没有弄清楚。
为什么要用this
this 示例
1 | function identify(){ |
简单说明
每个函数都包含两个非继承而来的方法:call()方法和apply()方法。
call()的第一个参数是作为函数上下文的对象,之后的参数可以是任意形式。
this 提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将API设计得更加简洁并且易于复用。
误解
太拘泥于“ this ”的字面意思就会产生一些误解。有两种常见的对于this的解释,但是他们都是错误的。
- 指向自身(错误理解)
如下代码和我们想象的不一样了难道foo.count不应该是4么?1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function foo( num ){
console.log( "foo: " + num );
this.count++;
}
foo.count = 0;
var i;
for( i=0; i<10; i++ ){
if( i > 5 ){
foo(i);
}
}
console.log( foo.count ); //0
//6 7 8 9
如何修改才能叫this指向foo.count呢1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18function foo( num ){
console.log( "foo: " + num );
//记录foo被调用的次数
//注意,在当前的调用方式下,this确实指向foo
this.count++;
}
foo.count = 0;
var i;
for( i=0; i<10; i++ ){
if( i > 5 ){
//使用call(),可以确保this指向对象foo本身
foo.call(foo, i);
}
}
console.log( foo.count ); //4
//6 7 8 9 - 它的作用域(错误理解)
第二种常见的错误理解,this指向函数的作用域。它不完全正确
这里需要明确一个概念是,this在任何情况下都不指向函数的词法作用域,在Javascript内部,作用域确实和对象类似,可见的标识符都是他的属性,但是作用域“对象”无法通过Javascript代码访问,它存在于Javascript引擎内部。
错误例1
2
3
4
5
6
7
8function foo(){
var a = 2;
this.bar(); //首先这里就是错误的,调用bar不需要this
}
function bar(){
console.log( this.a ); //链接foo想要不执行就读取foo中的a是不可能实现的。
}
bar(); - this 到底是什么
this
是在运行时进行绑定的,并不是在编写时绑定。它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
当一个函数被调用时,会创建一个活动记录。这个记录会包含函数在那里被调用,函数的调用方法,传入的参数等信息。this就是记录的其中一个属性,会在函数执行的过程中用到。
第2章 this全面解析
我们了解到每个函数的this是在调用时被绑定的,完全取决于函数的调用方法。
调用位置
调用位置,说白了就是函数在代码中被调用的位置(不是声明位置),只有了解了这个概念,才可以知道this到底引用的是什么。
绑定规则
默认绑定
默认绑定一般出现在独立函数调用,如下例
1 | function foo(){ |
通常情况下不要在代码中混合使用strict mode 和 non-strict mode
隐式绑定
另一条需要考虑的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含。
1 | function foo(){ |
通过这段代码我们基本上就可以了解到了。根据函数调用的位置不同,this绑定也是不同的。
还有,对像属性引用链中只有最顶层或最后一层会影响调用位置。
1 | function foo(){ |
隐式丢失
一个最常见的this绑定问题就是被绑定的函数会丢失绑定的对象,也就是说它会应用默认绑定,从而把this绑定到全局对象或者undefined上。
1 | function foo(){ |
显式绑定
通过Javascript提供的特殊函数call()及apply()方法,他们的第一个参数是一个对象,他们会把这个对象绑定到this,接着在调用函数时指定这个this,因为你可以直接指定this的绑定对象,因此我们称之为显示绑定
关于丢失绑定的解决办法
硬绑定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16function foo(){
console.log( this.a );
}
var obj = {
a: 2,
foo: foo
}
var bar = function(){
foo.call( obj ); //在这里已经硬绑定了this,再怎么也无法修改了
} //函数别名
var a = "oops, global"; //a是全局对象的属性
bar(); //2
setTimeout(bar(), 100); //2
bar.call(window) //2这种情况,无论以后怎么调用函数bar,它总会手动在obj上调用foo,这种绑定是一种显式的强制绑定被称之为硬绑定
示例1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function foo(something){
console.log( this.a, something );
return this.a + something;
}
var obj = {
a: 2
}
var bar = function(){
return foo.apply(obj, arguments); //arguments对象是所有(非箭头)函数中都可用的局部变量。
};
var b = bar( 3 ); //2 3
console.log( b ); //5硬绑定的典型应用场景就是创建一个包裹函数,传入所有的参数并返回接收到的所有值
另一种使用方法是创建一个i可以重复使用的辅助函数1
2
3
4
5
6
7
8
9
10
11
12function foo(something){
console.log( this.a, something );
return this.a + something;
}
var obj = {
a: 2
}
var bar = foo.bind( obj );
var b = bar( 3 ); //2 3API调用的“上下文”
第三方库及Javascript语言许多的新的内置函数都提供了可选的参数1
2
3
4
5
6
7
8
9
10function foo(el){
console.log(el, this.id);
}
var obj = {
id: "awesome"
};
//调用foo()时,把this绑定到obj
[1,2,3].forEach(foo, obj); //"awesome""awesome""awesome"new绑定
Javascript的new和其他语言的new有所区别,new在Javascript中只是一个普通的函数,用于创建新的对象,并将这个新对象绑定到函数调用的this上。
优先级
了解了this绑定的四条规则,如果某个调用可以应用多条规则,我们就要了解他们的优先级。
默认绑定 是四条规则中最低级别
隐式绑定 和 显式绑定 的优先级别,我们通过以下示例来进行判断
1 | function foo(){ |
可以看出显式绑定优先级更高
接下来我们比较一下new绑定和隐式绑定的优先级别
1 | function foo(something){ |
明显new绑定级别要高于隐式绑定
(实际上new和call,apply无法共同使用,所以无法直接判断优先级)
不过通过.bind我们可以判断new绑定和显式绑定的优先级
1 | function foo(something){ |
判断this
this绑定可以按照以下的顺序进行判断
① 函数是否存在new调用?如果是的话,this绑定的是新创建的对象
② 函数是否通过call | apply或是硬绑定调用?如果是的话,this绑定的是指定对象
③ 函数是否在某个上下文中调用?如果是的话,this绑定的是那个上下文对象
④ 如果都不是,则绑定全局对象,严格模式下就绑定到undefined
绑定例外
在特殊情况下this的绑定也有例外
被忽略的this
如果你把null或是undefined 作为绑定对象传入到 call, apply或者bind,这些值会在调用时被忽略。
1 | function foo(){ |
但是这种直接使用null,会给程序带来许多意想不到的bug,所以可以使用一下的安全this
1 | function foo( a, b ){ |
间接引用
创建一个函数的间接引用时,调用这个函数会应用默认的绑定规则
1 | function foo(){ |
p.foo = o.foo的返回值是对目标函数的引用,因此调用位置为foo()。
软绑定
由于硬绑定大大的降低了程序的灵活性,可以通过创建一个方法来实现软绑定
1 | if(!Function.prototype.softBind){ |
(太深奥,以至于此处完全不懂,之后返工)
第3章 对象
对象是什么?
语法
- 声明形式(文字形式)
1
var myObj = {key: value, ....};
- 构造形式 区别,声明形式,可以一次添加多个键值对,构造形式,需要一个一个添加。
1
2var myObj = new Object();
myObj.key = value;
(一般都会使用声明形式来创建对象)
类型
Javascript包含6种类型
string, number, boolean, null, undefined, object
函数,数组都属于对象
内置对象
String, Number, Boolean, Object, Function, Array, Date, RegExp, Error
内容
对象的内容是由一些存储在特定命名位置的值组成的,我们称之为属性。
访问对象里的值需要使用 .
或[]
操作符
1 | var myObject = { |
在对象中,属性名永远是字符串,即使是数字也会转换成字符串
可计算属性名
可计算属性名最常用的场景在ES6中,可以在文字形式中使用[]
包裹一个表达式作为属性名
1 | var prefix = "foo"; |
属性与方法
在Javascript中从技术角度来说,函数永远不“属于”一个对象,无论返回的值是什么类型,每次访问对象的属性就是属性访问。
如下
1 | function foo(){ |
someFoo和myObject.someFoo是对同一个函数的不同引用。并不能说函数属于哪一个对象。
数组
数组最好使用[]
来访问,最好只用对象来存储键/值对,数组来存储数值下标/值对。
复制对象
- 浅复制 ES6提供了一个方法 Object.assign({目标对象}, myObject);
- 深复制 ??
属性描述符
ES5之后,提供了一种新的方法Object.getOwnPropertyDescriptor()
1 | var myObject = { |
除了得到属性值外,还获得了对象的3个特性
- writable 是否可以修改
- configurable 是否可以配置,如果设置成false,除了不能修改其属性外,配置也不能更改。(Object.defineProperty({},”a”,{}))
- enumerable 是否可以枚举
不变性
如果你希望属性或是对象是不可变的,按照以下提供的方法可以做到浅不变形
- 对象常量,结合writable: false和 configurable: false。
1
2
3
4
5
6var myObject = {};
Object.defineProperty( myObject, "FAVORITE_NUMBER", {
value: 42,
writable: false,
configurable: false
} ); - 禁止扩展
1
2
3
4
5
6
7var myObject = {
a: 2
};
Object.preventExtensions( myObject );
myObject.b = 3;
myObject.b; //undefined - 密封 Object.seal()会创建一个“密封”的对象,其实就是Object.preventExtenssions() 和 configurable: false的结合
- 冻结 Object.freeze() 会创建一个冻结对象,其实就是Object.seal() + writable: false
[[Get]]
[[Get]]操作首先在对象中查找是否有名称相同的属性,如果找到就返回这个属性的值。没有则返回undefined (抽象理解即可,一般用不上)
[[Put]]
同[[Get]]一样大概了解就行,作用于给对象属性赋值的时候发生此操作。
Getter和Setter
对象默认的[[Put]]和[[Get]]操作分别可以控制属性值的设置和获取。
在ES5中可以使用getter和setter部分改写默认操作,只能应用在单个属性上,无法应用在整个对象上。
当你给一个属性定义getter,setter或者两者都有时,这个属性会被定义为“访问描述符”。对于访问描述符来说,Javascript会忽略它们的value和writable特性,取而代之的是set和get(configurable和enumerable)特性。
1 | var myObject = { |
由于我们只定义了a的getter,所以对a的值进行设置时,set操作会忽略赋值操作,不会抛出错误,即使有合法的setter,由于我们自定义的getter只会返回2,所以set操作是没有任何意义的。
另外一个例子
为了让属性更合理,我们设置一个setter,setter会覆盖单个属性默认的[[Put]]操作。
1 | var myObject = { |
本例中,实际上我们把赋值[[Put]]操作中的值2存储到了另一个变量a中。名称a值是一种惯例,没有任何特殊的行为。
存在性
属性值存储的值是undefined,同属性不存在而返回的undefined,如果区分这两种情况?
1 | var myObject = { |
遍历
数组的遍历我们可以使用for..in
对于数值索引的数组来说,可以使用标准的for循环遍历值
1 | var myArray = [1,3,4]; |
ES5中也提供了很多的方法 forEach(), every(), some(),但是他们的共同特点就是遍历数组下标。
ES6提供了一个新的额遍历数组方法 for..of
1 | var myArray = [1,3,4]; |
第4章 类
———————————————-以下内容为坑—————————————————
原因ES6为止Javascript还未正式启动对类的正式支持,非要强求只会带来更多的隐患。
类理论
类/继承描述了一种代码的组织结构形式——一种在软件中对真实世界中问题领域的建模方法
“类”设计模式
类其实就是一种设计模式,如同设计图,模子工厂。
JavaScript中的“类”
其实Javascript并没有提供类的功能,我们只能够使用一些近似类的方法比如 new
instanceof
class
类的机制
建造
类 和 实例 的概念来源于房屋建造, 类好比是蓝图,而实例则是根据蓝图造好的房子
构造函数
类实例是由一个特殊的类方法构造的,这个方法名通常和类名相同,被称为构造函数
———————————————-以上内容为坑—————————————————
第5章 原型
[[Prototype]]
Javascript中的对象有一个特殊的[[Prototype]]内置属性,其实就是对于其他对象的引用。几乎所有的对象在创建时[[Prototype]]属性都会被赋予一个非空的值。
1 | var anotherObj = { |
虽然myObj里面没有a属性,但是通过[[Prototype]]链找到了anotherObj,并[[Get]]到了属性a
即使你是用for..in遍历对象时原理和查找[[Prototype]]链类似。
1 | var anotherObj = { |
Object.prototype
[[Prototype]]的尽头,最终都会指向内置的Object.prototype
属性设置和屏蔽
1 | myObject.foo = "bar"; |
的详细过程
- 如果在[[Prototype]]链上层存在名为foo的普通数据访问属性并且没有被标记为只读(writable: false),那就会在myObject中添加一个名为foo的新属性,它是屏蔽属性。
- 如果在[[Prototype]]链上层存在foo,但是它被标记为只读(writable: false),那么无法修改已有属性或者在myObject上创建屏蔽属性。
- 如果在[[Prototype]]链上层存在foo并且它是一个setter,那就一定会调用这个setter。foo不会被添加到myObject, setter相关
“类”
Javascript 是特殊的面向对象的语言,它可以直接创建对象,并直接定义对象自己的行为。
“类”函数
在Javascript中没有类可以被复制,也不能创建实例,但是以new创建的新对象并不会和原型对象失去联系,他们是互联的。
“构造函数”
在Javascript中并不存在构造函数,你在普通的函数调用前面加上 new 关键字之后,new会劫持所有普通函数并用构造对象的形式来调用它。说白了Javascript没有构造函数,只有调用。
技术
参考以下代码
1 | function Foo( name ){ |
这段代码展示了另外两种“面向类”的技巧:
- this.name = name 给每个对象(a, b)都添加了一个.name属性。
- Foo.prototype.myName 它给Foo对象添加一个属性(函数)。现在a,b都可以使用此函数。
其实a,b并没有复制对象 Foo.prototype,只是关联到了 Foo.prototype上
(原型)继承
参考以下代码
1 | function Foo( name ){ |
这段代码的核心部分就是语句 Bar.prototype = Object.create( Foo.prototype ); 调用Object.create()会凭空创建一个新对象并把新对象内部的[[Prototype]]关联到拟制定的对象上
以下两种方法是错误的
1 | //和你想要的机制不一样! 这里属于直接引用 |
所以如果想创建一个合适的关联对象,我们必须使用Object.create()
检查“类”关系
假设我们要寻找一个对象的委托的对象
检查一个实例的继承祖先(Javascript中的委托关联)通常被称为内省(或者反射)
判断对象和函数之间的关系可以直接使用 instanceof
如下
1 | function Foo(){ |
那如何判断对象与对象的关系呢?
使用 Foo.prototype.isPrototypeOf( a ); //true
我们也可以直接获取一个对象的[[Prototype]]链,如下
Object.getPrototypeOf( a ) === Foo.prototype; //true
对象关联
现在我们知道了,[[Prototype]]机制就是存在于对象中的一个内部链接,它会引用其他对象。
通常来说这个链接的作用是:如果在对象上没有找到需要的属性或者方法引用,引擎就会继续在[[Prototype]]关联的对象上进行查找。同理,如果在后者中也没有找到需要的引用就会继续查找它的[[Prototype]],以此类推。这一系列对象的链接被称为“原型链”。
- 创建关联
Object.create()会创建一个新对象并把它关联到我们指定的对象1
2
3
4
5
6
7var foo = {
something: function(){
console.log("Tell me something good..");
}
};
var bar = Object.create( foo );
bar.something(); //Tell me something good.. - 关联关系是备用
如果我们把对象之间的关联当成是处理“缺失”属性或方法时的一种备用,我们就需要考虑一下是否需要重新设计你的代码。
为了更清晰的设计你的软件我们可以使用内部委托来替换直接委托,可以使你的代码更清晰。1
2
3
4
5
6
7
8
9
10
11var anotherObject = {
cool: function(){
console.log( "cool!" );
}
},
var myObject = Object.create( anotherObject );
myObject.doCool = function(){
this.cool(); //内部委托
};
myObject.doCool(); //cool!;