js进阶——this、call和apply详解

this的指向

  • 很多情况下,经常会出现this指向概念模糊的情况!this指向和函数作用域是时间线上完全相反的两个东西,函数作用域是在你定义的时候就已经决定了归属,而this是在执行代码时,有调用的“对象”决定!

this指向的大致情况

  • this在对象内部的函数方法里调用;
  • 在普通函数的内部调用;
  • 在构造器中调用;
  • 在call、apply中调用;

在对象内部函数的方法里调用

  • 此时,this指向该对象

var obj = {
    name:"Wythe",
    getName:function(){
        console.log(this.name);
    }
};

obj.getName();   //Wythe

在普通函数内部调用

  • 此时指向的是全局,window,普通函数在注册的时候便是挂载在window下,就像window.fun(){} 一样
var name = "lucy"
var norMal = function () {
    var name = "Wythe"
    console.log(this.name)
}
norMal();   //lucy
//或者这样
var obj = {
    name:"Wythe",
    getName:function(){
        console.log(this.name);
    }
};

var getName = obj.getName
getName();   //lucy   因为执行时的this已经不是由obj调用了!
  • 比如在实际应用中我们会遇到这样一种情况,为了方便,我在这里进行简略的书写:
//
//
//  
//  
//      
// // //

如果我们提前用一个变量接收this,并传递进去就可以解决该问题!就和闭包一样,用一个变量将作用域串起来!

构造器调用

  • javascript中本没有类,搞的人多了,边成了类!神奇的是官方给了new关键字,一个类的构造器!虽然 js 中确实没有实际意义上的类,但是聪明的程序员都能从中堆砌出一条类的道路来!

所以我们用构造器这样创建对象实例:


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

  • 凡事总有例外,因为函数返回值的特殊原因,如果返回值是一个对象或者显示地返回数据的话,那么函数会忽略前面的东西,直接将对象返回给表达式里接收值的变量:
var theClass = function () {
    this.name = 'lucy'
    return {
        name: 'Bob'
    }
}
var obj = new theClass();
console.log(obj.name); //Bob

如果return 'Bob'就不会出现这种情况!

Function.prototype.call 或 Function.prototype.apply

  • call 和 apply 在函数式风格的编程中经常用于改变this的指向,达到偷天换日,斗转星移的效果!就像C类面向对象语言一样,子类继承了父类,可以使用父类的方法一样,不需要你重新定义,非常方便:

var obj1 = function () {
    this.name = 'lucy'
    this.getName = function () {
        return this.name
    }
}
var obj2 = function () {
    this.name = 'Bob'
}
var obj3 = {
    name: 'Tom'
}
var obj4 = {
    name: 'Assa',
    getName: function () {
        return this.name
    }
}
var obj1 = new obj1();
var obj2 = new obj2();
console.log(obj1.getName());            //lucy
console.log(obj4.getName.call(obj3));   //Tom
console.log(obj1.getName.call(obj2));   //Bob
console.log(obj1.getName.call(obj3));   //Tom
console.log(obj4.getName.call(obj2));   //Bob

不管是构造器还是对象数据,call都能顺利的改变this的指向,call和apply当然不止这么简单,后面部分再进行详解。

丢失this

  • 怎么会丢失this呢?我们先来思考一下,由于this是指向执行时调用的对象,那么什么情况下执行时对象会无情的改变呢?就是作为普通函数执行的时候!如果将对象的函数体赋值给了新的变量,那么这个新的函数指向会重新改变,一般是指向全局的window,因为引擎编译的时候会将全局下的变量和函数都挂载在自己的石榴裙下!

var obj = {
    name: 'Assa',
    getName: function () {
        return this.name
    }
}
var myObjGetName = obj.getName;
console.log(obj.getName()); //Assa
console.log(myObjGetName()); //undefined

obj的getName函数体赋给myObjGetName后,因为myObjGetName本身是个全局变量,挂载全局window下,所以,执行的时候this的指向不会再指向原来的obj对象。

  • 另一个问题就是js的某些原生方法的重新定义问题,比如document.getElementById,它就是一个别叫特殊的方法,我们先看一下会出什么错:
//
//
//    
//    
//        
// // //

