6. JavaScript this指向相关

405 阅读8分钟

this 的概念与指向

this 的概念

通常来讲,this 的值是在执行的时候才能确认的,定义的时候不能确认。因为 this 是执行上下文环境的一部分,而执行上下文需要在代码执行前确定,而不是定义的时候。所以 this 永远指向最后调用它的那个对象。

但,这只是通常来讲。apply、call、bind、箭头函数都会改变 this 的指向。

作为一个函数调用

// 情况1
function foo() {
  console.log(this.a) //1
}
var a = 1
foo()  //this -> window

作为方法调用

// 情况2
function fn(){
  console.log(this);
}
var obj={fn:fn};
obj.fn();  //this -> obj

构造函数中

// 情况3
function CreateJsPerson(name,age){
// this是当前类的一个实例p1
this.name = name;  // => p1.name=name
this.age = age;  // => p1.age=age
}
var p1 = new CreateJsPerson("尹华芝",48);

call、apply、bind

// 情况4
function add(c, d){
  return this.a + this.b + c + d;
}
var o = {a:1, b:3};
add.call(o, 5, 7);  // 1 + 3 + 5 + 7 = 16  //这里使用了 call 对 this 进行了重定向
add.apply(o, [10, 20]);  // 1 + 3 + 10 + 20 = 34  //这里使用了 apply 对 this 进行了重定向

箭头函数

// 情况5
<button id="btn1">箭头函数this</button>
<script type="text/javascript">   
    let btn1 = document.getElementById('btn1');
    let obj = {
        name: 'kobe',
        age: 39,
        getName: function () {
            btn1.onclick = () => {
                console.log(this);//obj
            };
        }
    };
    obj.getName();
</script>

箭头函数在自己的作用域内不绑定 this,即没有自己的 this,如果要使用 this ,就会指向定义时所在的作用域的 this

在 《ES6 标准入门中》:箭头函数的this,总是指向定义时所在的对象,而不是运行时所在的对象。

由于箭头函数的 this 是在定义时确定的,所以我们不能在构造函数中使用箭头函数,构造函数的 this 要指向实例才行,因此只能使用一般的函数。

总结

  • 对于直接调用 foo 来说,不管 foo 函数被放在了生命地方,this 一定是 window
  • 对于 obj.foo() 来说,我们要记住,谁调用了函数,谁就是 this,所以在这个场景下,foo 函数中的 this 就是 obj 对象
  • 构造函数模式中,类中(函数体中)出现的 this.xxx = xxx 中的 this 就是当前类的一个实例
  • call、apply、bind 的 this 是第一个参数(call 的接收和 apply 不同,apply 接收数组,而 call 则是用“,”分隔,进行接收)
  • 箭头函数没有自己的this,看其外层是否有函数,如果有,外层函数的 this 就是内部箭头函数的 this,如果没有,this 就是 window。需要注意的是:箭头函数的 this 始终指向函数定义时的 this,而非执行时。

81223756

call、apply 与 bind

call、apply、bind 本质都是改变 this 的指向,不同点 call、apply 是直接调用函数,bind 是返回一个新的函数。callapply 就只有参数上不同。

call 手写代码

call() 让函数执行,第一个参数让 this 的指向改为传进去的参数,后面的当参数传进函数里面。

返回值为原函数的返回值,如果不传第一个参数为 this 就指向 window。

ES6 版:

Function.prototype.ca112 = function (context, ...arrs ){
    context = context || window; // 因为传递过来的 context 很可能是 null
    context.fn = this;  // 让 fn 的上下文是 context
    const result = context.fn(.. .arrs); 
    delete context.fn; 
    return result; 
}

ES5 版:

Function.prototype. call2 = function (context) { 
    var context = context || window;  // 因为传递过来的 context 很可能是 null
    context.fn = this; 
    var args = [];
    for (var i = 1; i< arguments.length; i++){
        // 不这样的话,字符串的引号会被去掉,变成变量
        args.push("arguments[" + i + "]");
    }
    args = args.join(",");  // 把数组变成字符串
    
    // 相当于执行 context.fn(arguments[1], arguments[2])
    var result = eval("contest.fn(" + args + ")");
    delete context.fn;
    return result;
}

ES5 版本的使用 eval 来执行语句,这样会又一定的性能影响,但是这样做兼容性好

因为不知道会输入多少个,所以这里直接使用 arguments 来遍历好了,先把 arguments 转成数组,再转成字符串,然后利用 eval 执行代码(看见网上说 eval 有安全性问题,不过这里这样就够了。)

apply 手写代码

ES6 版:

Function.prototype.apply2 = function(context, arr) {
    context = context || window;  // 因为传递的可能是 null
    context.fn = this;  // 让 fn 的上下文成为 context
    arr = arr || [];
    const result = context.fn(...arr);
    delete context.fn;
    return result;  // 因为有可能 this 函数会有返回值
};

ES5 版:

Function.prototype.apply2 = function(context, arr) {
    var context = context || window;
    context.fn = this;
    var args = [];
    var params = arr || [];
    for(var i = 0; i < params.length; i++) {
        args.push("params[" + i + "]");  
    }
    args = args.join(",");
    
    var result = eval("contest.fn(" + args + ")");
    delete context.fn;
    return result;
}

