文章目录
this的误解
this指向函数自身
大多数人会根据this的字面意思将它理解为【指向函数自身】。
什么情况下,this会指向自身呢?
- 递归 。也就是从函数本身的内部调用自己
- 当我们写一个第一次调用后,自己解除绑定的事件处理器时,这个时候也需要使用到函数自身
但是this真的就是指向函数自身吗?
function foo(num){
console.log("foo:" + num)
// 记录foo被调用的次数
this.count++
}
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 天,竟然是0
上面的this实际指得是 window
全局对象,而不是函数自身。
【如何让函数可以真的指向它自身呢?】
单单靠this是不够的,一般的我们还需要通过一个指向函数对象的词法标识符(变量)来引用它
【栗子一】
function foo () {
foo.count = 4;
}
这里的foo是指向它自身
【栗子二】
setTimeout(function(){
// 匿名(没有名字)函数无法指向自身
},100)
如上所示,第一个例子函数被称为具名函数,因此可以使用它的函数名来表示它自身;但是第二个例子中的函数是匿名函数(没有标识符),因此无法从函数内部引用自身
还有一种传统的但是已经被弃用和批判的用法,即使用
arguments.callee
来引用当前正在运行的函数对象。这是唯一一种可以从匿名函数对象内部引用自身的方法。但是最好的方式还是避免使用匿名函数,至少在需要引用自身方法时尽量使用具名函数(表达式)而不使用匿名函数
因此对于具名函数我们可以使用函数名来代表当前函数自身
针对上面的例子可以更改为:
function foo(num){
console.log("foo:" + num)
// 记录foo被调用的次数
foo.count++
}
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) // 4 ,这次是4啦
这种方法完全是依赖于foo的词法作用域,还有一种方法是确实可以让this指向函数自身(准确的说是可以任意指定this的指向)
function 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) {
// 注意这里的foo是如何被调用的
foo.call(foo, i)
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
// foo被调用了多少次
console.log(foo.count) // 4 ,这次也是4啦
这次的this确实是指向函数自身啦
this指向函数作用域
咳咳,需要知道的是:在js内部,作用域确实和对象类似,且可见的标识符都是它的属性,但是作用域“对象”是无法通过js代码访问的,它存在于js引擎内部
因此this在任何情况下都不指向函数的词法作用域
this到底是什么
【要知道的】
当一个函数被调用时,会创建一个记录(有时候也称为执行上下文)。这个记录包含了函数在哪里被调用(调用栈),函数的调用方式,传入的参数信息等。而this就是这个记录的一个属性
因此this实际上是在函数被调用时发生的绑定,他指向什么完全取决于函数在哪里被调用。而和函数声明的位置没有关系
this的全面解析
因为this是在函数被调用时绑定的,要想知道this就必须先寻找函数的调用位置 =》然后在根据this的绑定规则的优先级来确定它的绑定对象
默认绑定
即独立函数调用。不带任何修饰的函数引用进行调用
foo运行在非严格模式下,此时的this指向widow.(在严格模式下调用foo则不影响默认绑定)
foo运行在严格模式下,this则不能将全局对象用于默认绑定,因此this会绑定到undefined上
【foo运行在严格模式下】
function foo(){
"use strict"
console.log(this.a)
}
var a = 2
foo() // TypeError: this is undefined
【在严格模式下调用foo】
function foo(){
"use strict"
console.log(this.a)
}
var a = 2
foo() // TypeError: this is undefined
【foo运行在非严格模式下】
function foo(){
console.log(this.a)
}
var a = 2
(function(){
"use strict"
foo(); // 2
})()
隐式绑定
看调用位置是否有上下文对象,或者说是否被某个上下文对象拥有或者包含。如果有,隐式绑定规则会把函数中的this绑定到这个上下文对象
function foo () {
console.log(this.a)
}
var obj = {
a: 2,
foo: foo
}
obj.foo // 2
要知道的:foo函数虽然被当做引用作为obj的属性,但是严格来说它不属于obj对象
隐式丢失
一个最常见的this绑定问题就是被隐式绑定的函数会都丢失绑定对象,也就是说它会使用默认绑定,从而把this绑定到全局对象上火undefined上(取决于函数是否运行在严格模式下)
function foo () {
console.log(this.a)
}
var obj = {
a: 2,
foo: foo
}
var bar = obj.foo
var a = "opps global" // 全局对象上的属性
bar() // oops global
虽然bar
也是obj.foo
的一个引用,但是它实际上引用的是foo函数本身,因此bar()
其实是一个不带任何修饰符的函数调用,因此使用了默认绑定
类似的情况也会发生在回调函数中:
function () {
console.log(this.a)
}
function doFoo (fn) {
fn()
}
var obj = {
a: 2,
foo: foo
}
var a = "opps global"
doFoo(obj.foo) // opps global
这里的参数传递实际上就是一种隐式赋值
如果把函数传递给语言内置的函数,而不是自己定义的函数函数内呢?还会发生隐式丢失吗?
function foo () {
console.log(this.a)
}
var obj = {
a: 2,
foo: foo
}
var a = "oops global"
setTimeout(foo, 100) // oops global
实际上js环境内置的setTimeout()
函数实现和下面的伪代码类似
function setTimeout (fn, delay) {
// 等待delay秒后
fn() // 调用位置
}
显示绑定
【隐式绑定特点】:
必须在一个对象内部包含一个指向函数的属性,并通过这个属性来间接引用函数,从而才可以把this间接(隐式)的绑定到这个对象上
那么如果我们不想在对象内部包含函数引用,而是直接想在某个对象上强制调用函数。(想把函数中的this绑定到任意的对象上)
call、apply
function foo(){
console.log(this.a)
}
var obj = {
a: 2
}
foo.call(obj) // 2
如果我们传入了一个原始值(字符串类型、布尔类型、数字类型)来当做this的绑定对象,这个原始值会被转换成它的对象形式(也就是new String(..)
、new Boolean(...)
、new Number(...)
)。这通常被称为装箱
硬绑定
硬绑定是显示绑定的一个变种。硬绑定的典型应用场景:创建一个包裹函数,这个函数主要负责接受参数并返回值
【不接受参数,也不返回值的包裹函数】
function foo () {
console.log(this.a)
}
var obj = {
a: 2
}
// 创建一个包裹函数
var bar = function () {
foo.call(obj) // 将foo中的this强制绑定到obj对象上
}
bar() // 2 foo里面的this是obj
setTimeout(bar, 100) // 2 foo里面的this没有隐式丢失
bar.call(window) // 2 硬绑定的bar也不能修改foo里面的this
【接受参数并返回返回数据的包裹函数】
function foo(something){
console.log(this.a, something)
return this.a + something
}
var obj = {
a: 2
}
// 创建一个包裹函数
var bar = function () {
// 向foo传入参数并返回foo执行的结果
return foo.apply(obj, arguments)
}
var result = bar(3); // 2 3
console.log(result) // 5
另外一个种就是创建一个可以重复使用的辅助函数
function foo (something) {
console.log(this.a, something)
return this.a + something
}
// 简单的辅助绑定函数
function bind (fn, obj) {
return function(){
fn.apply(obj, arguments)
}
}
var obj = {
a: 2
}
var bar = bind(foo, obj)
var result = bar(3) // 2 3
console.log(result) // 5
由于硬绑定是一种非常常用的模式,所以ES5提供了内置的方法Function.prototype.bind
function foo (something) {
console.log(this.a, something)
}
var obj = {
a: 2
}
var bar = food.bind(obj)
var b = bar(3) // 2, 3
console.log(b)
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
上面的原始实际上也是通过call(...)
和apply(...)
实现了显示绑定,这样我们也可以写些代码
new绑定
【要知道的:】js中的new的机制和传统面向类的语言中的完全不同
js中的构造函数
在js语言中的“构造函数”只是一些使用new操作符是被调用的函数。他们并不会属于某个类,也不会实例化一个类。他们甚至都不能说是一种特殊的函数类型,他们只是被new操作符调用的普通函数而已
实际上在js中并不存在所谓的“构造函数”,只有对函数的“构造调用”
【使用new来调用函数,或者说当函数发生构造调用时】会自动执行下面的操作:
- 创建或构造一个全新的对象
- 这个新对象会被执行[[prototype]]连接
- 这个新对象会被绑定到函数调用的this上
- 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象
function foo(a){
this.a = a
}
var bar = new foo(2)
console.log(bar.a) // 2
// 因此这里的 bar.a 实际上和foo里面的 this.a 是同一个对象上的数据
使用new来调用foo(…)时,我们会构造一个新的对象并把它绑定到foo(…)调用中的this上,最后如果没有返回指定对象,则将刚在新创建的对象返回
function foo (a) {
var this = {}
...
return this
}
四种绑定的优先级
显示 > 隐式
new > 隐式
【那么 new和显示的优先级呢?】
new 和 call/apply
无法一起使用,因此无法通过new foo.call(obj1)
来直接进行测试。但是我们可以使用硬绑定来测试他俩的优先级。
因为
call
和apply
都是调用函数,并改变函数中的this
,但是仅仅只是改变了this
,执行完call
和apply
之后并没有返回一个函数类型的数据。而new
是对【函数】的构造调用,因此不可以new
一个非函数的东东的。
js中提供的硬绑定即Function.prototype.bind(...)
的工作原理:会创建一个新的包装函数,而且这个函数也会忽略它当前的this绑定(无论绑定的对象是什么)都会把我们提供的对象绑定到this上
这样看起来硬绑定(也是硬绑定的一种)似乎比new绑定的优先级更高,无法shiyongnew来控制this绑定
function foo(something){
this.a = something
}
var obj1 = {}
var bar = foo.bind(obj1)
bar(2)
console.log(obj1.a)
var baz = new bar(3)
console.log(obj1.a)
console.log(baz.a)
哈哈,虽然硬绑定很强硬把this固定住不许其他人更改,但是new很聪明,人家使用自己创建了的this,不使用硬绑定的this。因此new > 硬绑定
总上所述:
new > 显示绑定 > 隐式绑定 > 默认绑定
如何判断this
- 函数是否在
new
中调用(new
绑定)?如果是,this
就是新创建的对象
var bar = new foo() // foo里面的this就是bar
- 函数是否通过
call、apply
(显式绑定)或应绑定调用(bind
)? 如果是,this
就是被绑定的那个指定对象
var bar = foo.call(obj) // foo里面的this就是obj
- 函数是否在某个上下文对象中调用(隐式绑定),如果是,
this
就是那个上下文对象
var bar = obj1.foo() // foo里面的this就是obj1
- 如果都不是,就是用默认绑定。如果是严格模式下,
this
就绑定到undefined
上,否则就绑定到全局对象上
var bar = foo() // 此时foo里面的this就是undefined或全局对象