究其原因呢是getElementById这个方法内部使用了this,本来这个this是指向document的,能够正常接入文档流,但是被赋值给变量后成为了普通函数,this指向发生了改变!

//
//
//    
//    
//        
// // //

函数自执行将document.getElementById作为参数传递进去,apply将函数体里的this强行指向了参数的document。如上例子,在getId执行时,id通过参数传递进去,由func接收,同时自执行函数接收document.getElementById并拿到对象,apply将document当成了一个this传给了getId,使得getId的this发生了改变,得以修正!

call 和 apply

  • Function.prototype.bind:这是一个用于绑定this的原生函数,大部分的浏览器都是完美实现了的,但是如果我们要自己做一个呢?
Function.prototype.bind = function (params) {
    var self = this;
    return function () {
        return self.apply(params,arguments);
    }
};

var obj = {
    name:"Lucy"
};

var func = function () {
    console.log(this.name);
}.bind(obj);

func()//Lucy
  • 这个例子其实有三个过程,第一个过程,func调用bind,并传递对象obj。bind被调用时,bind的this指向func,但是因为func本身是个普通函数,所以指向func后,被func的this指向了window;第二个过程,bind函数里,用self保存了这个指向func然后指向window的this,接收到了对象obj,return将后续内容传递回调用者func函数;第三个过程,本来被返回的是个函数体,但是内部有一个执行了的apply函数,于是,apply将预先保存好this的变量self指向了第一个参数params,并将params的属性遍历后生成的类数组arguments一并返回给了func函数,而这个params也就是obj。最终也就形成了func的this指向了obj对象的结果。

  • 总的来说,call和apply都是用于改版this的指向,使调用者的this指向第一个参数传递进去的对象,而后面的参数都是作为数据传递给调用者!区别在call的数据是分开传递,后面可以由多个参数,而apply的第二个参数是以数组或类数组的方式传递!


obj.getName.call(obj2,1,2,3,4);
obj.getName.apply(obj2,[1,2,3,4]);

call apply 借用其他对象的方法

  • 既然call和apply可以指向别的对象,那么我们某些对象想用到已经创建好的方法的时候,就可以让这个方法的this指向某些对象即可,就像继承一样,可以畅享自己没有的方法:

var A = function (name) {
    this.name = name;
}
var B = function () {
    A.apply(this,arguments)
}
B.prototype.getName = function () {
    return this.name;
}

var b = new B('Lucy');
console.log(b.getName())

实例一个B的对象,由变量b接收,传递参数‘lucy’,虽然B函数体没有设置形参接收参数,但是arguments仍然能接到数据并创建类数组;然后,A.apply将this指向apply的第一个参数this,这个this代表的是构造器的this,并将arguments的数据传递给A;紧接着,A的this(指向构造器的this,实例化后指向对象b)的name属性接收参数name的数据(由arguments传递过来),使b形成了一个属性name,值为’Lucy’;最后,b调用getName方法,返回出本身的属性name的值。

实用的Array方法

  • 在js里面有很多类似于数组但不是数组的数据,就像上面的arguments,还有哪些HtmlCollection文档元素集合等,像归像,本质上仍然不是数组,就不能够使用数组的方法!所以,我们会借用Array.prototype对象上的方法:

(function () {
    Array.prototype.push.call(arguments,3);
    console.log(arguments);
})(1,2)

/* 
Arguments(3)
    0: 1
    1: 2
    2: 3
    callee: ƒ ()
    length: 3
    Symbol(Symbol.iterator): ƒ values()
    __proto__: Object
*/
  • 但是要注意的是:如果对象调用数组的方法,因为对象本身没有length的属性,所以,可以能会因为浏览器的差异造成失败,比如说IE浏览器。
var a  = {};
Array.prototype.push.call(a,'first');
console.log(a.length,a[0])//1 'first'
var a  = 1;
Array.prototype.push.call(a,'first');
console.log(a.length,a[0])//undefined undefined

如果遇到某些不支持的浏览器,我们需要给对象等显示的赋值length属性obj.length.

[1]参考文献——JavaScript设计模式与开发实践,曾探–腾讯AlloyTeam;

你可能感兴趣的:(JavaScript学习笔记)