There are a number of articles on this, call, apply and bind in JS. This article aims to clarify the concepts and explain how to use native JS to implement these functions

This points to the problem

this

The direction of this differs between strict and non-strict modes; What exactly does this refer to, in most cases, depends on how the function is called

Status of global execution environment:

In non-strict mode, this refers to a global object (window, global, self) in the global execution environment; In strict mode, undefined

Calls as object methods:

If the function is defined as a method on an object, this refers to the object on which it was last called

Such as:

a = 10
obj = {
    a: 1,
    f() {
        console.log(this.a) // this -> obj
    }
}

obj.f() // 1 is last called by obj
Copy the code

Obj.f () is the same as window.obj.f(), which is finally called by the obj object, so this refers to the obj

This is true even if the object’s method is assigned to a variable and executed:

const fn = obj.f
fn() // this is equivalent to window.fn(), so this still refers to the object that last called it, window
Copy the code

Call apply bind

When we want to change the direction of this, we usually use the method above to change the direction of this

a = 10
obj = {
	a: 1
}
function fn(. args) {
	console.log(this.a, 'args length: ', args)
}

fn.call(obj, 1.2)
fn.apply(obj, [1.2]) fn.bind(obj, ... [1.2) ()Copy the code

You can see that this is all bound to the obj object, and this. A is all printed as 1

Case of the new operator:

The new operator creates a new instance. The function called new is called a constructor. This in the constructor’s object method always refers to the new object:

a = 10
function fn(a) { this.a = a }
b = new fn(1)
b.a / / 1
Copy the code

Case of arrow function:

  • Normal functions only determine what this refers to at runtime
  • The arrow function defines this at function definition time, and this points to the outer scope
a = 10
fn = (a)= > { console.log(this.a) }
obj = { a: 20 }
obj.fn = fn
obj.fn()
window.obj.fn()
f = obj.fn
f()
Copy the code

In any case, the this inside the FN function is always fixed to the outer scope (the window object in the above example).

This changes the direction of the problem

If you need to change the direction of this, there are several methods:

  • Arrow function
  • Internal cache this
  • The apply method
  • Call method
  • The bind method
  • New operator

Arrow function

Common function

a = 10
obj = {
    a: 1,
    f() { // this -> obj
		function g() { // this -> window
        	console.log(this.a)
    	}
		g()
	}
}

obj.f() / / 10
Copy the code

This refers to obj in the scope where g is in f:

Inside g, this becomes window:

Change to arrow function

a = 10
obj = {
    a: 1,
    f() { // this -> obj
		const g = (a)= > { // this -> obj
        	console.log(this.a)
    	}
		g()
	}
}

obj.f() / / 1
Copy the code

In the body of f, this refers to obj:

Inside the g function, this is still obj:

Internal cache this

This method was often used to manually cache this for another variable named _this or that, and replace it with the latter when needed

a = 10
obj = {
    a: 20,
    f() {
        const _this = this
        setTimeout(function() {
            console.log(_this.a, this.a)
        }, 0)
    }
}

obj.f() // _this.a points to 20 this.a points to 10
Copy the code

Check the pointer to this and _this: the former points to window and the latter to obj:

call

The call method takes the first argument to specify the this object to bind to; The other parameters are passed values:

Note that if the first parameter is:

  • Null, undefined, not passed, this will point to a global object (in non-strict mode)
  • The original value will be converted to the corresponding wrapper object, such asf.call(1)This will point toNumberAnd the Number of[[PrimitiveValue]]Value of 1
obj = {
    name: 'obj name'
}

{(function() {
    console.log(this.name)
}).call(obj)}
Copy the code

apply

Similar to call except that the second argument must be an array:

obj = {
    name: 'obj name'
}

{(function (. args){
	console.log(this.name, [...args])
}).apply(obj, [1.2.3])}
Copy the code

bind

For example, a common function contains an asynchronous method:

function foo() {
	let _this = this // _this -> obj
	setTimeout(function() {
		console.log(_this.a) // _this.a -> obj.a
	}, 0)
}
obj = {
	a: 1
}
foo.call(obj) // this -> obj
/ / 1
Copy the code

We mentioned above that we can cache this to fix this, so using the bind code looks more elegant:

function foo() { // this -> obj
	setTimeout(function () { // If you do not use the arrow function, you need to bind this with the bind method
		console.log(this.a) // this.a -> obj.a
	}.bind(this), 100)
}
obj = {
	a: 1
}

foo.call(obj) // this -> obj
/ / 1
Copy the code

Or just use the arrow function:

function foo() { // this -> obj
	setTimeout((a)= > { // Arrow functions do not inherit this from the outer scope of this
		console.log(this.a) // this.a -> obj.a
	}, 100)
}
obj = {
	a: 1
}

foo.call(obj) // this -> obj
/ / 1
Copy the code

New operator

The new operator essentially generates a new object, which is an instance of the original object. Since the arrow function does not have this, the function cannot be used as a constructor. The constructor changes the orientation of this through the new operator.

function Person(name) {
	this.name = name // this -> new generates the instance
}
p = new Person('oli')
console.table(p)
Copy the code

This.name indicates that the newly created instance has a name attribute; When the new operator is called, this in the constructor is bound to the instance object

Native implementation call Apply bind New

The first half of this article explained the reference to this and how to modify this reference using the Call bind apply method. The second half of the article we use JS to achieve these three methods

myCall

  • First, myCall needs to be defined inFunction.prototypeSo that the custom myCall method can be used in function calls
  • We then define the myCall method, in which this refers to the function that myCall is called from
  • Next, myCall adds the method this points to in the first argument object and calls it
  • Finally, delete the temporary method

Code implementation:

Function.prototype.myCall = function(ctx) {
	ctx.fn = this
	ctx.fn()
	delete ctx.fn
}
Copy the code

The basic myCall is implemented, and CTX represents the object that needs to be bound, but there are a few problems. If the CTX object itself has an FN property or method, it can cause conflicts. To solve this problem, we need to modify the code to use Symbol to avoid attribute collisions:

Function.prototype.myCall = function(ctx) {
	const fn = Symbol('fn') // Use Symbol to avoid attribute name conflicts
	ctx[fn] = this
	ctx[fn]()
	delete ctx[fn]
}
obj = { fn: 'functionName' }
function foo() { console.log(this.fn) }

foo.myCall(obj)
Copy the code

Also, we need to solve the problem of parameter passing. There are no other parameters introduced in the above code and we need to continue to modify them:

Function.prototype.myCall = function(ctx, ... argv) {
	const fn = Symbol('fn')
	ctx[fn] = thisctx[fn](... argv)// Pass in parameters
	delete ctx[fn]
}
obj = { fn: 'functionName'.a: 10 }
function foo(name) { console.log(this[name]) }

foo.myCall(obj, 'fn')
Copy the code

Also, we check if the first value passed in is an object:

Function.prototype.myCall = function(ctx, ... argv) {
	ctx = typeof ctx === 'object' ? ctx || window : {} // When CTX is an object, the default is CTX; Set to Window if null otherwise empty object
	const fn = Symbol('fn')
	ctx[fn] = thisctx[fn](... argv)delete ctx[fn]
}
obj = { fn: 'functionName'.a: 10 }
function foo(name) { console.log(this[name]) }

foo.myCall(null.'a')
Copy the code

If CTX is an object, then check if CTX is null and return the default window otherwise return the CTX object; If CTX is not an object, set CTX to an empty object.

The execution effect is as follows:

A customized myCall is now complete

If CTX is an Object, you can use Object directly. The delete object property can also be changed to Reflect of ES6:

Function.prototype.myCall = function(ctx, ... argv) {
	ctx = ctx ? Object(ctx) : window
	const fn = Symbol('fn')
	ctx[fn] = thisctx[fn](... argv)Reflect.deleteProperty(ctx, fn) // Is equivalent to the delete operator
	return result
}
Copy the code

myApply

Apply works like Call, passing the array to the function through the extension operator

Function.prototype.myApply = function(ctx, argv) {
	ctx = ctx ? Object(ctx) : window
	// We can also verify that argv is an array
	const fn = Symbol('fn')
	ctx[fn] = thisctx[fn](... argv)Reflect.deleteProperty(ctx, fn) // Is equivalent to the delete operator
	return result
}
Copy the code

myBind

Bind, unlike call and apply, does not call this function immediately. Instead, it returns a new function with this changed. We’ll write a custom myBind:

Function.prototype.myBind = function(ctx) {
	return (a)= > { // Use the arrow function, otherwise this points to an error
		return this.call(ctx)
	}
}
Copy the code

It is important to note that the reason why this points is to return an arrow function. The arrow function inside this points from outside

Then consider merging the received arguments, as bind might be written as follows:

f.bind(obj, 2) (2)
// or
f.bind(obj)(2.2)
Copy the code

Modify code:

Function.prototype.myBind = function(ctx, ... argv1) {
	return (. argv2) = > {
		return this.call(ctx, ... argv1, ... argv2) } }Copy the code

As an added bonus, the bind function may also use the new operator to create objects. So this should be ignored but the argument passed in is passed in normally.

Here’s an example:

obj = {
    name: 'inner' // First define an object containing the name attribute
}
function foo(fname, lname) { // Then define a function
	this.fname = fname
	console.log(fname, this.name, lname) // Prints the name attribute
}
foo.prototype.age = 12
Copy the code

Then we use bind to create a new function and return the new object with a new call:

boundf = foo.bind(obj, 'oli'.'young')
newObj = new boundf()
Copy the code

We can see from the image that although we defined obj.name and bind this using the bind method, this is rebound to newObj using the new operator. So this. Name printed is undefined

So we’ll continue to modify our myBind method:

Function.prototype.myBind = function (ctx, ... argv1) {
	let _this = this
	let boundFunc = function (. argv2) { // We can't write an arrow function here, because using the new operator gives an error
		return _this.call(this instanceof boundFunc ? this: ctx, ... argv1, ... argv2)// Check if this is an instance of boundFunc
	}
	return boundFunc
}
Copy the code

Then we use it to see how it works:

The newObj instance does not inherit the value from the binding function prototype, so we need to fix this problem by adding a prototype link to the code:

Function.prototype.myBind = function (ctx, ... argv1) {
	let _this = this
	let boundFunc = function (. argv2) {
		return _this.call(this instanceof boundFunc ? this: ctx, ... argv1, ... argv2) } boundFunc.prototype =this.prototype // Concatenate prototype inherits the values in the prototype
	return boundFunc
}
Copy the code

It looks good, but there’s still a problem. Try modifying Boundf’s prototype:

We found that the value of our prototype in foo was also changed, because we assigned it directly to the = operator, which was essentially the value of the prototype. Finally, we changed it again, using an empty function to create a new one:

Function.prototype.myBind = function (ctx, ... argv1) {
	let _this = this
	let temp = function() {} // Define an empty function
	let boundFunc = function (. argv2) {
		return _this.call(this instanceof temp ? this: ctx, ... argv1, ... argv2) } temp.prototype =this.prototype // Inherits the value of the binding function prototype
	boundFunc.prototype = new temp() // Use the new operator to create an instance and assign a value
	return boundFunc
}
Copy the code

Finally, look at the effect:

New operator

Finally, we’ll implement a new operator called myNew

How does the new operator work?

  • Generate a new object
  • Bind prototype (new is an instance, then the instance’s__proto__Must be attached to the constructor’s prototype.
  • Binding this
  • Return this new object

Code implementation:

function myNew(Constructor) { // Receive a Constructor
	let newObj = {} // Create a new object
	newObj.__proto__ = Constructor.prototype // Bind the object's __proto__ to the constructor's prototype
	Constructor.call(newObj) // Modify this pointer
	return newObj // Return this object
}
Copy the code

Then consider passing in parameters and continue to modify the code:

function myNew(Constructor, ... argv) { // Receive parameters
	letnewObj = {} newObj.__proto__ = Constructor.prototype Constructor.call(newObj, ... argv)// Pass in parameters
	return newObj
}
Copy the code

summary

This far,

  • This points to the problem
  • How to modify this
  • How to implement call Apply Bind and new methods using native JS

Meet similar problem again, basic common situation can deal with

(after)

Reference:

  • https://juejin.cn/post/6844903496253177863#heading-1
  • https://segmentfault.com/a/1190000015438195#articleHeader3
  • https://github.com/Abiel1024/blog/issues/16
  • Thank youwebgzh907247189Some code implementations have been modified