this 绑定
前沿:
当一个函数调用时,会创建一个执行上下文,包含函数调用的一些信息(调用栈,传入参数,调用方式),this 指向的就是这个上下文。this 不是静态的,不是在编写的时候绑定的,而是在函数运行的时候绑定的,它的绑定和函数声明的位置没有关系,只取决于函数的调用方式。
一. 默认绑定
默认绑定通常是函数独立调用,不涉及其他绑定
- 1.1 -- 严格模式,this 指向 undefined.
"use strict"
var bar = 123;
function print(){
console.log(this); //undefined
console.log(window.bar) // 123
}
print();
- 1.2 -- 非严格模式,this 指向 window
- let,const, 声明定义变量时,存在暂时性死期,而且不会挂在在 window 对象上。
let a = "let";
const b = "const";
var bar = 123;
function print(){
console.log(this); // window
console.log(this.bar); //123
console.log(window.bar); //123
console.log(this.a); // undefined
console.log(this.b); // undefined
}
print();
-1.3 -- 对象内执行
var a = 123;
function foo(){
console.log(this.a);
}
var bar = {
a: 456,
fun(){
foo();
}
}
bar.fun() // 123
//foo 虽然是在 bar 的 fun 函数内运行,但 foo 仍然是独立运行的,所以 this 指向 window。
- 1.4 -- 函数内执行
var str = "outer";
var fun = function(){
var str = "inner";
function innerfun(){
console.log(this.str);
}
innerfun(); // outer
}
fun();
//同上
- 1.5 -- 自执行函数
自执行函数只要( js 代码)执行就会运行,并且只会执行一次。默认情况下,this 指向 window。
var str = "123";
(function(){
console.log(this.str); //123
})()
二. 隐式绑定
-
如果函数的调用是从某个对象上出发的,通俗点说就是“ XXX.func() ”这种调用模式,此时 this 指向XXX。如果存在链式调用,例如“ AAA.BBB.CCC.fun() ”,记住一个原则:this 永远指向离它最近的那个对象( CCC )。
-
只要 func 前面什么都没有, 肯定不是隐式绑定
var a = 1;
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo,
};
obj.foo(); // 2
//obj 是通过 var 定义的,obj 会挂载到 window 之上的, obj.foo() 就相当与 window.obj.foo(),这也印证了上面的原则。
三. 隐式绑定丢失
隐私绑定丢失之后,this 的指向会启用默认绑定。
- 3.1 使用一个变量存放指向一个函数的指针,直接使用这个变量执行
var a = 1;
var obj = {
a: 2,
foo: function(){
console.log(this.a)
}
}
var foo = obj.foo;
obj.foo(); //2
foo(); // 1
// foo 真正执行时,相当于直接执行堆内存里的函数,与 obj 已经没有关系了。
var otherObj = {
a: 3,
foo: obj.foo
}
otherObj.foo() // 3
// otherObj.foo 指向 obj.foo 的堆内存,此后执行与 obj 无关(除非使用 call/apply 改变 this 指向)。
- 3.2 函数作为参数传递
var foo = funcion(){
console.log(this.a);
}
var doFoo = function(fn){
console.log(this);
fn();
}
var a = 1;
var obj1 = {
a: 2,
foo,
};
doFoo(obj1.foo); // window 1
//1.找出形参和变量声明,值赋予 undefined,2.将实参的值赋予形参。
//obj1.foo 作为实参,将指向 foo 地址的指针复制给形参 fn,此时 fn 执行已经和 Obj1 不会产生任何关系了,fn 为默认绑定
var obj2 = {
a: 3,
doFoo,
}
obj2.doFoo(foo); // obj2 1
//obj2.doFoo 符合 XXX.fn 格式(隐式绑定),this 为 obj2
//(同上)
- 3.3 回调函数
var name = "zhao";
var introduce = function(){
console.log(this);
console.log(this.name);
}
var info = {
name: "qian",
introduce1: function(){
setTimeout(function(){
console.log(this);
console.log(this.name);
}, 0)
},
introduce2: function(){
setTimeout(()=>{
console.log(this);
console.log(this.name);
}, 0)
}
}
const Mary = {
name: "Mary",
introduce,
};
const Lisa = {
name: "Lisa",
introduce,
};
info.introduce1(); //window zhao
// setTimeout的第一个参数是回调函数,this 是默认绑定,指向 window
info.introduce2(); //info qian
// setTimeout的第一个参数是回调函数,this 是默认绑定,但是是一个箭头函数,它本身没有 this,如果函数内用到 this,则取决于它的上层作用域,introduce2 函数又属于 xxx.fn,所以。。。
setTimeout(Mary.introduce, 100); //window zhao
// setTimeout的第一个参数是回调函数,this 是默认绑定,指向 window
setTimeout(function(){
Lisa.introduce();
}, 100) // Lisa "Lisa"
//符合 xxx.fn 模式
四. 显示(硬)绑定
fn.call(null) 或者 fn.call(undefined) 都相当于 fn()
- 4.1 -- call
强行改变函数的 this 的指向,参数为若干列表,函数直接执行。
Function.propotype.call = function(){
let contxt = Object(arguments[0]) || window;
contxt.fn = this;
let args = [];
for(let i = 1; i < arguments.length; i++){
args.push(arguments[i])
}
let result = contxt.fn(...args); // xxx.fn, this 指向第一个参数
delete contxt.fn;
return result;
}
let name = "call"
let obj = {
name: "123",
}
function foo(){
console.log(this.name)
}
foo.call(obj) // "123"
- 4.2 -- apply
强行改变函数的 this 的指向,参数数组列表,函数直接执行。
Function.prototype.apply = function(contxt, args){
var contxt = Object(contxt) || window;
contxt.fn = this;
let result;
if(args) {
result = contxt.fn(...args); // xxx.fn, this 指向第一个参数
}else {
result = contxt.fn();
}
delete contxt.fn;
return result;
}
let name = "apply"
let obj = {
name: "123",
}
function foo(){
console.log(this.name)
}
foo.apply(obj) // "123"
- 4.3 -- bind
强行改变函数的 this 的指向,参数是一个列表,返回一个新的函数。
//方法一
Function.prototype.bind = function(){
var contxt = Object(arguments[0]) || window;
contxt.fn = this;
var args = [];
for(let i = 1; i < arguments.length; i++){
args.push(arguments[i])
}
return function(){
contxt.fn(...args, ...arguments);
}
}
var name = "bind";
var obj = {
name: "123",
}
function foo(){
console.log(this.name);
}
var fun = foo.bind(obj);
fun(); // "123"
//方法二
Function.prototype.myBind = function(){
var first = [].shift.apply(arguments, arguments);
var fn = this;
var args = [...arguments];
return function(){
fn.apply(first, [...args, ...arguments])
}
}
//求最小值
const arr = [2,3,5,1,5,6];
Math.min.apply(null, arr);
四. new 绑定
使用 new 来构建函数,会执行如下四部操作:
- 创建一个新的空对象,即 {}。
- 为空对象添加属性 proto , 将该属性链接至构造函数的原型对象。
- 执行构造函数,并将新对象作为 this 的上下文。
- 如果结果不是对象,则返回这个新对象。
function Foo() {
getName = function () {
console.log(1);
};
return this;
}
Foo.getName = function () {
console.log(2);
};
Foo.prototype.getName = function () {
console.log(3);
};
getName(); // 5 函数声明提升
var getName = function () {
console.log(4);
};
getName(); // 4
function getName() {
console.log(5);
}
console.log(new Foo());
Foo.getName(); // 2
getName(); // 1 执行了new Foo, getName 重新赋值了
Foo().getName(); // 1 window 上面挂载的 getName 方法发生了替换
getName(); // 1 同上
let foo1 = new Foo.getName(); // 2
console.log(foo1.constructor); // Foo.getName
// 首先从左往右看: new Foo 属于不带参数列表的 new(优先级19),Foo.getName 属于成员访问(优先级20), getName() 属于函数调用(优先级20),同样优先级遵循从左向右执行
// 1. Foo.getName 执行,获取到 Foo 上的 getName 属性
// 2. 此时原表达式为 new (Foo.getName)(), new (Foo.getName) 为带参数列表, (Foo.getName)()属于函数调用, 从左往右执行
new Foo().getName(); // 3
// 首先从左往右看: new Foo() 属于带参数列表的 new(优先级20),Foo().getName 属于成员访问(优先级20), getName() 属于函数调用(优先级20)同样优先级遵循从左向右执行
// 先执行 new Foo(), 返回一个以 Foo 为实例的构造函数,
// Foo 实例上没有 getName 方法, 沿原型链找到 __proto__ 找到 Foo.prototype.getName 方法,打印 3
let foo2 = new new Foo().getName(); // 3
console.log(foo2.constructor); Foo.prototype.getName
// 首先从左往右看,第一个 new 不带参数列表(优先级19),new Foo()带参数列表(优先级20),剩下的属性访问,函数调用优先级都是20
// new Foo() 优先执行, 返回一个以 Foo 为构造函数的实例
// 执行 new (new Foo()).getName(), 返回一个以 Foo.prototype.getName 为构造函数的实例,打印 3
五. 箭头函数
-
箭头函数没有 this, 它里面的 this 取决于它的外层作作用域。此时 this 不取决于它执行时的环境,而取决于它定义时的环境。
-
不能通过显示绑定改变 this 的指向。箭头函数的底层逻辑就是绑定了它的外层作作用域,即 bind(外层作作用域)。
-
应该避免用箭头函定义对象方法,事件中的回调函数,DOM事件的回调函数中 this 已经封装指向了调用元素,如果使用箭头函数,会是其 this 指向 window 对象。
// 当 new 碰上箭头函数时
function User(name, age) {
this.name = name;
this.age = age;
this.intro = function () {
console.log("My name is " + this.name);
};
this.howOld = () => {
console.log("My age is " + this.age);
};
}
User.prototype.howDo = () => {
console.log("My do " + this.age);
};
var name = "Tom",
age = 18,
zc = new User("ZC", 28);
zc.intro(); // "My name is ZC"
zc.howOld(); // "My age is 28"
// 箭头函数指向 User 函数定义时的this,外层作用域为 User, this 指向 zc
zc.howDo(); // "My do 18"
压轴题
var number = 5;
var objNum = {
number: 3,
fn: (function () {
var number;
this.number *= 2;
number = number * 2;
number = 3;
return function () {
var num = this.number;
this.number *= 2;
console.log(num);
number *= 3;
console.log(number);
};
})(),
};
var myFun = objNum.fn;
myFun.call(null);
objNum.fn();
console.log(window.number);
//10, 9, 3, 27, 20
/*
1, obj.fn 为立即执行函数: 默认绑定, this 指向 window
此时的 obj 可以类似的看成以下代码(注意存在闭包)
var objNum = {
number: 3,
fn: function () {
var num = this.number;
this.number *= 2;
console.log(num);
number *= 3;
console.log(number);
};
var number: 立即执行函数的 AO(临时作用域) 中添加 number 属性,值为 undefined
this.number *= 2: windo.number = 10
number = number * 2: 立即执行函数的 AO(临时作用域)中 number 值为 undefined, number 赋值 NAN
number = 3: number 赋值 3
返回匿名函数,形成闭包
2, myFun.call(null): 相当于 MyFun(), 隐式绑定丢失,MyFun 的 this 指向 woindow
var num = this.number, this指向 window,num = window.number = 10;
this.number *= 2: window.number = 20
console.log(num); 打印 10
number *= 3: 当前 AO 中没有 number 属性,沿作用域链可在立即执行函数的 AO 中查到 number 属性, 修改其值为 9, 打印 9
3, obj.fn(): 隐式绑定,fn 的 this 指向 obj
var num = this.number; this.number == obj.number = 3, num = 3;
this.number *= 2: obj.number = 3 * 2 = 6
console.log(num): 打印 3
number *= 3: 当前 A0(临时作用域)中不存在 number, 继续修改立即执行函数 AO(临时作用域)中的 number 9, number = 9 * 3 = 27;
console.log(number): 打印 27
4, console.log(window.number);
window.number = 20 打印 20
*/
六. 优先级
显示绑定 > 隐式绑定
function foo(){
console.log(this.num)
}
let obj1 = {
num: 1,
foo,
}
let obj2 = {
num: 2,
foo,
}
obj1.foo(); // 1
obj2.foo(); // 2
obj1.foo.call(obj2); // 2
obj2.foo.call(obj1); // 1
new 绑定 > 隐式绑定
function foo(str){
this.a = str
}
var obj1 = {
foo,
};
obj1.foo(2);
obj1.a; // 2
var bar = new obj1.foo(4);
bar.a; 4
new 绑定 > 显示绑定
function foo(str){
this.a = str
}
var obj = {};
var bar = foo.bind(obj);
bar(2);
console.log(obj.a) // 2
var baz = new bar(3);
console.log(obj.a) // 2
console.log(baz.a) // 3
// 显示绑定函数如果被 new 调用,就会用使用创建的 this 替换硬绑定的 this。