第二部分:this和对象原型
1.关于this
1.1 为什么要使用this
this上下文对象
function identify(){
return this.name.toUpperCase();
}
function speak(){
var greeting = "hello,i'm "+identify.call(this);
console.log(greeting)
}
var me = {
name:"kyle"
};
var you = {
name :"reader"
};
identify.call(me); // KYLE
identify.call(you); // READER
speak.call(me); // hello,i'm KYLE
speak.call(you); // hello,i'm READER
这段代码使得可以在不同的上下文中重复使用函数identify和speak,不用针对每个对象编写不同版本的函数
否则需要为函数identify和speak显示传递一个上下文对象
this就提供了一个更优雅的方式隐式传递一个对象引用
1.2 误解
- 误解一:
1.this指向函数自身
函数内部引用函数自身,最常见的使用是递归,或者写一个在第一次被调用后自己接触绑定的事件处理
function foo(num){
console.log("foo:"+num);
this.count++
// 记录foo被调用的次数
}
foo.count = 0;
var i;
for(i = 0;i<10;i++){
if(i>5){
foo(i)
}
}
// foo:6
// foo:7
// foo:8
// foo:9
// foo 被调用的次数
console.log(foo.count) // 0
因此this并不是对自身的引用
function foo(num){
console.log("foo:"+num);
data.count++
// 记录foo被调用的次数
}
var data = {
count = 0;
}
var i;
for(i = 0;i<10;i++){
if(i>5){
foo(i)
}
}
// foo:6
// foo:7
// foo:8
// foo:9
// foo 被调用的次数
console.log(data.count) // 4
上述方法虽然解决了计数问题,但是从本质上,是使用了词法作用域的方式来进行解决
function foo(){
foo.count = 4;
// foo指向它的自身
}
setTimeout(function(){
// 匿名,函数无法指向自身
},10)
从函数内部引用自身,需要一个指向函数对象的词法标识符(变量)来引用它
第一个函数称为具名函数,,内部使用foo来引用自身
但是第二个函数中,回调函数没有名称标识符
- 传统的方式:
arguments,和callee来引用当前正在运行的函数对象
function foo(num){
console.log("foo:"+num);
this.count++
// 记录foo被调用的次数
// 在当前的调用方式下,this确实指向foo
}
foo.count = 0;
var i;
for(i = 0;i<10;i++){
if(i>5){
foo.call(foo,i)
}
}
// foo:6
// foo:7
// foo:8
// foo:9
// foo 被调用的次数
console.log(foo.count) // 4
- 误解二:
2.this作用域
第二种常见误解,就是this指向函数的作用域,这种说法,时而正确,时而不正确
作用域和对象确实相似,可见标识符都是他们的属性,但是作用域无法通过js代码访问,存在于js引擎内部
1.3 this到底是什么
this是在运行时进行绑定的,并不是在编写时绑定,
他的上下文取决于函数调用时的各种条件,只取决于函数调用方式
当一个函数被调用时,会创建一个活动记录(执行上下文)
这个记录包含函数在哪里被调用,函数的调用方式,传入的参数信息等
2.this全面解析
2.1 调用位置
调用位置是函数在代码中被调用的位置
主要分析调用栈(为了到达当前执行位置所调用的所有函数)
调用位置是当前正在执行的函数的前一个调用中
function baz(){
// 当前调用栈是:baz
// 因此当前调用位置是全局作用域
console.log("baz")
bar(); // bar 调用位置
}
function bar(){
// 当前调用栈是:baz -> bar
// 因此当前调用位置是baz
console.log("bar")
foo(); // foo 调用位置
}
function foo(){
// 当前调用栈是:baz -> bar ->foo
// 因此当前调用位置是bar
console.log("foo")
}
baz(); // baz的调用位置
baz的调用位置可以将调用栈想象成一个函数调用链
使用浏览器的调试工具:
在foo函数的第一行设置一个断点,或者在第一行代码之前插入一条debugger,则运行时,会显示出当前位置的函数调用列表
2.2 绑定规则
1.默认绑定
独立函数调用,可以看成是无法应用其他规则时的默认规则
this指向全局对象
function foo(){
console.log(this.a)
}
var a = 2;
foo()
foo()调用的时候,是直接不适用任何修饰的函数引用进行调用的,此时是默认绑定,this指向全局对象
但是只有在使用非严格模式下,默认绑定才能绑定到全局对象,在严格模式下,不影响默认绑定
2.隐式绑定
考虑调用位置是否有上下文对象,或者是否被某对象拥有或包含
function foo(){
console.log(this.a)
}
var obj = {
a:2,
foo:foo
}
obj.foo()
调用位置使用obj为上下文来引用函数,因此被调用时,obj对象拥有或包含函数引用
当函数拥有上下文对象时,隐式绑定规则会把函数调用中的this绑定在这个上下文对象上
function foo(){
console.log(this.a)
}
var obj2 = {
a:42,
foo:foo
}
var obj1 = {
a:2,
foo:foo
}
obj1.obj2.foo(); // 42
对象属性引用链中,只有上一层或者说最后一层在调用位置中起作用
function foo(){
console.log(this.a)
}
var obj = {
a:2,
foo:foo
}
var bar = obj.foo; //函数别名
var a = "opps,global"; //a是一个全局对象的属性
bar(); //"opps,global"
bar是obj.foo的一个引用,但实际上,引用的是函数本身,此时bar是一个不带任何修饰的函数调用,为默认绑定
function foo(){
console.log(this.a)
}
function doFoo(fn){
// fn 实际引用的是foo
fn(); // 调用位置
}
var obj = {
a:2,
foo:foo
}
var a = "opps,global"; //a是一个全局对象的属性
doFoo(obj.foo); //"opps,global"
参数传递,也是一种隐式传递,被隐式赋值,默认绑定
setTimeout的回调函数也会发生丢失this的情况
3.显示绑定
在某个对象上强制调用函数,使用函数的call()和apply()方法,
第一个参数为一个对象,为this准备,在调用的时候将其绑定到this,由于可以直接指定this绑定到的对象,称为显示绑定
function foo(){
console.log(this.a)
}
var obj = {
a:2,
}
foo.call(obj); // 2
调用call函数,将this绑定到obj中
如果此时传入的是一个原始值(字符串类型,布尔类型,或者数字类型)作为this的绑定对象,则这些原始值会被转换成它的对象形式,(new String(),new Boolean()等),这种行为称为装箱。
显示绑定的变种:
硬绑定
function foo(){
console.log(this.a)
}
var obj = {
a:2,
}
var bar = function(){
foo.call(obj)
};
bar(); // 2
setTimeout(bar,100); // 2
// 硬绑定的bar不可能再修改它的this
bar.call(window); // 2
bar()函数中,foo.call(obj):强制把foo的this绑定到了obj,后面再调用bar函数,则总在obj上调用foo
上述方法为显示的强制绑定,称为硬绑定
使用场景:
- 包裹函数:负责接收参数并返回值
function foo(something){
console.log(this.a,something)
return this.a+something
}
var obj = {
a:2,
}
var bar = function(){
return foo.apply(obj,arguments);
};
var b = bar(3); // 2 3
console.log(b) // 5
- 创建一个可重复使用的辅助函数
function foo(something){
console.log(this.a,something)
return this.a+something
}
function bind(fn,obj){
return function(){
return fn.apply(obj,arguments)
} ;
}
var obj = {
a:2,
}
var bar = bind(foo,obj)
var b = bar(3); // 2 3
console.log(b) // 5
ES5提供了内置的方法:Function.prototype.bind
bind会返回一个硬编码的新函数,把你指定的参数设置为this的上下文并调用原始函数
API调用的上下文
第三方库的很多函数,以及js语言和宿主环境中许多新的内置函数,都提供了一个可选参数,称为上下文,和bind的作用一致,确保回调函数使用指定的this
function foo(el){
console.log(el,this.id);
}
var obj = {
id:"awesome"
};
// 调用foo(...)时将this绑定到obj
[1,2,3].forEach(foo,obj);
// 1 awesome 2 awesome 3 awesome
4.new绑定
在js中,构造函数是一些使用new操作符时被调用的函数,并不属于某一个类,也不会实例化一个实例
包括内置对象函数(比如Number)在内的所有函数都可以使用new来调用,这种函数调用被称为构造函数调用
实际上不存在所谓的构造函数,只存在函数的构造调用
使用new来调用函数,会自动执行下面的操作
- 创建(或称构造)一个全新的对象
- 这个新对象执行Protype连接
- 这个新对象会绑定到函数调用的this
- 如果函数没有其他的返回对象,则new表达式中的函数调用会自动返回这个新对象
2.3优先级
如果某一个调用位置可以应用多条规则,这是需要为这些规则设置一个优先级
- 默认绑定,是优先级最低的
function foo(){
console.log(this.a)
}
var obj1 = {
a:2,
foo:foo
}
var obj2 = {
a:3,
foo:foo
}
obj1.foo(); // 2
obj2.foo(); // 3
obj1.foo.call(obj2); // 3
obj2.foo.call(obj1); // 2
- 由此可见,显示绑定优先级更高
优先级:
1.new绑定
2.显示绑定
3.隐式绑定
4.默认绑定
2.4绑定例外
- 被忽略的this
若把null或undefined作为this的绑定对象传入call,apply,bind,在调用时,会被忽略,执行的是默认绑定规则
当并不关心第一个参数的时候,使用null进行占位
function foo(a,b){
console.log("a:"+a+"b:"+b)
}
// 把数组展开成参数
foo.apply(null,[2,3]) // a:2,b:3
// 使用bing(...)进行柯里化
var bar = foo.bind(null,2)
bar(3); // a:2,b:3
但此时this是绑定到全局对象中的,容易造成一定的副作用,产生难以追踪和分析的bug
-
更安全的this
传入一个特殊的对象,将this绑定到这个对象,不会对程序产生任何副作用
“DMZ”(demilitarized zone)对象是一个空的非委托对象 -
间接引用
函数的间接引用,会使用默认绑定规则
间接引用最容易在赋值时发生 -
软绑定
硬绑定会降低函数的灵活性,使用硬绑定之后就无法使用隐式绑定或显示绑定修改this
如果可以给默认绑定指定一个全局对象,和undefined之外的值,可以实现和硬绑定相同的效果,同时保留隐式绑定或者显示绑定来修改this的能力 ——软绑定
2.5 this的词法
箭头函数不使用this的四种规则,二是根据外层(函数或全局)作用域来决定this
箭头函数的绑定不会被修改
箭头函数最常用于回调函数中,例如事件处理器或者定时器
这个ES6之前代码中的self = this机制相同