定义理解
当一个函数被调用时,会创建执行上下文,执行上下文会包含函数的调用栈,调用方法,传入的参数等信息,this就是执行上下文的其中的一个属性,是一个动态绑定的指针,是在函数被调用时发生的绑定
this的绑定分为以下5种
- 默认绑定
- 隐式绑定
- 显式绑定
- new绑定
- 箭头函数绑定
默认绑定
- 非严格模式下:独立函数调用,无法应用其他绑定规则的默认规则,this指向全局对象
- 严格模式下: 不能将全局对象应用于默认绑定,this会绑定到undefined,但是在严格模式下调用函数则不影响默认绑定
独立调用foo函数,this应用默认绑定,绑定到了window上
var x = 10;
function foo(){
console.log(this.x) // 10
}
foo();
运行在严格模式下,this会绑定到undefined
var x = 2;
function foo() { // 运行在严格模式下,this会绑定到undefined
"use strict";
console.log(this.x);
}
// 调用
foo(); // TypeError: Cannot read property 'a' of undefined
严格模式下调用函数则不影响默认绑定
var x = 2;
function foo() { // 运行
console.log(this.x);
}
(function() {
"use strict";
foo(); // 2
})();
隐式绑定
当函数引用有上下文对象时,会把函数中的this绑定到这个上下文对象。对象属性引用链中,只有最后一层在调用中起作用
调用位置的上下文对象是obj,此时应用隐式绑定,this绑定到obj上,所以this.a的值为2
var obj = {
a: 2,
foo: foo
};
function foo() {
console.log( this.a );
}
obj.foo() // 2
隐式丢失:此时foo不是作为obj的对象被调用,而是将obj.foo赋值给了bar,bar引用的是函数foo本身,所以独立调用的时候,foo函数中的this就会绑定到当前的执行上下文对象上,也就是window
var obj = {
a: 2,
foo: foo
};
function foo() {
console.log( this.a );
}
var bar = obj.foo; // 函数别名
bar() // undefined
在实际开发中,将方法作为回调传给子组件,是很常用的方法,参数传递就是一种隐式赋值,所以在回调函数中如果使用了this,那么如果不把回调函数绑定当前的this,就会出现隐式丢失
实际应用中的隐式丢失举例
class MyComponent extends React.Component {
this.a = 2;
// 这样的写法如果在方法中调用了this,传递给组件作为回调函数,
// 就会出现隐式丢失,因为函数传递下去,是保持的对函数的引用
testFunc(){
console.log(this.a)
}
render(){
return (
<div>
<Button onClick={this.testFunc}>点击事件</Button>
</div>
)
}
}
所以才会有两种方法来避免隐式丢失的问题
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.state = {};
// 方法1:通过bind强制绑定当前的this
this.testFunc = this.testFunc.bind(this);
}
// 方法2:通过箭头函数绑定当前的this
testFunc = ()=> {
console.log(this.a)
}
render(){
return (
<div>
<Button onClick={this.testFunc}>点击事件</Button>
</div>
)
}
}
注意
class Foo {
constructor(name){
this.name = name
}
display(){
console.log(this.name);
}
}
var foo = new Foo('Saurabh');
foo.display(); // Saurabh
//下面的赋值操作模拟了上下文的丢失。
//与 实际在 React Component 中将处理程序作为 callback 参数传递 相似。
var display = foo.display;
display(); // TypeError: this is undefined
按照默认绑定的规则,在非严格模式运行代码,this 的值会指向全局对象,在严格模式 this 是 undefined。
类声明和类表达式的主体以 「严格模式」 执行,即构造函数、静态方法和原型方法。Getter 和 setter 函数也在严格模式下执行。
而 import 的模块,默认也是用「严格模式」
显示绑定
通过call(...)和apply(...)方法。第一个参数是一个对象,在调用函数时将这个对象绑定到this,call和apply的区别就是传入的其他参数不同,另外第一个参数也是可选的
call除了第一个参数以外,可以接收一个参数列表,apply只接受一个参数数组
function foo(name,age) {
console.log(name);
console.log(age);
console.log(this.value);
}
let obj = {
value: 'value'
};
foo.call(obj,'sam',666); // 调用foo时强制把foo的this绑定到obj上
foo.apply(obj,['sam',666]); // 调用foo时强制把foo的this绑定到obj上
如果传入了原始值来当做this的绑定对象,这个原始值会转化成它的对象形式,也就是new String()、new Boolean()、new Numbe(),通常被称为装箱
另外 如果把 null 或 undefined 作为this的绑定对象传入 call、apply、bind ,这些值在调用时会忽略,实际应用的是默认绑定规则
硬绑定
类似借用构造函数
function foo() {
console.log( this.a );
}
var obj = {
a: 2
};
var bar = function() {
foo.call( obj );
};
bar(); // 2
// 硬绑定的bar不可能再修改它的this
bar.call( window ); // 2
call和apply不能解决绑定丢失的问题,所以和call和apply类似的方法还有个bind,bind传入的第一个参数也是需要绑定的对象,只是该方法是返回一个函数,这个方法是内置在ES5上的Function.prototype.bind中,bind会返回一个硬绑定的函数
function 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 3
console.log( b ); // 5
api绑定的上下文,JS中也有很多内置函数,会提供一个可选参数,就是传入当前的上下文对象,用于确保回调函数绑定指定的this,实际上就是通过call和apply实现的绑定
function foo(el) {
console.log( el, this.id );
}
var obj = {
id: "awesome"
}
var myArray = [1, 2, 3]
// 调用foo(..)时把this绑定到obj
myArray.forEach( foo, obj );
// 1 awesome 2 awesome 3 awesome
new绑定
- 在JS中,构造函数只是使用new操作符被调用时的普通函数,JS没有传统意义上类的概念
- 包括内置对象函数在内的所有函数都可以用new来调用,被称为构造函数调用
- 理论上是实际上不存在“构造函数”,只存在函数的“构造调用”,只是我们习惯以面向对象的构造函数来称呼这种调用方式
使用new的过程 1、创建一个新对象 2、对这个对象执行原型连接 3、新对象绑定到函数调用的this 4、如果没有返回其他对象,则自动返回这个创建的新对象
手写模拟new的调用
function myNew(){
// 取出传入的构造函数
const Con = [].shift.call(arguments)
// 创建一个新的对象,并设置对象原型,利用Object.create可以直接实现
let obj = Object.create(Con.prototype);
// 进行this绑定,传入剩余参数
let result = Con.apply(obj,arguments);
// 如果函数有返回对象,则返回函数的对象,如果没有则返回new方法内构造的对象
return result instanceof Object ? result : obj;
}
// 实际使用
function Parent(){}
// 原始的new
const obj = new Parent(...);
// 自己模拟的new调用
const obj2 = myNew(Parent,...)
这里有的地方的写法是直接通过创建一个obj,然后修改obj.__proto__来实现的,ES5新增了Object.create方法,可以让我们在新增对象的时候,同时指定新增对象的原型,这里就应该用这个方法实现,因为__proto__的标准化是在ES6实现的,而且并不推荐直接访问,如果要获取或者修改原型,有Object.getPrototypeOf()和Object.setPrototypeOf()可以实现
箭头函数
箭头函数不应用上述任何规则,而是根据最近的非箭头函数的当前作用域来决定this,箭头函数没有自己的this,所以必须根据作用域链往外层查找,直到找到了一个绑定了this的函数作用域,并指向该函数作用域的this
箭头函数绑定中,this指向外层作用域,并不一定是第一层,也不一定是第二层。 因为没有自身的this,所以只能根据作用域链往上层查找,直到找到一个绑定了this的函数作用域,并指向调用该普通函数的对象。
使用箭头函数和function函数的区别
- 箭头函数,方法会声明在每个类的实例上
- function函数,方法会声明在class的原型上,class的实例可以在原型上找到这个方法并复用
- 箭头函数没有arguments对象,不可以使用yield命令,箭头函数不能用作Generator函数
- 不可以使用new调用,因为没有自己的this,无法使用call,apply,没有prototype属性
如果在constructor中使用bind绑定函数,则还是会在每个实例上创建新的函数 总的说来,箭头函数和非箭头函数,确实会有一些性能差异,不考虑this指向的情况下,可以在原型上得到这个函数的声明,但是在考虑使用this的情况下,在constructor中的bind声明,和使用箭头函数没有什么大的区别,但是使用箭头函数,简介轻便的语法,可以减少编码人员的思考负担
this绑定的优先级
- new绑定
- 显式绑定
- 隐式绑定
- 默认绑定