this,apply,call,bind解析并手写

147 阅读4分钟

this

对象的内存结构

对象的内存结构

var obj = {
    name: 'a',
    say: function(){
        console.log(this.name)
    }
}

var say = obj.say;
obj.say();  //行1 a
say();  //行2 undefined

我这里直接进入对象中的函数的声明与调用,以上文中的obj为例。 在创建执行上下文中,变量的声明的步骤依次为:创建、初始化、赋值。对象是一种特殊的变量,所以它也存在这3个步骤。 在初始化之后赋值之前,obj的值为undefined,在赋值操作时,javascript引擎会在内存里创建一个下面的对象,然后将这个对象的内存地址赋值给 obj 这个变量。

{
    name: 'a',
    say: function(){
        console.log(this.name)
    }
}

若要读取 obj.name ,引擎会先拿到obj的内存地址,然后到这个内存地址中找到name这个属性的value属性。 当给对象赋值的是一个函数时,如上面的obj.say的地址,引擎会把函数单独保存在内存中,然后再将函数的地址赋值给say属性的value属性。

{
  name: {
    [[value]]: 'a'
    [[writable]]: true
    [[enumerable]]: true
    [[configurable]]: true
  },
  say: {
    [[value]]: 函数的地址
    [[writable]]: true
    [[enumerable]]: true
    [[configurable]]: true
  }
}

那么执行var say = obj.say;时,其实是把函数的地址赋值给了say,那么say就可以独立运行了,当执行say时,引擎可以直接拿到函数的地址而不再通过obj的内存了。

this 永远指向最后调用它的那个对象

let obj = {
  name: 'tim',
  talk() {
    console.log(this);
    // {
    //   name: 'tim',
    //   talk: [Function: talk],
    //   fun: { name: 'berg', say: [Function: say] }
    // }
  },
  fun: {
    name: 'berg',
    say() {
      console.log(this); //{ name: 'berg', say: [Function: say] }
    }
  }
}
obj.fun.say()
obj.talk()

根据上面的内存结构原理,引擎会把函数对象单独保存在内存中,所以say和talk在单独的内存中,obj的talk属性指向了在单独内存中的talk函数地址,同理func的say属性引用了say的函数地址.obj.talk()是在obj这个运行环境执行的,this指向obj,obj.fun.say()是最后fun根据函数地址找到say函数并运行,fun是运行环境,因此this用员指向最后调用它的那个对象.


function Say(age) {
  this.age = age
  this.face = 'hansome'
  this.name = 'Avicii'
}
Say.prototype.on = function () {
  console.log(this, this.age, this.name, this.passion);
  // 原则:js中访问一个对象的属性,会遵循一个规则:先在这个对象本身找私有属性,如果找到了就返回,如果找不到就沿着原型链往上找,
  // 直到最顶层null还找不到就会返回undefined。
  //1.通过new调用,则 this指向实例,因为最后由实例调用(ins.on(),this指向ins),而实例已经继承了构造函数的属性和方法了
  // 并且实例在new的内部实现了ins=Object.create(Say.prototype)(也有其他相似方法执行),所以ins.__proto__==Say.prototype,
  // ins可以访问定义在原型上面的方法和属性
  //并且实例遵循上面的属性查找原则
  // 2.通过ins=Object.create(Say.prototype),则ins.__proto__==Say.prototype,ins可以访问原型上面的属性和方法,但没有继承
  // 并且也没有继承和访问Say函数里面定义的方法
  //3.直接调用(Say.prototype.on()) 则this指向Say.prototype中的prototype这个对象,不能访问Say函数里面定义的属性和方法
}


//test
Say.prototype.name = 'Tim'
Say.prototype.passion = 'music'
Say.prototype.on()
// Say { on: [Function], name: 'Tim', passion: 'music' } undefined Tim music
let ins = new Say(30)
ins.on()
// Say { age: 30, face: 'hansome', name: 'Avicii' } 30 Avicii music

概述

