Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

this全面解析 #9

Open
Genluo opened this issue Aug 31, 2019 · 0 comments
Open

this全面解析 #9

Genluo opened this issue Aug 31, 2019 · 0 comments

Comments

@Genluo
Copy link
Owner

Genluo commented Aug 31, 2019

从规范来看如何确定this

this就是构建上下文的时候重要的三个之一,但是this到底是怎么确定的?首先根据规范我们将ECMAscript中的类型分为两部分,一部分是语言类型:就是开发者直接可以操作的,另一种称之为规范类型:相当于 meta-values,是用来用算法描述 ECMAScript 语言结构和 ECMAScript 语言类型的。规范类型包括:Reference, List, Completion, Property Descriptor, Property Identifier, Lexical Environment, 和 Environment Record,这里面有个重要的属性为Reference,他和this的指向有着密切的联系。这个类型是为了解释诸如delete、typeof以及赋值等操作行为。Reference由三部分组成,分别是:

  • base value : 就是属性所在的对象或者就是EnvironmentRecord它的值只可能是语言类型或者EnvironmentRecord
  • referenced name : 属性的名称
  • strict reference

那么函数调用的时候如何确定this的值:首先计算MemberExpression,将计算的结果赋值为ref,然后根据ref的情况来确定this的值,具体分了如下三种情况:

  • 如果ref是Reference,并且IsPropertyReference(ref)是true,那么this的值就是GetBase(ref)
  • 如果ref是Reference,并且base value的值是Environment Record,那么this的值为ImplicitThisValue(ref)
  • 如果ref不是Reference,那么this的值为undefined

首先计算MemberExpression的结果赋值给ref,那么什么是MemberExpression?包含下面几种:

  • PrimaryExpression // 原始表达式 可以参见《JavaScript权威指南第四章》
  • FunctionExpression // 函数定义表达式
  • MemberExpression [ Expression ] // 属性访问表达式
  • MemberExpression . IdentifierName // 属性访问表达式
  • new MemberExpression Arguments // 对象创建表达式

比如:

function foo() {
    console.log(this)
}

foo(); // MemberExpression 是 foo

function foo() {
    return function() {
        console.log(this)
    }
}

foo()(); // MemberExpression 是 foo()

var foo = {
    bar: function () {
        return this;
    }
}

foo.bar(); // MemberExpression 是 foo.bar

然后判断ref是不是一个Reference类型,按照上面提到规范进行处理。

解释下面的代码,说明当前this的指向:

var value = 1;

var foo = {
  value: 2,
  bar: function () {
    return this.value;
  }
}

//示例1
console.log(foo.bar());
//示例2
console.log((foo.bar)());
//示例3
console.log((foo.bar = foo.bar)());
//示例4
console.log((false || foo.bar)());
//示例5
console.log((foo.bar, foo.bar)());

但是函数中的this为什么执行undefined

function foo() {
    console.log(this)
}

foo(); 

在这个函数中,MemberExpression 是 foo,解析标识符,查看规范 10.3.1 Identifier Resolution,会返回一个 Reference 类型的值,类似如下这种:

var fooReference = {
    base: EnvironmentRecord,
    name: 'foo',
    strict: false
};

但是IsPropertyReference(ref)返回结果是false,所以只能走第二种情况,this的值为IsPropertyReference(ref),这个函数始终返回的undefined,所以最后this的值就是undefined。

call、apply、bind、箭头函数、new 绑定this对比

this的绑定有三种方式,分别是:

  • 默认绑定

函数在严格模式中会将this绑定为全局对象

  • 显示绑定(call,apply)
    • 硬绑定bind

首先call和apply的区别就是传入的参数不同,bind是通过call和apply来实现,所以将之称之为硬绑定

Function.prototype.myBind = function (oThis, ...params) {
    const that = this;
    if (typeof that !== 'function') {
        throw new Error(`${this} is not callable`);
    }
    function prototype() {};
    function bind (...args) {
        const isNew = this instanceof prototype;
        return that.apply(isNew ? this : oThis, [...params, ...args]);
    }
    // 引入中间函数,为了保证修改bind之后函数的原型对原函数原型不产生影响
    prototype.prototype = that.prototype;
    bind.prototype = new prototype();
    return bind;
}

// 但是在规范中为了bind之后生成的函数的原型不影响到原函数的原型,所以所以直接将新生成的函数原型设置为undefined,为了更加贴合规范,我们使用下面这种方式进行模拟
Function.prototype.myBind = function (oThis, ...params) {
  const that = this;
  if (typeof that !== 'function') {
      throw new Error(`${this} is not callable`);
  }
  function bind (...args) {
      const isNew = new.target === bind;
      if (isNew) {
        return new that(...params, ...args);
      }
      return that.apply(oThis, [...params, ...args]);
  }
  // 引入中间函数,为了修改bind之后函数的原型对原函数原型不产生影响
  bind.prototype = null;
  return bind;
}

那么如何实现call和apply函数?

Function.prototype.myCall = function(context, ...args) {
    const that = this;
    context = context || window;
    if(typeof that !== 'function') {
        throw new Error('myCall 只支持函数调用')
    }
    // 保证context不存在fn属性,如果存在将会导致报错
    context.fn = that;
    const result = context.fn(...args);
    delete context.fn;
    return result;
}
// 使用Symbol进行模拟
Function.prototype.myCall = function (context, ...args) {
  context = context || window;
  var fn = Symbol(); // added
  context[fn] = this; // changed
   
  let result = context[fn](...args); // changed

  delete context[fn]; // changed
  return result;
}
  • 隐式绑定

