this + call + apply + bind【JS深入知识汇点5】
误解
this 机制在 javascript 中是动态绑定,或称为运行期绑定的。这就导致 JS 中的 this 关键字会有多重含义,所以会给我们造成一误解。学习 this 的第一步是明白this既不指向函数自身也不指向函数的词法作用域。
指向自身
人们容易把 this 理解成指向函数自身,看下面的代码
function foo(num) {
console.log("foo: " + num
this.count++;
}
// 这里为 foo 添加了一个属性 count,初始化为 0
foo.count = 0;
for(var i = 0; i < 10; i++) {
if (i > 5) {
foo(i);
}
}
// foo: 6
// foo: 7
// foo: 8
// foo: 9
console.log(foo.count); // 0
// foo 确实被调用了4次,但是 foo.count 还是 0,
// 是因为函数内部的 this 并不是指向那个函数对象,
// 所以虽然属性名相同,但是根对象却不相同
指向它的作用域
this指向函数的作用域,这个问题比较复杂,因为有时它是正确的,有时它的错误的。但可以明确的是:this在任何情况下,都不指向函数的词法作用域。
function foo() {
var a = 2;
this.bar();
}
function bar() {
console.log(this.a);
}
foo();
结果是 undefined ,因为这里通过 this.bar() 来引用 bar 函数,这里能调用成功是因为:
- foo()被调用时,this === window,所以可以调用 bar 函数。
- bar执行时,this === window,使用对象属性访问规则,没有找到 a,所以返回undefined。
this 解析
javascript中,this 是在运行时进行绑定的,它的上下文取决于函数调用时的各种条件。
绑定规则
先根据把绑定的优先级抛出结论,按照以下顺序进行判断:
-
函数是否在 new 中调用(new 绑定)?如果是的话,this 绑定的是新创建的对象。
var bar = new Foo()
-
函数是否通过 call、apply(显式绑定)或者硬绑定调用?如果是的话, this 绑定的是指定的对象。
var bar = foo.call(obj2)
-
函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上下文对象。
var bar = obj1.foo()
-
如果都不是,使用默认绑定,严格模式下,绑定到 undefined,否则绑定到全局对象。
var bar = foo()
new 绑定
在传统的面向类的语言中,构造函数是类的一些特殊方法,使用 new 初始化类时,会调用类中的构造函数。
首先,我们需要澄清的一个误解是:在JS中,是没有构造函数的,在普通的函数调用前面加 new 关键字之后,就会把这个函数调用变成一个“构造函数调用”。实际上,new 会劫持所有普通函数并用构造函数的形式来调用它。
那么在JS中,使用 new 来调用函数,var obj = new Foo()
,会执行以下操作:
-
首先创建一个全新的空对象。
var o = new Object()
-
将空对象的原型 [[prototype]] 赋值为构造函数的原型.
o.__proto__ = Foo.prototype
-
更改 this 的指向。
Foo.call(o)
-
若 return 有值,并且值是对象,则直接返回此对象,否则,返回新创建的对象 o。
现在看一段简单的代码检测一下学习成果
function foo1(a) {
this.a = a;
}
function foo2(name) {
this.name = name;
return {
w:1
};
}
foo2.prototype.getName = function() {
return this.name;
};
var bar = new foo1(2);
console.log(bar.a) // 2
var a = new foo2('hh');
a.getName();
// Uncaught TypeError: a.getName is not a function
// 这是因为,foo2 有返回值,并且是一个对象,所以a值是 {w: 1}
显式绑定和硬绑定(call + apply + bind)
如果想调用函数时,强制把函数的 this 绑定到 obj 上,可以使用 call(...) 和 apply(...) 方法,它们的第一个参数一个对象,是给 this 准备的,这称之为显式绑定。
apply 和 call 的作用一模一样,只是传参的形式有区别而已。
apply + call
apply 方法传入两个参数:一个是作为函数上下文的对象,另外一个是作为函数参数
所组成的数组
call 方法传入两个参数:一个是作为函数上下文的对象,另一个是参数列表
两者的区别是:传入的第二个参数不同,apply 是传入带下表的集合,数组或类数组。call 从第二个开始传入的参数是不固定的,都会传给函数做参数。
call 的性能比 apply 好一些
apply 和 call 的用法
- 改变 this 指向
function foo() {
console.log(this.a)
}
var obj = {
a: 2
};
foo.call(obj) //2
⚠️注意:如果传入了一个原始值(布尔、数字、字符串等类型)来当作 this 的绑定对象,这个原始值会被转换成它的对象形式(也就是 new Boolean(...)、new Number(...)、new String(...))。
- 借用别的对象的方法
var Person1 = function () {
this.name = 'linxin';
}
var Person2 = function () {
this.getname = function () {
console.log(this.name);
}
// Person1的 this 变成了 Person 2 的this,并执行了一遍 Person1
Person1.call(this);
}
var person = new Person2();
person.getname();
- 调用函数 apply、call 方法都会使函数立即执行,所以也可以用来调用函数。call和apply没有参数,指向的是window。bibi
bind
bind()会创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,其余参数将作为新函数的参数,供调用时使用。
call/apply 与 bind 的区别是:call、apply将立即执行该函数,bind 不执行函数,只返回一个可供执行的函数。
硬绑定就是显式绑定的一个变种,用于我们将 this 被永久绑定到 obj 的 foo 函数,这样我们就不必每次调用 foo 都在尾巴上加上 call 那么麻烦。
function foo() {
console.log(this.a)
}
var obj = {
a: 2
};
var bindFunc = foo.bind(obj)
bindFunc(); //2
var foo = {
value: 1
};
function bar(name, age) {
console.log(this.value);
console.log(name);
console.log(age);
}
var bindFoo = bar.bind(foo, 'daisy');
bindFoo('18');
// 1
// daisy
// 18
bind的模拟实现
console.log({}) // 在 prototype.constructor.prototype 上有 bind 函数
console.log(typeof Function.prototype.bind) // 'function'
console.log(typeof Function.prototype.bind()) // 'function'
console.log(Function.prototype.bind.name) // 'bind'
console.log(Function.prototype.bind().name) // 'bound'
所以可得出结论:
- bind 是 Function.prototype 的一个属性,每个函数都可以调用它
- bind 是一个函数名为 bind 的函数,返回值也是一个函数,函数名是 bound。
var obj = {
name: '程序媛兔纸'
}
function original(a, b) {
console.log(this.name)
console.log([a, b]);
return false;
}
var bound = original.bind(obj, 1)
var boundResult = bound(2) // '程序媛兔纸' [1, 2]
console.log(boundResult) // false
console.log(original.bind.name) // 'bind'
console.log(original.bind.length) // 1
console.log(original.bind().length) // 2,返回 original 函数的形参个数
console.log(bound.name) // 'bound original'
console.log((function(){}).bind().name) // 'bound '
console.log((function(){}).bind().length) // 0
可以得出结论:
- 调用 bind 函数的 this 指向 bind() 函数的第一个参数
- 传给 bind() 的其他参数接收处理了,bind() 返回的函数的参数也接收了,也就是合并处理了。
- bind() 后的 name 为
bound + 空格 + 调用 bind 的函数名
,如果是匿名函数,name 是bound + 空格
- bind() 的返回值,执行结果是原函数的返回值
- bind 函数的形参是1,bind()的形参根据原函数的形参个数确定
所以我们先试着实现一个 bindFn
Function.prototype.bindTn = function(thisArg) {
var self = this;
var args = Array.prototype.slice.call(arguments, 1)
return function bound() {
var params = Array.prototype.slice.call(arguments)
self.apply(thisArg, args.concat(params))
};
}
后续的new那部分的...过于复杂,我就不继续下去了,已经写完的够我现阶段使用
隐式绑定
当一个函数被一个对象调用时,会把函数调用中的 this 绑定到这个上下文对象中。
fucntion foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
};
obj.foo() //2
⚠️注意:当我们使用隐式绑定规则时,要注意下面两点:
- 在参数传递、赋值时,隐式绑定的函数可能会丢失绑定对象,也就是会应用默认绑定规则。
function foo() { console.log(this.a); } function doFoo(fn) { fn(); } var obj = { a: 2, foo: foo }; var a = 'oops, global'; // obj.foo 引用 foo 函数本身,应用默认绑定 doFoo(obj.foo); // oops, global var bar = obj.foo; // bar 引用 foo 函数本身,应用默认绑定 bar(); // oops, global
- 对象属性引用链中只有最后一层在调用位置中起作用。
默认绑定
当直接使用不带任何修饰的函数引用进行函数调用时,只能使用默认绑定,无法应用其他规则。在严格模式下,this 会绑定到 undefined,在非严格模式下, this会绑定到全局对象。
如果把 null 或者 undefined 作为 this 的绑定对象传入 call、apply或者 bind,会应用默认绑定规则。
箭头函数
箭头函数根据外层(函数或全局)作用域来决定 this。
var obj = {
age : 20,
say : () => {
alert( this.age )
}
}
obj.say(); // undefined 因为obj对象不能产生作用域,所以箭头函数相当于定义在全局作用域,this指向全局
function foo() {
return (a) => {
// 继承自 foo()
console.log(this.a)
};
}
var obj1 = {
a: 2
};
var obj2 = {
a: 3
};
var bar = foo.call(obj1);
// 这是因为箭头函数根据外层的作用域来决定this,此处的语句只能让函数执行
bar.call(obj2) // 2
好题练习
练习题1:
function Foo() {
getName = function() {
console.log(1);
};
return this;
}
// 静态方法赋值
Foo.getName = function() {
console.log(2);
};
Foo.prototype.getName = function() {
console.log(3);
};
var getName = function() {
console.log(4);
};
function getName() {
console.log(5);
}
Foo.getName(); //2
// 变量声明提升,赋值语句留在原地,所以结果是4
getName(); //4
// 在Foo函数中,重写了全局作用域下的 getName 函数
Foo().getName(); //1
getName(); //1
// 等价于new (Foo.getName)(), 运算符优先级:成员访问 > new(不带括号) > 函数调用
new Foo.getName(); //2
// 等价于 (new Foo()).getName(),运算符优先级:成员访问、new(带括号)的优先级一样,所以从左到右执行。
// 使用 new 实例 Foo 后生成的实例上,只有原型方法,没有静态方法
new Foo().getName();
练习题2:
var title = 'world';
var a = {
title: 'hello',
alias: this.title,
show() {
console.log(this.title, this.alias)
},
ashow: () => {
console.log(this.title, this.alias)
}
}
a.show(); // hello world
var b = a.show;
b(); // world undefined
a.ashow(); // world undefined
练习题3:
var b = 20;
var a = {
b: 15,
fn: function() {
var b = 30;
return function() {
return this.b;
};
}
};
// 未指定函数的调用者的话,this就绑定到全局对象上。
console.log(a.fn()()); // 20
练习题4:
var b = 20;
var a = {
b: 15,
fn: function() {
var b = 30;
return function() {
return this.b;
};
}
};
// 未指定函数的调用者的话,this就绑定到全局对象上。
console.log(a.fn()()); // 20
练习题5:
var a = 'javascript';
var obj = {
a : 'php',
prop : {
getA : function() {
return this.a;
}
}
}
console.log(obj.prop.getA());
var text = obj.prop.getA;
console.log(text());
// undefined javascript
练习题6:
var x = [typeof x , typeof y][1]; // undefined
console.log(typeof typeof x); // typeof undefined 是‘undefiend’,typeof ‘undefiend’ 是 string
练习题7:
var obj1 = {
name: "blue",
fn: function(){
console.log(this.name);
}
};
var newFunction = obj1.fn;
newFunction();
// 结果是空,因为window有name属性,是空,如果是别的变量,就可能是 undefined
练习题8:
var age = 38;
var obj = {
age:18,
getAge: function () {
console.log(this.age);
function foo(){
console.log(this.age);
}
foo();
}
}
obj.getAge(); // 18 38
练习题9:
var length = 10;
function fn() {
console.log(this.length);
}
var arr = [fn, "222"];
fn(); // 10
// arr[0]方式的调用,使用隐式绑定的方式,this就是arr
arr[0](); // 2
练习题 10:
var length = 10
function fn() {
console.log(this.length)
}
var obj = {
length: 5,
method: function (fn) {
fn();
arguments[0]();
}
}
obj.method(fn, 10, 5, 2, 6);
// 10
// 5 是因为 arguments[0]() 时,使用隐式绑定的方式,此时 arguments 是[fn, 10, 5, 2, 6] ,arguments.length 是 5
转载自:https://juejin.cn/post/6844904164129013774