apply,call,bind的作用是一样的,都是用来改变某个函数运行时的上下文(context),简单理解就是改变函数体内部的this指向但是bind只是改变函数内部this的指向并不自动调用,需要手动调用.apply和call改变函数内部this指向并立即调用.

fn.call(target, arg1, arg2, ...)

fn.apply(target, [arg1, arg2, ...])

fn.bind(target)

这三个方法的第一个参数都是传入this,就是将函数内部的this指向你传入的对象,但是apply的第二个参数是一个数组,里面存放你传递给函数并调用的参数.call的后面第二个参数,第三个参数,第四个参数...都是传递给函数并调用的参数.bind自己手动调用时传入参数

apply

const me = { name: 'Jack' }
function say(age) {
  console.log(`My name is ${this.name || 'default'} and my age is ${age || 'default'}`);
  return age
}
const age = say.apply(me, [32])
<!--My name is Jack and my age is 32-->
console.log(age);
<!--32-->

手写apply

<!--如果不传入context或传入的context为null,undefined,则将context指向全局对象:globalThis,globalThis的初始信息如下,具有定时器等相关方法,它能在任何跑js的环境找到,是一个统一的标准.-->
<!--Object [global] {-->
<!--  global: [Circular],-->
<!--  clearInterval: [Function: clearInterval],-->
<!--  clearTimeout: [Function: clearTimeout],-->
<!--  setInterval: [Function: setInterval],-->
<!--  setTimeout: [Function: setTimeout] { [Symbol(util.promisify.custom)]: [Function] },-->
<!--  queueMicrotask: [Function: queueMicrotask],-->
<!--  clearImmediate: [Function: clearImmediate],-->
<!--  setImmediate: [Function: setImmediate] {-->
<!--    [Symbol(util.promisify.custom)]: [Function]-->
<!--  }-->
<!--}-->
Function.prototype.myApply = function (context) {
  if (typeof this !== 'function') {
    throw new TypeError('Error');
  }
  <!--引用类型(array/object/function)都具有对象特性,可以扩展属性或者方法,但是像number等其他数据类型无法扩展属性,因此统一将传入的this包装成对象,使它们可以扩展属性或者方法,为下一步改变this指向的动作做完准备,另外null或者undefined也会被包装,因此防止传入的this为null或者undefined-->
  context = Object(context)
<!--这一步是apply,call改变函数this指向的核心,在context上创建一个方法fn,并绑定this(这个this就是函数本身,比如下面test中的say函数,运行say.myApply(me)查看context信息:
{ name: 'Jack', fn: [Function: say] },context里面有原先定义的属性name,还有现在定义的方法fn并绑定了this(say函数)-->
  context.fn = this
  <!--创建一个res用于返回函数的返回值,如果没有,则是undefined-->
  let res
  <!--如果传递了函数所需要的参数参数-->
  if (arguments[1]) {
  <!--这里把参数传递进函数并运行,此时是context调用fn,fn指向函数,那么运行时函数内部的this就指向调用者context,这里就成功改变了函数内部this指向为我们传递进myApply的第一个参数:this对象,如果函数有返回值,则赋值给res并在函数最后返回-->
    res = context.fn(...arguments[1])
  } else {
  <!--同理,如果没有传递参数则直接contex调用fn方法-->
    res = context.fn()
  }
  <!--如果函数有返回值,则返回值赋值给res并返回,如果没有,则res为undefined-->
  return res
}
<!--test-->
const me = { name: 'Jack' }
function say(age) {
  console.log(`My name is ${this.name || 'default'} and my age is ${age || 'default'}`);
  return age
}
const age = say.myApply(me, [32])
<!--My name is Jack and my age is 32-->
console.log(age);
<!--32-->

call

const me = { name: 'Jack' }
function say(age,habby) {
  console.log(`My name is ${this.name || 'default'} and my age is ${age || 'default'},and my habby  is ${habby||'default'} `);
  return age
}
const age = say.call(me, 32,'avicii')
<!--My name is Jack and my age is 32,and my habby  is avicii -->
console.log(age);
<!--32-->

手写call

<!--这里的原理与上面的myApply几乎一样,唯一区别就是将参数减裁掉第一个后将剩余参数返回到args数组,然后context调用fn时利用es6的解析操作符...将参数逐个传递进去.-->
Function.prototype.myCall = function (context) {
  if (typeof this !== 'function') {
    throw new TypeError('Error');
  }
  context = Object(context)
  context.fn = this
  console.log(context);
  let res
  const args = [...arguments].slice(1)
  if (args) {
    res = context.fn(...args)
  } else {
    res = context.fn()
  }
  delete context.fn
  return res
}
<!--test-->
const me = { name: 'Jack' }
function say(age, habby) {
  console.log(`My name is ${this.name || 'default'} and my age is ${age || 'default'},and my habby  is ${habby || 'default'} `);
  return age
}
const age = say.myCall(null, 32, 'avicii')
console.log(age);

bind

const me = { name: 'Jack' }
function say(age, habby) {
  console.log(`My name is ${this.name || 'default'} and my age is ${age || 'default'},and my habby  is ${habby || 'default'} `);
  return age
}
const b = say.bind(me)(32,'avicii')
<!--My name is Jack and my age is 32,and my habby  is avicii -->
console.log(age);
<!--32-->

手写bind

<!--这里是我根据前面apply,call的原理写的bind-->
<!--与上面的原理一样,只是这里直接返回了context.fn方法,后面需要手动调用并传参(如果函数需要参数)-->
Function.prototype.myBind = function (context) {
  if (typeof this !== 'function') {
    throw new TypeError('Error');
  }
  context = Object(context)
  context.fn = this
  return context.fn
}
<!--这里是我参考学习其他人写的bind-->
Function.prototype.myBind2 = function (context) {
  // 外部保存this
  const fn = this
  const newFunc = function () {
    // 将传进的arguments这个伪数组转化为真正的数组后赋值给newArgs
    const newArgs = Array.from(arguments);
    this.name = 'mx'
    // 返回构造函数的返回值
    // fn.apply(context,newArgs)使传递进去的context继承fn里面定义的属性和方法并将newArgs相应赋值
    return fn.apply(context, newArgs)
  }
  // 使用Object.create()利用现有对象的原型(fn.prototype)创建一个新对象,并使newFunc.prototype.__proto__指向新对象,因此newFunc.prototype.__proto__==fn.prototype
  // newFunc.prototype可以访问fn.prototype中的属性和方法
  // newFunc具有原型,可以通过new newFunc()产生一个实例(ins),因为通过new调用,ins.__proto__==newFunc.prototype,newFunc.prototype.__proto__==fn.prototype
  // 因此ins可以通过原型链(__proto__)访问定义在newFunc.prototype和 fn.prototype上面的属性和方法
  // 但是newFunc没有通过fn.apply(newFunc,args)继承fn函数中定义的方法和属性,newFunc只是能够访问fn.prototype
  newFunc.prototype = Object.create(fn.prototype)
  return newFunc

}
//test
const me = {
  name: 'avicii'
}
function Talk() {
  this.name = 'mx'
}
function Say(name) {
  this.name = name
  return this.name
}
const func = Say.myBind2(me)
func.prototype.name = 'berg'
let ins = new func('tim')
let ins2 = Object.create(func.prototype)
console.log(Say, func.prototype.__proto__, ins, ins2.__proto__ == ins.__proto__, ins.__proto__ == func.prototype, ins.__proto__, ins.name, ins2.name, me);
console.log(func.prototype, ins);
//  [Function: Say] Say {} Say {} true true Say { name: 'berg' } berg berg { name: 'tim' }
//  1.ins为什么打印出Say {}
// ins=Object.create(proto[,propertiesObject])方法只是将ins.__proto__=proto,ins只能够通过原型链(__proto__)访问proto上面的方法和属性,但没有继承它们,ins就是一个空的proto对象
// 在这里首先myBind2内部中将newFunc.prototype = Object.create(fn.prototype),因此newFunc.prototype.__proto__==fn.prototype
// newFunc.prototype就是一个空的fn.prototype对象,
// 而在new的内部执行了obj=Object.create(newFunc.prototype),因此obj.__proto__==newFunc.prototype,
// obj就是一个空的newFunc.prototype对象
// 当我们在在外面用Say.myBind2()调用的时候,fn就是Say了,因此newFunc.prototype是一个空的Say.prototype
// 又因为obj是一个空的newFunc.prototype对象,那么最后obj就是一个空的Say.prototype对象了,打印出Say {}
//因为Say内部返回的是一个字符串类型,不是Array/function/object这些引用类型对象,所以new直接返回了obj赋值给ins,ins就是Say {}了
//如果没有newFunc.prototype = Object.create(fn.prototype),newFunc.prototype没有被改变而就是一个原本的newFunc.prototype对象,此时使obj=Object.create(newFunc.prototype)
// 那么obj就是一个空的newFunc.prototype对象了,打印出newFunc {}
// // 2.如果直接通过new Say()创建一个实例ins3,那么ins3就继承了Say函数里面定义的属性和方法,打印出的ins3里面会列出相应属性和方法
// // 3.ins2也是func的实例,它们指向同一个原型对象,因此它们都能通过(__proto__)访问定义在func.prototype上面的name属性
// // 4.因为newFunc内部执行了fn.apply(context, newArgs),我们将fn内部this通过apply指向传进去的me,在外面fn指向Say,Say内部改变了this.name属性值
// // 所以me的name的值被改编为传递进去的参数‘tim’

new的实现

// new的过程:
//   1.先创建一个空对象ins,
//   2.获取构造函数Con,并用 ins=Object.create(Con.prototype)等方法使ins.__proto__==Con.prototype,ins就可以访问构造函数原型对象中的方法属性
//   3.利用apply或者call方法将Con内部的this绑定为为ins,使ins继承构造函数中的方法属性
//   4.返回ins或者优先构造函数返回的对象
const myNew = function () {
  // 1、获得构造函数,同时删除 arguments 中第一个参数
  Con = [].shift.call(arguments);
  // 2、创建一个空的对象并链接到COn的原型,obj 可以通过原型链(__proto__)访问构造函数原型中的属性和方法,但没有继承
  let obj = Object.create(Con.prototype);
  // 3、将Con内部的this指向Obj,实现继承
  let ret = Con.apply(obj, arguments);
  // apply的内部实现核心:
  //1. obj.fn=Con
  // obj设置一个fn属性并将Con赋值给它,根据内存结构原理,fn指向了构造函数Con在独立内存中的函数地址
  //2. obj.fn(arguments)=Con(arguments)
  // 这一步运行函数时构造函数内部的this就会指向obj了,因为obj是fn函数的运行环境,obj.fn(arguments)是通过obj找到fn,fn通过保存在独立内存中的构造函数的的函数地址找到Con并运行
  // 如果构造函数中有如this.name=arguments[name]等设置属性的语句,那么这时就相当于是obj.name=arguments[name],
  // obj上面就设置了构造函数中会设置的属性或者方法并且把传递进的参数相应赋值
  // 4、优先返回构造函数返回的对象
  // 如果返回obj,则这时通过上面的apply方法obj已经继承了构造函数的属性或者方法了,并且在new时传递进的参数被赋值给继承的属性或者在方法中被调用
  return ret instanceof Object ? ret : obj;
}

//test
const me = { name: 'Jack' }
function say(age, habby) {
  console.log(`My name is ${this.name || 'default'} and my age is ${age || 'default'},and my habby  is ${habby || 'default'} `);
  // My name is Jack and my age is 32,and my habby  is avicii 
  return age
}
let age = say.myBind2(me)
// let age = say.myBind2(me)(32, 'avicii')
let ss = myNew(age, 32, 'avicii')
console.log(ss);
// say {}