JavaScript函数中this的指向
本文是学习coderwhy前端课程中js高阶的一些笔记总结,其中的例子是引用了老师上课的一些例子
文章目录
一、函数的基本用法
// 定义函数
function foo(name) {
console.log("foo函数:", this)
}
// 1.方式一: 直接调用
// foo()
// 2.方式二: 通过对象调起
var obj = { name: "why" }
obj.aaa = foo
obj.aaa()
二、this绑定规则
1. this绑定规则一:默认绑定
默认绑定规则描述 :
默认绑定是最基本的规则,当函数调用不符合其他绑定规则(如隐式绑定、显式绑定或者new绑定)时应用。在默认绑定下,函数的调用方式决定了this
的值:
- 非严格模式下,
this
默认绑定到全局对象(浏览器环境中是window
对象)。 - 严格模式下,
this
将绑定到undefined
,防止不小心修改全局对象。
(1)普通的函数被独立调用
// "use strict"
// 定义函数
// 1. 普通的函数被独立调用
function foo() {
console.log("foo:",this)
}
foo() // 在非严格模式下,this指向全局对象window;在严格模式下,this为undefined。
当函数不在任何对象或类中直接调用时,this
的值取决于是否在严格模式下运行。在非严格模式下,this
会默认绑定到全局对象(浏览器中是window
对象),在严格模式下,this
会被绑定到undefined
。
(2) 函数定义在对象中,但独立调用
//2. 函数定义在对象中,但是独立调用
var obj = {
name: "why",
bar: function() {
console.log("bar:",this)
}
}
var baz = obj.bar
baz()
在这里,bar
函数从其属于的对象obj
中脱离出来,并被赋值给baz
。当通过baz()
独立调用时,this
遵循默认绑定规则,即指向全局对象或在严格模式下为undefined
。
(3)高阶函数
// 3.高阶函数
var obj = {
name: "why",
bar: function() {
console.log("bar:",this)
}
}
function test(fn) {
fn()
}
test(obj.bar)// 传递obj.bar给函数test并调用
即使bar
函数最初是obj
对象的方法,当它被作为参数传递并在test
函数中调用时,this
再次遵循默认绑定,指向全局对象或在严格模式下为undefined
。
(4)严格模式下的默认绑定
严格模式下,独立调用的任何函数中的this
值将被设置为undefined
。这是一种防止函数意外修改全局对象的安全措施。
2. this绑定规则二:隐式绑定
隐式绑定规则:
- 调用位置:在确定
this
的绑定时,关键在于查找函数被调用的位置。具体来说,是查看函数调用时点符号.
前面的对象是什么。 - 调用方式:如果一个函数是作为对象的方法被调用,那么
this
就指向这个对象。
在JavaScript中,隐式绑定是指当一个函数作为对象的属性被调用时,this
自动指向这个对象。
function foo() {
console.log("foo函数:", this)
}
var obj = {
bar: foo
}
obj.bar() // 调用方式决定了this绑定到obj
在这个例子中,函数foo
最初被定义为一个普通的函数。之后,它被赋值给对象obj
的bar
属性。当通过obj.bar()
调用foo
时,this
关键字自动绑定到obj
对象。这是因为foo
是作为obj
对象的bar
属性被调用的。
隐式绑定的失效情况:
尽管隐式绑定似乎很直接,但在某些情况下,它可以失效,使this
回到默认绑定(指向全局对象或undefined
)。失效的常见情况包括:
-
间接引用:如果将方法赋值给一个变量,并通过这个变量调用方法,隐式绑定将失效。
var anotherFunc = obj.bar; anotherFunc(); // this不再指向obj,而是全局对象或在严格模式下为undefined
-
传递给高阶函数:如果你将一个对象的方法作为回调函数传递给另一个函数,除非使用绑定工具(如
.bind
),否则原有的this
绑定通常会丢失。setTimeout(obj.bar, 100); // 当bar被调用时,this通常不会指向obj
3. this绑定规则三:new绑定
/*
new绑定的步骤:
1.创建新的空对象
2.将this指向这个空对象
3.执行函数体中的代码
4.没有显示返回非空对象时, 默认返回这个对象
*/
function foo() {
this.name = "why"
console.log("foo函数:", this)
}
new foo()
在这个示例中:
new foo()
创建了一个新对象。this
在foo
函数中指向了这个新创建的对象。- 在
foo
函数内部,这个新对象被赋予了一个名为name
的新属性,并设为"why"
。 - 函数执行了
console.log("foo函数:", this);
语句,打印了这个新对象,显示它现在包含一个name
属性。 - 由于函数没有返回任何值,所以
new foo()
的结果是新创建的对象。
4. this绑定规则四:显示绑定
在JavaScript中,显式绑定是一种通过特定的函数方法,如 .call()
, .apply()
, 和 .bind()
, 来设定函数执行时this
的值的技术。通过显式绑定,我们可以直接指定函数调用的上下文对象,不论函数在何处被声明或被调用。
显式绑定的机制:
显式绑定主要通过三种函数方法实现:
.call(thisArg, arg1, arg2, ...)
:调用一个函数,其this
值被显式设置为call
的第一个参数thisArg
,其余参数依次传递给函数。.apply(thisArg, [argsArray])
:与.call()
类似,但接受一个参数数组作为函数执行时的参数。.bind(thisArg[, arg1[, arg2[, ...]]])
:创建一个新函数,当这个新函数被调用时,其this
值永久地被绑定为.bind()
的第一个参数thisArg
,其余参数将作为新函数的预设参数。
var obj = {
name: "why"
};
function foo() {
console.log("foo函数:", this);
}
// 使用.call()方法,将this显式绑定到obj对象
foo.call(obj); // 输出 "foo函数: { name: 'why' }"
// 将this显式绑定到数字123
foo.call(123); // 输出 "foo函数: Number {123}"
// 将this显式绑定到字符串"abc"
foo.call("abc"); // 输出 "foo函数: String {'abc'}"
-
foo.call(obj)
:在这个调用中,this
被显式设置为指向obj
对象。因此,当foo
执行时,它打印出this
指向的对象,显示对象中的name
属性。 -
foo.call(123)
和foo.call("abc")
:在这两个调用中,this
不指向一个常规的对象。JavaScript将这些原始值(数值和字符串)转换为它们对应的包装对象(Number
和String
对象)。因此,函数内部的this
实际上是一个对象,而这个对象是原始值的一个包装形式。
三、this绑定规则优先级
1. 绑定优先级概述:
JavaScript中this
的绑定规则有四种主要形式,且它们具有不同的优先级:
- new绑定
- 显式绑定 (
call
,apply
,bind
) - 隐式绑定 (作为对象的方法调用)
- 默认绑定 (独立函数调用)
2. 显式绑定高于隐式绑定:
function foo() {
console.log("foo:", this)
}
// 测试一: apply/call高于默认绑定
var obj = { foo: foo };
obj.foo.apply("abc"); // this 绑定到 "abc"
obj.foo.call("abc"); // this 绑定到 "abc"
// 测试二: bind高于默认绑定
var bar = foo.bind("aaa");
var obj = {
name: "why",
baz: bar
};
obj.baz(); // this 仍然绑定到 "aaa", 即使是作为 obj 的方法调用
无论是.apply()
, .call()
还是.bind()
,显式绑定的this
值都会覆盖隐式绑定。
3. new 绑定高于隐式绑定
function foo() {
console.log("foo:", this)
}
var obj = {
name: "why",
foo: function() {
console.log("foo:", this);
console.log("foo:", this === obj);
}
};
new obj.foo(); // this 指向新创建的对象,而不是 obj
即使foo
函数作为obj
的方法被定义和调用,new
操作符创建了一个新的对象,并且this
被绑定到这个新对象上。
4. new绑定高于bind
function foo() {
console.log("foo:", this);
}
var bindFn = foo.bind("aaa");
new bindFn(); // this 指向新创建的对象,而不是绑定的 "aaa"
尽管foo
被.bind()
方法绑定到"aaa"
,但new bindFn()
的调用创建了一个新的对象,this
指向这个对象,显示new
绑定优先级高于.bind()
。
5. bind和apply/call的比较
var bindFn = foo.bind("aaa");
bindFn.call("bbb"); // this 仍然绑定到 "aaa"
这个例子中,尽管.call()
试图将this
绑定到"bbb"
,bindFn
的this
已被永久绑定到"aaa"
,表明.bind()
的效果是不可被后续的.call()
或.apply()
覆盖的。
6. 总结
this
绑定的优先级从高到低依次是:new
绑定、显式绑定(bind
优先于call/apply
)、隐式绑定、默认绑定。
四、this绑定之外的情况
1. 显式绑定null
或undefined
当使用.call()
, .apply()
, 或 .bind()
进行显式绑定时,如果第一个参数是null
或undefined
,则这些方法不会将this
绑定到任何特定值上,而是采用默认绑定规则(非严格模式下绑定到全局对象,严格模式下绑定到undefined
)。
function foo() {
console.log("foo:", this)
}
foo.apply("abc"); // "foo:", String {'abc'}
foo.apply(null); // "foo:", Window(浏览器环境下的全局对象)或 global(Node.js)
foo.apply(undefined); // "foo:", Window 或 global
这里,.apply("abc")
将this
绑定到字符串"abc"的包装对象。但是,.apply(null)
和.apply(undefined)
则回落到默认绑定规则。在严格模式下,这些调用将导致this
绑定到undefined
。
2.情况二:间接函数引用
间接引用发生时,函数虽然是从某个对象上取得,但由于赋值或其它操作,函数的调用已经与原对象分离。
var obj1 = {
name: "obj1",
foo: function() {
console.log("foo:", this)
}
};
var obj2 = {
name: "obj2"
};
// obj2.foo = obj1.foo
// obj2.foo() // 正常隐式绑定,"foo:", obj2
(obj2.foo = obj1.foo)(); // "foo:", Window 或 global
在(obj2.foo = obj1.foo)()
的调用中,函数通过赋值操作被取得,并立即调用。这种调用方式使得函数的执行脱离了任何对象的上下文(默认绑定),因此在非严格模式下,this
指向全局对象。这是因为赋值表达式(obj2.foo = obj1.foo)
返回的是函数本身,并且这个返回值不持有原始的对象引用。
五、箭头函数
箭头函数在ECMAScript 6中引入,提供了一种更简洁的函数写法,主要用于匿名函数表达式。与传统的函数表达式相比,箭头函数具有几个特点,包括更短的语法、没有自己的this
、arguments
、super
或new.target
,这些值由外围最近一层非箭头函数决定。
箭头函数的注意事项:
- 不适合用作方法:由于箭头函数没有自己的
this
,如果将箭头函数作为对象的方法,this
将不会指向该对象。 - 不能用作构造函数:箭头函数不能使用
new
关键字,因为它们没有[[Construct]]
方法,不具备构造对象的能力。 - 没有
arguments
对象:不能直接访问命名参数之外的参数,除非使用剩余参数语法。
1. 箭头函数基本使用
(1) 传统函数和箭头函数的对比:
// 1.函数传统写法
function foo1() {}
var foo2 = function(name, age) {
console.log("函数体代码", this, arguments)
console.log(name, age)
}
// 2.箭头函数完整写法
var foo3 = (name, age) => {
console.log("箭头函数的函数体")
console.log(name, age)
}
(2)forEach中使用箭头函数
// 3.箭头函数的练习
// 3.1. forEach
var names = ["abc", "cba", "nba"]
names.forEach((item, index, arr) => {
console.log(item, index, arr)
})
- 箭头函数作为回调函数传递给
forEach
方法,可以直接访问item
,index
, 和arr
。 - 这种场景下箭头函数非常方便,因为它可以继承外围作用域的
this
。
(3)setTimeout中使用箭头函数
// 3.2. setTimeout
setTimeout(() => {
console.log("setTimeout")
}, 3000)
- 在
setTimeout
调用中使用箭头函数,无需担心this
的指向问题,因为箭头函数不绑定this
,可以安全地用在定时器和延时调用中。
2. 箭头函数的简写
var names = ["abc", "cba", "nba"]
var nums = [20, 30, 11, 15, 111]
// 1.优化一: 如果箭头函数只有一个参数, 那么()可以省略
names.forEach(item => {
console.log(item)
})
var newNums = nums.filter(item => {
return item % 2 === 0
})
// 2.优化二: 如果函数体中只有一行执行代码, 那么{}可以省略
names.forEach(item => console.log(item))
// 一行代码中不能带return关键字, 如果省略, 需要带return一起省略(下一条规则)
var newNums = nums.filter(item => {
return item % 2 === 0
})
// 3.优化三: 只有一行代码时, 这行代码的表达式结果会作为函数的返回值默认返回的
var newNums = nums.filter(item => item % 2 === 0)
var newNums = nums.filter(item => item % 2 === 0)
// 4.优化四: 如果默认返回值是一个对象, 那么这个对象必须加()
// 注意: 在react中我会经常使用 redux
var arrFn = () => ["abc", "cba"]
var arrFn = () => {} // 注意: 这里是{}执行体
var arrFn = () => ({ name: "why" })
console.log(arrFn())
// 箭头函数实现nums的所有偶数平方的和
var nums = [20, 30, 11, 15, 111]
var result = nums.filter(item => item % 2 === 0)
.map(item => item * item)
.reduce((prevValue, item) => prevValue + item)
console.log(result)
3. 箭头函数中this的使用
// 1.普通函数中是有this的标识符
function foo() {
console.log("foo", this);
}
foo(); // 在非严格模式下,this指向全局对象(浏览器中的window,Node.js中的global)
foo.apply("aaa"); // 显式绑定this到字符串"aaa"的包装对象
// 2. 箭头函数中不存在this
var bar = () => {
console.log("bar:", this);
}
bar(); // 箭头函数中的this继承自创建时的上下文
bar.apply("aaaa"); // .apply()和.call()无法改变箭头函数的this,它仍然指向创建时的上下文
箭头函数bar
被定义在全局作用域中,因此其this
指向全局对象(在浏览器中是window
,在Node.js中可能是global
或模块的exports
对象),即便使用.apply()
或.call()
也无法改变this
的指向。
4. this查找规则
var obj = {
name: "obj",
foo: () => {
var bar = () => {
console.log("bar:", this);
}
return bar;
}
}
var fn = obj.foo();
fn.apply("bbb"); // 嵌套的箭头函数中的this仍然指向创建时的上下文,不会被.apply()改变
在这个例子中,即使foo
是作为obj
的属性定义的,由于foo
是箭头函数,其this
并不指向obj
,而是继承自其创建时的上下文(全局作用域)。因此,嵌套的箭头函数bar
中的this
同样指向全局作用域,而非obj
或通过.apply()
尝试指定的任何对象。