JavaScript设计模式之基础知识:this,call,apply

设计模式系列是笔者读《JavaScript设计模式与开发实践》时的读书笔记,其中有一些是笔者自己的理解。如果有错误的地方,希望大家帮忙指正,谢谢!

----------------------------------------------------------------------------------------------------

this 是在函数运行时基于函数的执行环境绑定的。在全局函数中,this等于window;当函数作为某个对象的方法调用时,this等于该对象。

一、基础知识

1、词法作用域

词法作用域就是定义在词法阶段的作用域。说人话,就是由写代码时的位置决定的。像函数,var、let、const声明的变量等,在函数中调用时,就是按词法作用域来查找的。词法作用域是按从里到外的顺序依次查找是否有该标识符。

例1:

function fn(){
	var a = 10;
	function fee(){
		return 50;
	}
	function foo(){
		var b = 20;
		return a + b + fee();
	}
	console.log(foo());  // 80  a:10,b:20,fee():50
}

例2:

var a = 100;
function fn(){
	var a = 10;
	function fee(){
		return 50;
	}
	function foo(){
		var b = 20;
		return a + b + fee();
	}
	return foo;
}
var obj = {};
obj.fnn = fn();
console.log(obj.fnn());  // 80 a:10,b:20,fee():50

词法作用域,不关心函数在哪儿执行,只会按照声明时的位置,从里到外的依次查找所需的函数、变量等。

2、函数的执行作用域

与执行作用域关系最紧密的就是this。执行作用域不关心函数是如何声明的,只关心在哪里执行。像上例2, 稍做一些修改:

this.a = 200;
function fn(){
	this.a = 10;
	function fee(){
		return 50;
	}
	function foo(){
		var b = 20;
		return this.a + b + fee();
	}
	return foo;
}
var obj = {
	a: 100
}
obj.fnn = fn();
console.log(obj.fnn());  // 170 this.a:100 this -> obj

这里的this指向了obj,而不是函数fn中的this。

二、this的指向,分四种情况:

1、像上例所示,作为对象的方法调用,方法中的this指向调用它的对象。

this.a = 200;
var obj = {
	a: 100,
	fn: function (){
		console.log(this.a);
	}
}
obj.fn();  // 100  this -> obj

2、作为普通函数调用:当函数以普通函数方式被调用时,this总是指向全局对象,在浏览器中即window。

this.a = 200;
var obj = {
	a: 100,
	fn: function (){
		console.log(this.a);
	}
}
var foo = obj.fn;
foo();  // 200  this -> window

注:在严格模式下,this被规定不能指向全局对象,而是undefined。

3、构造器调用,即使用new运算符调用函数时,该函数总会返回一个对象,通常情况下,构造器里的this指向返回的这个对象。

var Fn = function (){
	this.name = 'fn';
}
var obj = new Fn();
console.log(obj.name);  // fn

4、使用Function.prototype.call或Function.prototype.apply、Function.prototype.bind调用,可以动态改变传入函数的this,指向第一个参数。如果第一个参数不传或者为null,则指向window。

this.a = 'window';
var obj = {
	a: 'obj',
	fn: function (){
		console.log(this.a);
	}
}
var newObj = {
	a: 'newObj'
}
obj.fn();  // obj 
obj.fn.call();  // window
obj.fn.call(null);  // window
obj.fn.call(newObj);  // newObj

三、call、apply、bind的用途及实现

1、用途:

call、apply、bind都是在Function的原型上定义的方法,其中call和apply是ECMAScript3中定义的,bind是ECMAScript5中定义的。这三者都是用来改变函数体内this对象的指向,具体使用时有所不同:

call和apply的第一个参数相同,都是代表函数体内的this指向,除了第一个参数外,call还会接受一个参数列表,apply则是接受一个参数数组,作为参数传为被调用的函数;

bind 接受的参数与call相同,区别在于call和apply会直接执行该函数,而bind 会返回一个函数,该函数的this指向是传入的第一个参数。

