设计模式系列是笔者读《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指向。