关于this, call, apply, bind 的知识点
总结了this, call, apply, bind 的知识点。参考的文章有 阮一峰,冴羽的相关文章还有《JavaScript高级程序设计》。主要是相关知识的总结
目录:
一、对this的理解
二、总结this在不同场合的指向以及需要注意的问题
三、绑定this的方法call,bind, apply
四、手写 call
五、手写apply
六、手写bind
七、箭头函数中的this
八、this指向的题目练习
一、对this的理解
this
就是属性或方法“当前”所在的对象,由于对象的属性或者方法可以复制给另一个对象,所以属性或方法“当前”所在的对象是可变的。所以this
的指向也是可变的。
this
的设计是和内存里面的数据结构有关系的。
1、当我们定义个变量时:
var obj = { name: ‘zz’};
JavaScript
引擎会先在内存里面生成一个对象 {name: ‘zz’}
。 然后把内存的地址分配给变量obj
。所以obj
上存储的实际是一个地址。当我们需要读取 obj.name
时,会先从obj
拿到地址,然后在把name
值取出来。
大家对变量的描述应该有一些认识。在内存中保存变量的形式是。我们读取的就是保存在 [[vaule]]
上的值
{
name: {
// 值
[[value]]: ‘zz’,
// 是否可写
[[writable]]: true,
// 是否可枚举
[[enumerable]]: true,
// 是否可配置
[[configurable]]: true
}
}
复制代码
2、当我们定义的是个函数时:
JavaScript
引擎会将函数单独保存在内存中,然后再把函数的地址赋值给name
属性的value
{
function f () {
console.log(this)
};
name: {
// 值
[[value]]:函数的地址 f,
…
}
}
复制代码
当f()
单独执行时 会有一个运行环境. 当 obj.name()
执行时obj
就是f
的运行环境。 所以在函数体内引用当前环境其他变量时,根据运行环境的不同,取到的值也是不同的。
var f = function () {
console.log(this.name);
}
var name = ‘zz’;
var obj = {
name: ‘dd’,
f: f
}
f() // this指向的是window, this.name zz
obj.f() // this指向的是obj,this.name dd
复制代码
由此才会有那句:函数(非箭头函数)中的this
永远指向最后调用它的那个对象。
二、总结this在不同场合的指向
1、全局环境: 执向顶层对象 window
function f() {
console.log( this === window);
}
f();
复制代码
2、构造函数: this指向的实例对象
var Persion = function (name) {
this.name = name;
}
var p = new Persion(‘zz’);
p.name // zz
复制代码
3、对象的方法
如果对象的方法里面包含this
,this
的指向就是方法运行时所在的对象。该方法赋值给另一个对象,就会改变this
的指向。
var obj ={
foo: function () {
console.log(this);
}
};
obj.foo() // obj
// 情况一
(obj.foo = obj.foo)() // window
// 情况二
(false || obj.foo)() // window
// 情况三
(1, obj.foo)() // window
复制代码
解析:
JavaScript
引擎内部,obj
和obj.foo
储存在两个内存地址,称为地址一和地址二。obj.foo()
这样调用时,是从地址一调用地址二,因此地址二的运行环境是地址一,this
指向obj
。
但是,上面三种情况,都是直接取出地址二,赋值后进行调用,这样的话,运行环境就是全局环境,因此this
指向全局环境。
// 情况一
(obj.foo = function () {
console.log(this);
})()
// 等同于
(function () {
console.log(this);
})()
复制代码
// 情况二
(false || function () {
console.log(this);
})()
// 情况三
(1, function () {
console.log(this);
})()
复制代码
如果this
所在的方法不在对象的第一层,这时this
只是指向当前一层的对象,而不会继承更上面的层。
var a = {
p: 'Hello',
b: {
m: function() {
console.log(this.p);
}
}
};
a.b.m() // undefined
复制代码
4、遇到的问题
(1)避免多层嵌套, 如果遇到嵌套,需要在上层使用一个变量固定this,然后再在内层函数调用这个变量。
var o = {
f1: function() {
console.log(this);
var that = this;
var f2 = function() {
console.log(that);
}();
}
}
o.f1()
复制代码
(2)避免数组处理方法中的this。 比如foreach, map的回调函数中 this是指向了window的,如果需要固定this,可以采用 在上层定义变量的方式,或者在foreach的第二个参数绑定this的值
var o = {
v: 'hello',
p: [ 'a1', 'a2' ],
f: function f() {
var that = this;
this.p.forEach(function (item) {
console.log(that.v+' '+item);
});
}
// 另一种方法
//this.p.forEach(function (item) {
// console.log(this.v + ' ' + item);
//}, this);
}
o.f()
复制代码
三、绑定this的方法call,bind, apply
1、Function.prototype.call()
call
方法可以指定函数内部this
的指向(函数执行时所在的作用域)
call
方法可以接受多个参数
func.call(thisValue, arg1, arg2, …)
第一个参数:this所要指向的那个对象。这里的thisValue 是一个对象,
(1)如果参数为空,null 和 undefined 默认为全局对象
(2)如果this是一个原始值,就会转换为对应的包装对象
(3)如果是正常对象,就不用做转换了
var n = 100;
var obj = {n: 200};
function a () {
console.log(this.n);
}
function f () {
return this;
}
a.call() // 100
a.call(null) // 100
a.call(undefined) // 100
a.call(obj) // 200;
f.call(5) // // Number {[[PrimitiveValue]]: 5}
复制代码
其他的参数:是函数调用时所需的参数
function add (a, b) {
return a + b;
}
add.call(this, 1, 2) // 3
复制代码
2、Function.prototype.apply()
用法和call
是类似的,区别是 接受一个数组作为函数执行时的参数。
func.apply(thisValue, [arg1, arg2,…])
3、Function.prototype.bind()
(1) bind()
将函数体内的this
绑定到某个对象,然后返回一个新函数。
bind
的第一个参数 是函数体内this
绑定的某个对象。如果第一个参数是null
,或者 undefined
,则默认将this
绑定到全局对象中。
var counter = {
count: 0,
inc: function () {
this.count++;
}
}
const func = counter.inc.bind(counter);
func() // 1
复制代码
(2)bind还可以接受更多的参数
var add = function (x, y, z) {
return x * this.m + y * this.n + z;
}
var obj = {
m: 2,
n: 2
};
var newAdd = add.bind(obj, 5);
const result = newAdd(5, 6);
console.log('result----', result);// 26
var newAdd = add.bind(obj, 5, 5);
const result = newAdd(6);
console.log('result----', result);// 26
// 这里 bind后面的参数,是给add绑定强两个参数,之后每次调用 newAdd 就只用传入第三个参数。
复制代码
(3)bind方法的注意点:每一次返回都是一个新的函数
这种方式就无法取消这个匿名函数
element.addEventListener('click', o.m.bind(o));
element.removeEventListener('click', o.m.bind(o));
复制代码
正确方法:
var listener = o.m.bind(o);
element.addEventListener('click', listener);
element.removeEventListener('click', listener);
复制代码
四、手写call方法
参考文章:segmentfault.com/a/119000000…
call方法的作用和参数:
(1)call
改变了this
的指向
(2)call
绑定的方法执行了
(3)call
可以携带参数
模拟的步骤
(1) 将函数设为绑定对象(可以为null,undefined
)的属性,
(2)执行这个函数,并传递相关的参数
(3)删除这个函数
Function.prototype.call2 = function(obj) {
const context = obj || window;
context._func = this;
let args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
args.push(arguments[i]);
}
const result = context._func(...args);
delete context._func;
return result;
}
var value = 2;
var obj = {
value: 1
}
function bar(name, age) {
console.log(this.value);
return {
value: this.value,
name: name,
age: age
}
}
bar.call2(null); // 2
console.log(bar.call2(obj, 'kevin', 18));
/*
1,
{value: 1, name: kevin; 18}
*/
复制代码
五、手写apply 原理和call是样的。只在参数部分有区别
Function.prototype.apply2 = function (obj, arr) {
const context = obj || window;
context._fn = this;
var result;
if (!arr || !arr.length) {
result = context._fn();
}
else {
result = context._fn(...arr);
}
delete context._fn
return result;
}
复制代码
六、手写bind
总结下bind
: bind()
方法会创建一个新的函数,新函数被调用时,bind
的第一个参数将作为它运行时的this
之后的参数,将作为函数的参数传入。
bind
的特点: 返回一个函数, 第一个参数是新函数的运行环境 ,可以传入参数
(1)关于this的绑定部分,可以使用 call, apply来实现。
Function.prototype.bind2 = function (context) {
var self = this;
return function () {
return self.apply(context);
}
}
复制代码
(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');
复制代码
通过上面的例子,我们知道 bindFoo的参数包含两次传递的参数
Function.prototype.bind2 = function (context) {
var self = this;
// 获取bind2函数从第二个参数到最后一个参数。 bind调用时传递的参数
var args = Array.prototype.slice.call(arguments, 1);
return function () {
// 这个时候的arguments是指bind返回的函数传入的参数
var bindArgs = Array.prototype.slice.call(arguments);
return self.apply(context, args.concat(bindArgs));
}
}
复制代码
(3)bind返回的新函数,可以作为构造函数,此时bind时指定的this值会失效,但是传入的参数值时有效的。
Function.prototype.bind2 = function (context) {
// 调用bind的必须是一个函数
if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
// 做原型链继承的中转
var fNOP = function () {};
var fBound = function () {
var bindArgs = Array.prototype.slice.call(arguments);
// 如果是用的是new 调用的 bind返回的函数,判断此时this的指向。
return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
}
// 修改fBound的.prototype
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
}
复制代码
以上关于,call, apply, bind的代码手写来自于 冴羽 的文章,好文值得分享。在这里做记录标记了自己的理解
七、箭头函数中的this
创建箭头函数时,就已经确定了它的this
指向。 箭头函数内的this
指向外层的this
。
所以要想确定箭头函数this
,就必须要先知道外层this
的指向。 参考上文关于this
指向的总结
八、练习题
参考文章:
在实际的题目中,会存在各种情况。要把握住的是:
this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件
this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式
关于调用的方式:确定函数的调用位置和调用栈,确定this的绑定对象。
第一题
function foo() {
console.log(this)
console.log( this.a );
}
function doFoo() {
console.log(this)
foo();
}
var obj = {
a: 1,
doFoo: doFoo
};
var a = 2;
obj.doFoo()
复制代码
正确输出:
/*
obj:{a:1. doFoo:}
window
2
obj.doFoo 函数的 的运行环境是 obj
obj.doFoo() 内部调用的是 foo() 此时 foo 调用位置是在 doFoo中。但是foo的this绑定是默认的window
foo() 输出是 2
*/
复制代码
第二题
var a = 10
var obj = {
a: 20,
say: () => {
console.log(this.a)
}
}
obj.say()
var anotherObj = { a: 30 }
obj.say.apply(anotherObj)
复制代码
正确输出
/*
obj.say() 是箭头函数,内部的是this 此时为上一次obj的this指向 是window, 输出 10
obj.say.apply(anotherObj) 此时把 obj.say的运行环境改为了anotherObj, this对anotherObj指向的 window 输出:10
*/
复制代码
第三题
function a() {
console.log(this);
}
a.call(null);
复制代码
正确输出
window
call的第一个参数为null时,指向的是window
复制代码
第四题
var obj = {
name : 'cuggz',
fun : function() {
console.log(this.name);
}
}
obj.fun()
new obj.fun()
复制代码
正确输出
/*
obj.fun() 此时fun 函数的运行环境是 obj, 此时的this指向为obj,this.name 输出: cuggz
new obj.fun() 是取出了 obj.fun 作为构造函数,此时的this指向的是构造函数, 因为没有name 参数,输出:undefined
*/
复制代码
第五题
var obj = {
say: function() {
// var f1 = () => {
// console.log("1111", this);
// }
// f1();
const f1 = function() {
console.log("1111", this);
}
f1();
},
pro: {
getPro:() => {
console.log(this);
}
}
}
var o = obj.say;
o();
obj.say();
obj.pro.getPro();
复制代码
正确的输出
/*
var o = obj.say; 是把say函数赋值给了 o 此时 o是在全局作用域中;o() 输出: 1111 window
obj.say() 此时say的运行环境是 obj, f1 是箭头函数,指向外层say所在的运行环境 所以此时 输出: 1111 obj
如果f1不是箭头函数,那么f1 的运行环境就是 window。f1虽然在 say函数内调用,但是默认的绑定是 window
obj.pro.getPro() getPro是箭头函数,this指向外层的this,getPro的外城 pro是个对象不够成单独的作用域,所以指向了obj的运行环境window。 输出 window
*/
复制代码
第六题
var myObject = {
foo: "bar",
func: function() {
var self = this;
console.log(this.foo);
console.log(self.foo);
(function() {
console.log(this);
console.log(this.foo);
console.log(self.foo);
}());
// var fn = function() {
// console.log(this);
// console.log(this.foo);
// console.log(self.foo);
// };
// fn();
}
};
myObject.func();
复制代码
正在输出
/*
myObject.func()
this.foo 输出 bar
self.foo 此时 this和self的指向是相同的 输出 bar
立即指向函数中function中 this 指向的是window, 此时函数没有绑定到func上。 输出:undefined
self指向的是 func, 此时的 self.foo 输出 bar
*/
复制代码
第七题
window.number = 2;
var obj = {
number: 3,
db1: (function test1() {
console.log('ddd', this);
this.number *= 4;
return function test() {
console.log('return', this);
this.number *= 5;
}
})()
}
var db1 = obj.db1;
db1();
obj.db1();
console.log(obj.number);
console.log(window.number);
复制代码
正确输出
/*
obj.db1 是个立即执行函数,在解析时就立即执行了。会输出 ddd window,此时 this.number = 2 * 4 = 8;
var db1 = obj.db1; db1 指向的是 obj.db1对应的立即执行函数的返回值. 此时 return 指向的是 window
obj.db1() 调用的实际是 obj.db1的返回函数, return obj, this.number = 15
obj.number 的输出就是 15
window.number 的输出是 8
*/
复制代码
第八题
var length = 10;
function fn() {
console.log(this.length);
}
var obj = {
length: 5,
method: function(fn) {
fn();
arguments[0]();
}
};
obj.method(fn, 1);
复制代码
正常输出
/*
obj.method(fn, 1); fn是传入的函数,this 是window,输出是 10
arguments[0] => function fn() {console.log(this.length);}
此时 this 是 [ƒ, 1, callee: ƒ, Symbol(Symbol.iterator): ƒ] arguments,
所以运行环境是arguments, 输出是 2
*/
复制代码
第九题
var a = 1;
function printA() {
console.log(this.a);
}
var obj = {
a: 2,
foo: printA,
bar: function() {
printA();
}
}
obj.foo();
obj.bar();
var foo = obj.foo;
foo();
复制代码
正确输出
/*
obj.foo 存入的是 printA函数的指针。obj.foo() 此时printA的运行环境是 obj this.a 输出 2
obj.bar() 函数内部的 printA() 运行环境是 window 输出 1
foo = obj.foo 此时把 printA 函数的指针 赋值给了 foo。 运行环境是 window。 foo() 输出 1
*/
复制代码
第十题
var x = 3;
var y = 4;
var obj = {
x: 1,
y: 6,
getX: function() {
var x = 5;
return function() {
return this.x;
}();
},
getY: function() {
var y = 7;
return this.y;
}
}
console.log(obj.getX())
console.log(obj.getY())
复制代码
正确输出
/*
obj.getX() // function() { return this.x;}(); // 立即执行函数 运行环境是 window 对应的function 输出: 3
obj.getY() // 运行环境是 obj 输出:6
*/
复制代码
第十一题
var a = 10;
var obt = {
a: 20,
fn: function() {
var a = 30;
console.log(this.a)
}
}
obt.fn();
obt.fn.call();
(obt.fn)();
复制代码
正确输出
/*
obt.fn() 运行环境是 obt this.a 输出 20
obt.fn.cal() 绑定了全局环境 this.a 输出 10
(obt.fn) 是全局环境, this.a 输出 10
*/
复制代码
第十二题
function a(xx) {
this.x = xx;
return this
};
var x = a(5);
var y = a(6);
console.log(x.x)
console.log(y.x)
复制代码
正确输出
/*
a(5) 返回的 this 是window; x = 5; x 覆盖了之前x 输出:undefined
a(6) y = {x: 6} 输出:6
*/
复制代码
第十三题
function foo(something) {
this.a = something
}
var obj1 = {
foo: foo
}
var obj2 = {}
obj1.foo(2);
console.log(obj1.a);
obj1.foo.call(obj2, 3);
console.log(obj2.a);
var bar = new obj1.foo(4)
console.log(obj1.a);
console.log(bar.a);
复制代码
正确输出
/*
obj1.foo(2) 输出 obj1.a // 2 此时 obj1上添加了一个属性 a 值为2
obj1.foo.call(obj2, 3) obj2.a // 3
obj1.a // 2
bar.a // 4
*/
复制代码
第十四题
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);
复制代码
正确输出
/*
foo.bind(obj1) 是吧foo中的this替换为obj1, 返回了bar
bar(2) 给foo赋值2 obj1.a // 2
new bar(3) 构造函数,此时 foo.bind指定的this指向obj1失效。
obj1.a // 2
baz.a // 构造函数中的this.a 3
*/
复制代码