bind 手写代码

bind 是封装了 call 的方法改变了 this 的指向并返回一个新的函数

ES6 版:

Function.prototype.bind2 = function(context, ...arrs) {
    let _this = this;
    return function() {
        _this.call(context, ...arrs, ...arguments);
    }
}

ES5 版:

Function.prototype.bind2 = function(context) {
    var _this = this;
    var argsParent = Array.prototype.slice.call(arguments, 1);
    return function() {
        var args = argsParent.concat(Array.prototype.slice.call(arguments));
        _this.apply(context, args);
    };
}

三者的同异性总结

call、apply、bind

  1. 三者都是用来改变函数的 this 对象的指向的
  2. 第一个参数都是 this 要指向的对象
  3. 都可以利用后续参数进行传参
  • 参数传递

call 方法传参是传一个或多个参数,第一个参数是指定的对象

func.call(thisArg,arg1,arf2,……)

apply 方法传参是传一个或两个对象,第一个参数是指定的对象,第二个参数是一个数组或类数组(说到类数组就想起了 arguments)

func.apply(thisArg,【argsArray】)

bind 方法传参是传一个或者多个参数,跟 call 方法传递参数一样。

func.bind(this.thisArg,arg1,arg2,arg3……)
  • 调用后是否立即执行

call apply 在函数调用它们之后,就会立即执行这个函数;

而函数调用了 bind 后,会返回调用函数的引用,如果要执行的话,需要执行返回函数的引用。

let name = 'window name';
let obj = {
    name: 'call_me_R'
};
function sayName() {
    console.log(this.name);
}

sayName();  // window name
sayName.call(obj);  // call_me_R
sayName.apply(obj);  // call_me_R

let _sayName = sayName.bind(obj);
_syaName();  // call_me_R

执行的区别在与 ball 和 apply 都是立即执行的,bind 会返回回调函数,手动执行回调函数以执行。

new 与 Object.create()

new 做的事情

New 关键字会进行如下的操作

  • 创建一个空的简单 JavaScript 对象(即 {})

  • 链接该对象(即设置该对象的构造函数)到另一个对象

  • 将创建的新的对象作为 this 的上下文

  • 如果该函数没有返回对象,则返回 this

  • new 会创建一个新的对象,并且这个新对象继承构造函数的 prototype,也就是说创建的实例的 proto 指向构造函数的 prototype

  • new Object()会创建一个实例,该实例的 proto 指向 Object 的 prototype

手写 new 四个步骤:

  1. 创建一个空对象,并且 this 变量引用该对象
  2. 继承函数的原型
  3. 属性和方法加入 this 引用的对象中,并执行函数
  4. 新创建的对象有 this 所引用,并且最后隐式返回 this
function _newfunc{
    let target = {},
    target.__proto__ = func.prototype;
    let res = func.call(target);
    iftypeof(res)=='object' || typeof(res)=='function'){
        return res;
    }
    return target;
}

简单来说:

new 做的三件事情:

  • 指定 prototype
  • 用 call 调用对象
  • 返回 this
function myNew (fun{
  return function ({
    // 创建一个新对象且将其隐式原型指向构造函数原型
    let obj = {
      __proto__ : fun.prototype
    }
    // 执行构造函数
    fun.call(obj, ...arguments)
    // 返回该对象
    return obj
  }
}

function person(name, age{
  this.name = name
  this.age = age
}

let obj = myNew(person)('chen'18// {name: "chen", age: 18}

Object.Create() 基本实现及其原理

// 思路:将传入的对象作为原型
function create(obj) {
  function F() {}
  F.prototype = obj
  return new F()
}

Object.Create 是创建了一个新的对象并返回,这个新对象的原型指向了拷贝的对象,当我们通过 b.a 访问 obj.a 时,是通过原型进行访问的。

但是要注意的是,Object.Create 并不是深拷贝,Object.Create() 新建的对象共享的是拷贝的对象的引用类型的地址(浅拷贝)。

所以如果修改的是引用类型,还是会变化。

由于 Object.create() 还可以传递第二个参数,所以更好的实现是:

function myCreate(proto, properties) {
    // 新对象
    let fn = function() {};
    fn.prototype = proto;
    if(properties) {
        // defineProperties 在新对象上定义新的属性或修改现有属性
        Object.defineProperties(fn, properties)
    }
    return new fn();
}

new 与 object.create 的异同

  • new Object() 继承内置对象 Object,Object.create 继承指定对象
  • 可以通过 Object.create(null) 创建一个干净的对象,也就是没有原型,而 new Object() 创建的对象是 Object 的实例,原型永远指向 Object.prototype

81223759

Object.create 接受两个参数,即 object.create(proto,propertiesObject)

proto:现有的对象,即新对象的原型对象(新创建的对象 proto 将指向该对象)。如果 proto 为 null,那么创建出来的对象是一个 {} 并且没有原型。

81223757

propertiesObject 可选,给新对象添加新属性以及描述器。如果没有指定即创建一个 {},有原型也有继承 Object.prototype 上的方法。可参考 Object.defineProperties()的第二个参数。

81223758

参考