This 关键字
JavaScript 中 this 不是固定不变的, 它会随着执行环境的改变而改变.
- 在方法中, this 表示该方法所属的对象.
- 如果单独使用, this 表示全局对象.
- 在函数中, this 表示全局对象.
- 在函数中, 在严格模式下, this 是未定义的 (undefined).
- 在事件中, this 表示接收事件的元素.
- 类似 call() 和 apply() 方法可以将 this 引用到任何对象.
1 | function identify() { |
同时如果不使用 this
我们可以传入一个上下文到调用的函数中, 例如这样:
1 | function identify(context) { |
几个对 this
关键字的误解
认为 this
是指向函数自身
1 | function foo(num) { |
但是实际上我们操作的不是这个 foo
里面的 count
而是一个全局变量 count
解决方案
当然解决这个问题很简单, 不要在函数中操作 this
就是一个Solution:
1 | function foo(num) { |
或者操作一个全局的count.
或者用另一种办法强行使用 this
1 | function foo(num) { |
认为 this
指向函数的 scope
这太愚蠢了…
1 | function foo() { |
方法调用及调用栈
想要理解 this
首先就要了解一个方法在哪里调用的
1 | function baz() { |
多数浏览器的 Debugger
工具可以方便地看到调用栈
调用规则
-
默认绑定
- 直接定义的变量都属于
global object
- 注意这种绑定在
strict mode
不生效并且会报Undefined
1
2
3
4
5
6
7
8
9
10var a = 10;
b = 10;
this.a === a; // true
this.b === b; // true
//--------------------------
function foo() {
console.log(this.a);
}
var a = 2;
foo(); // 2 - 直接定义的变量都属于
-
隐式绑定
1
2
3
4
5
6
7
8
9
10function foo() {
console.log(this.a); // `this.a` is synonymous with `obj.a` .
}
var obj = {
a: 2,
foo: foo
};
obj.foo(); // 2- 注意这里的调用处仅仅会剥离一层, 因此最后一个调用者将会是
this
所代表的内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function foo() {
console.log(this.a);
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42 - 注意这里的调用处仅仅会剥离一层, 因此最后一个调用者将会是
-
隐式丢失
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
31function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // function reference/alias!
var a = "oops, global"; // `a` also property on global object
bar(); // "oops, global"
//--------------------------
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo; // function reference/alias!
var a = "oops, global"; // `a` also property on global object
bar(); // "oops, global"
setTimeout(obj.foo, 100); // "oops, global"特别对于上面
setTimeout
函数1
2
3
4function setTimeout(fn, delay) {
// wait (somehow) for `delay` milliseconds
fn(); // <-- call-site!
} -
显式绑定
- 当调用
call()
或者applt()
的时候我们可以强行传一个obj
作为this
1
2
3
4
5
6
7
8
9function foo() {
console.log(this.a);
}
var obj = {
a: 2
};
foo.call(obj); // 2同时注意如果给
this
传进原始类型的数据时,对应数据会进行装包(boxing),即转换成对应Obj (new String(…), new Boolean(…), or new Number(…), respectively) - 当调用
-
强绑定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18function foo() {
console.log( this.a );
}
var obj = {
a: 2
};
var bar = function() {
foo.call( obj ); // 强行将obj传给this
};
bar(); // 2
setTimeout( bar, 100 ); // 2
// `bar` hard binds `foo` 's `this` to `obj`
// so that it cannot be overriden
bar.call( window ); // 2另外使用
bind()
方法可以强行设定this
的值为某个其他变量。
绑定顺序
new
绑定的条件下, 那么这是一个全新的Obj
1 | var bar = new foo() |
- 通过
call
或者apply
进行显式绑定, 或者使用了bind
进行强绑定, 那么这就是个显式绑定的Object
1 | var bar = foo.call(obj2) |
- 通过上下文进行隐式调用, 或者是某个对象的Attr, 那么
this
就是当前上下文
1 | var bar = obj1.foo() |
- 否则就是默认绑定了. 记住如果是严格模式
this=undefined
, 否则this=global object
1 | var bar = foo() |
例外情况
当模块不需要用到 this
的时候, 但是却需要使用 bind
等函数, 可以将 null
传到 this
.
同时这种情况下就会默认使用 默认绑定
的规则
1 | function foo() { |
1 | function foo(a, b) { |
Indirection
话说这个到底怎么翻译啊… 重定向吗?
1 | function foo() { |
还是很好理解的, 上面的赋值语句执行后返回了一个单纯的 foo
变量, 因此导致了 Indirection
, 并且使用了 默认绑定
注意默认绑定的规则:
non-strict mode
模式下: 引用global object
strict mode
模式下: 对应引用变成Undefined
语义绑定/Lexical this/ES6
ES6多了个新玩意: 箭头符号
相关的绑定称作"Lexical this"
1 | function foo() { |
如果是普通函数输出应该是3因为 this
绑定到了 obj2
而语义绑定无法被重载, 即使用了 new
关键字
一个例子:
1 | function foo() { |
另一种针对箭头符号的解决方案, 通过外部重新赋值来实现可读性, 这样就知道这儿的 this
是指向函数的了
1 | function foo() { |
不过上述两段代码都是某种意义上的 偷懒
, 如果真的想要掌握 this
还是需要:
Use only lexical scope and forget the false pretense of
this
-style code.Embrace
this
-style mechanisms completely, including usingbind(..)
where necessary, and try to avoidself = this
and arrow-function “lexical this” tricks.
Objects
属性标识符 Property Descriptors
没什么好说的, 就几个特殊的属性:
Writable
注意必须要在严格模式下才会报错
1 | //注意必须要在严格模式下才会报错 ; |
Configurable
表示是否允许下一次使用 defineProperty
进行配置
非严格模式下也会报错, 这是一种无法返回的操作
1 | var myObject = { |
并且设置为 false 之后也无法使用 delete
删除对应的属性
1 | myObject.a; // 2 |
delete
用于删除一个是对象的属性
, 如果这个属性是某变量的最后一个属性, 那么delete
之后就会变成空引用并且对应资源会被回收但是这玩意不能用于内存回收, 他只是删除了一个属性而已
Enumerable
很多奇怪的函数里面会进行判断这个属性
Immutability
这不是一个实际的属性, 不过我们有时候需要将一个变量变得 永恒不变
, 通过下面这些办法:
对象常量 Object Constant
很简单:
writable:false
andconfigurable:false
1 | var myObject = {}; |
关闭扩充性 Prevent Extensions
Object.preventExtensions(..)
将令变量无法添加新属性
1 | var myObject = { |
- 严格模式下: 报错
- 非严格模式: 不报错, 但是修改无效, b依然等于2
Seal
Object.seal(..)
= Object.preventExtensions(..)
+ configurable:false
但是依然可以修改属性的值
1 | var obj = { |
Freeze
Object.freeze(..)
= Object.seal(..)
+ writable:false
1 | var obj = { |
Class
这里只强调ES6的 class
的使用方法
基本和多数OO语言一样
1 | // unnamed |
构造函数和属性方法
1 | class Rectangle { |
静态方法
不通过初始化实例就能调用的方法
1 | class Point { |
继承
1 | class Animal { |
注意即使是以前使用原型创造的父类也可以进行继承
1 | function Animal(name) { |
还有另一种继承方法, 使用 Object.setPrototypeOf(Dog.prototype, Animal);
1 | var Animal = { |
超类
直接用super关键字
1 | class Lion extends Cat { |
多继承
ES不支持多继承, 但是可以用 mixin
的方法伪装一个:
1 | //将一个类传入,并且返回一个扩展之后的类 |
Prototype
所有的 Object
都的最顶层都是 Object.prototype
.
Setting & Shadowing Properties
1 | var anotherObject = { |
注意上面如果不给子类自增而直接给父类执行自增, 那么子类因为是调用继承的属性因此也会返回3
- 当一个属性在继承链的高层被发现并且可写的话, 那么就会发生Property Shadowing
- 当然如果在高层发现并且不可写, 那么就会设置失败, 并且严格模式下会直接报错
- 单原型链上存在一个与这个属性相关的
Setter
并且一定会调用到这个Setter
, 那么这个属性的再次赋值必然会失败
constructor
constructor 没啥特别的, 一个类对应的函数就是一个constructor
但是使用 new
关键字的时候会调用这个constructor, 这是唯一一个constructor和函数的区别
constructor和prototype的关系
1 | function test() { |
- 首先
new
的时候执行了对应的constructor, 输出 t
是没有prototype
这个属性的, 因为它不是class而是objtest.prototype.constructor
是test()
定义的时候创建的t.constructor
也指向同一个test()
另外, 如果将 test
的 prototype
改为另一个方法, 那么 t.constructor
也会指向那个新方法
1 | function test() { |
因为我们将 test.prototype
转到了一个新的Obj上面, 并且修改之后 test.prototype.constructor
不存在了 , 因此接下来初始化的Obj会继承最高层的 Object.prototype.constructor
解决这个问题的方法很简单, 在切换这个 test.prototype
的同时也将constructor也赋值过去, 或者直接在新的prototype里面放一个 constructor
的属性
1 | Object.defineProperty( test.prototype, "constructor" , { |
Generally, such references should be avoided where possible.
“(Prototypal) Inheritance”
正确的继承方法
1 | function Foo(name) { |
错误的继承方法
1 | // doesn't work like you want! |
第一行改变了引用, 因此之后如果希望可以Bar进行扩展(比如添加新方法)的时候实际扩展了Foo
第二行同样使用Foo的constructor来创建新实例, 但是要注意进行扩展(比如扩展this)的时候同样会扩展到Foo
ES6的扩展
1 | // pre-ES6 |
类反射 Reflection
前三种方法中: 父类必然是子类实例对应的class
就是OOP里面根据instance获取对应class的方法:
1 | a instanceof Bar; // true |
更详细的一种方法:
1 | function isRelatedTo(o1, o2) { |
更简单的一种方法:
1 | Foo.prototype.isPrototypeOf(a); // true |
简单粗暴的ES5的方法:
1 | Object.getPrototypeOf(a) === Foo.prototype; // false, 如果Bar继承于Foo, 此处依然检测不出来 |
总结
上方继承代码集合:
1 | function Foo() { |
类反射判断:
1 | // relating `Foo` and `Bar` to each other |
使用原始的对象连接OLOO (objects-linked-to-other-objects)模式来实现上方的代码:
1 | var Foo = { |
对应的类反射就有些不同:
1 | // relating `Foo` and `Bar` to each other |