var obj = {
	a: 1,
	fn: function (){
		console.log(this.a + arguments[0] + arguments[1]);
	}
}
var newObj = {
	a: 10
}
obj.fn.call(newObj, 20, 30);  // 60
obj.fn.apply(newObj, [20, 30]);  // 60
obj.fn.bind(newObj, 20, 30)();  // 60

在实际项目中有可能会遇到内部函数执行时this改变的问题:

document.getElementById('box').onclick = function (){
	console.log(this.id);  // box
	var fn = function (){
		console.log(this.id);
	};
	fn();  // undefined 按照函数调用时,函数内的this指向window
	fn.call(this);  // box 使用call修正this的指向
}

当然也可以在外部函数中使用变量保存this,内部函数访问该变量时,即访问的外部函数的this,这种方法使用的更加普遍:

document.getElementById('box').onclick = function (){
	var self = this;
	console.log(self.id);  // box
	var fn = function (){
		console.log(self.id);
	};
	fn();  // box
}

这两种方法各有不同的适用场景,可以按个人的习惯和场景来选择。

另外,还可以借用其他对象的方法,最常用的是操作类数组时,借用Array.prototype的方法:

(function (){
	console.log(arguments);  // 类数组对象:Arguments(2)
	var arg = Array.prototype.slice.call(arguments);
	console.log(arg);  // 数组对象:[1,2]
})(1,2);

2、实现:

call、apply、bind虽然都是Function.prototype提供的方法,但是也可以自己用js模拟出来。尤其是bind,在低版本浏览器中不兼容,模拟出来一个使用就很方便了。

实现的重点:

1) 不传第一个参数时,默认指向window;

2) 改变this指向:设置该函数为新对象的方法,执行后再删除该方法;

Function.prototype.calls = function (){
	var args = Array.prototype.slice.call(arguments);  // 把类数组转为数组
	var context = args.shift() || window;  // 取第一个参数为this指向的新对象,如果不存在,则为window,并且args会改变,删除了第一个参数

	context.fn = this;  // 给该对象添加方法fn,并赋址this,即调用calls方法的函数

	var res = context.fn(...args);  // 执行该方法,传入参数,并获取返回值,此时args已删除了第一个参数

	delete context.fn;  // 删除该方法

	return res;  // 返回方法执行的返回值
}	

var obj = {
	name: 'this is obj'
}
function fn(a, b){
	console.log(a + b);  // 3
	return this.name;  // this is obj
}
fn.calls(obj, 1, 2); // obj为this新指向的对象,1,2为fn的参数a,b

apply的实现方法与call基本类似,只是把函数的参数改为了数组:

Function.prototype.applys = function (){
	var args = Array.prototype.slice.call(arguments);
	var context = args.shift() || window;
	var res;

	context.fn = this;

	// 区分是否有第二个参数
	if(args.length > 0){
		res = context.fn(...args[0]);
	}else{
		res = context.fn();
	}

	delete context.fn;

	return res;
}	
var obj = {
	name: 'this is obj'
}
function fn(a,b,c){
	console.log(a + b + c);  // 6
	return this.name;  // this is obj
}
fn.applys(obj, [1,2,3]);  

bind的区别是,会返回一个函数,可以参考这种实现方式:

Function.prototype.binds = function (){
	var self = this;
	var args = Array.prototype.slice.call(arguments);
	var context = args.shift();

	return function () {
		return self.apply(context, args.concat(Array.prototype.slice.call(arguments)));  // 把两个函数的参数合并
	}
}	
var obj = {
	name: 'this is obj'
}
function fn(a,b,c,d){
	console.log(a + b + c + d);  // 10
	return this.name;  // this is obj
}
fn.binds(obj, 1,2)(3, 4);

备注:ES6中的箭头函数没有this,它引用了定义时所在函数的this对象,因为其没有自己的this对象,所以不能用作构造函数,也无法使用call、apply、bind来改变this指向。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值