隐式绑定就是通过上面提到,判断this的过程

  • new 绑定

使用new的过程:

  1. 创建(或者说构造)一个全新的对象
  2. 这个新对象会执行[[原型]]连接
  3. 这个新的对象会绑定到函数调用的this
  4. 如果函数没有返回其他对象或者函数,那个new表达式中的函数调用会自动返回这个新对象

在了解到new的执行过程,我们可以手动实现一个new操作符,具体实现如下:

Function.prototype.create = function (...params) {
    const that = this;
    // 首先生成一个全新的对象
    const obj = new Object();
    // 进行原型链连接
     Object.setPrototypeOf 
            ? Object.setPrototypeOf(obj, that.prototype) 
            : obj.__proto__ = that.prototype;
    // 将obj传入作为函数进行调用
    const funReturn = that.apply(obj, params);
    if (Object.prototype.toString.call(funReturn) === '[object Object]') {
        return funReturn;
    }
    // 判断构造函数返回值是不是对象,如果不是对象返回新生成的对象
    return typeof funReturn === 'object' ? funReturn : obj;
}

下面这两种情况是对上面出现的情况进行说明:

// 情况一
function Person(name) {
    this.name = name
    return name;
}
let p = new Person('Tom');
// 情况二
function Person(name) {
    this.name = name
    return null;
}
let p = new Person('Tom');
let q = new Person; // 相当于上面传入undefined

如果构造函数没有返回值或者返回的值是非对象,那么返回的就是构造函数实例化之后的对象,typeof测试对象包含Function,Array,Date,RegExg,Error

  • 箭头函数

1、没有自己的thissuperargumentsnew.target绑定。 2、不能使用new来调用。 3、没有原型对象。 4、不可以改变this的绑定。 5、形参名称不能重复。

箭头函数中没有this绑定,必须通过查找作用域链来决定其值。 如果箭头函数被非箭头函数包含,则this绑定的是最近一层非箭头函数的this,否则this的值则被设置为全局对象

这四种方式存在优先级问题:
new 绑定>显式绑定>隐式绑定>默认绑定,new绑定和显式绑定判断如下:

这里面new和call/apply无法同时使用,所以只能通过显式绑定的变形-硬绑定来实现。

function foo(something) {
    this.a = something;
}
var obj1 = {};
var bar = foo.bind( obj1 ); bar( 2 );
console.log( obj1.a ); // 2
var baz = new bar(3); 
console.log( obj1.a ); // 2 
console.log( baz.a ); // 3

令人惊奇的是使用new竟然更改了硬绑定中的this,所以new 绑定this的优先级高于硬绑定。另一种比较有点事称之为软绑定。

Function.prototype.softBind = function (oThis, ...params) {
    const that = this;
    if (typeof that !== 'function') {
        throw new Error(`${this} is not callable`);
    }
    function bind (...args) {
        const isNeedBind = !this || this === window;
        return that.apply(isNeedBind ? oThis : this, [...params, ...args]);
    }
    // 软绑定不需要中间函数
    bind.prototype = that.prototype;
    return bind;    
}

如何绑定this

  • 使用反柯里化
  • 使用箭头函数
  • 使用代理进行绑定
  • 使用bind方法

举例

     var a = 1;
        var obj = {
            a: 2,
            c: {
                a: 3,
                b: this.a, // 这里的this并不在函数中
            },
            fn: function () {
                return this.a; // 这里的this在函数中
            }
        }
        console.log(obj.c.b);//1 node中值为undefined
        console.log(obj.fn());//2
function fn(){
  console.log(this.length)
}
function test(){
  arguments[0]()
}
test(fn, 1)  // 2
var name = 'window'

var person1 = {
  name: 'person1',
  show1: function () {
    console.log(this.name)
  },
  show2: () => console.log(this.name),
  show3: function () {
    return function () {
      console.log(this.name)
    }
  },
  show4: function () {
    return () => console.log(this.name)
  }
}
var person2 = {
  name: 'person2'
}

person1.show1() // person1
person1.show1.call(person2) // person2

person1.show2() // undefined
person1.show2.call(person2) // undefined

person1.show3()() // undefined
person1.show3().call(person2) // person2
person1.show3.call(person2)() // undefined

person1.show4()() // person1
person1.show4().call(person2) // person1
person1.show4.call(person2)() // person2
var value = 1;

var foo = {
  value: 2,
  bar: function () {
    return this.value;
  }
}

//示例1
console.log(foo.bar());
//示例2
console.log((foo.bar)());
//示例3
console.log((foo.bar = foo.bar)());
//示例4
console.log((false || foo.bar)());
//示例5
console.log((foo.bar, foo.bar)());
var obj = {
    id: '1',
    cool: function() {
        console.log(this.id);
    }
}
id = 2;
obj.cool();
setTimeout(obj.cool, 100);
var num = 1;
var myObject = {
    num: 2,
    add: function() {
        this.num = 3;
        (function() {
            console.log(this.num);
            this.num = 4;
        })();
        console.log(this.num);
    },
    sub: function() {
        console.log(this.num)
    }
}
myObject.add();
console.log(myObject.num);
console.log(num);
var sub = myObject.sub;
sub();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant