Photo by Anas Alshanti on Unsplash
有关 JS 中的 this、call、apply 和 bind 的概念网络上已经有很多文章讲解了 这篇文章目的是梳理一下这几个概念的知识点以及阐述如何用原生 JS 去实现这几个功能
this 的指向在严格模式和非严格模式下有所不同;this 究竟指向什么是,在绝大多数情况下取决于函数如何被调用
全局执行环境的情况:
非严格模式下,this 在全局执行环境中指向全局对象(window、global、self);严格模式下则为 undefined
20190306083121.png
作为对象方法的调用情况:
假设函数作为一个方法被定义在对象中,那么 this 指向最后调用他的这个对象
比如:
1a = 10
2obj = {
3 a: 1,
4 f() {
5 console.log(this.a) // this -> obj
6 }
7}
8
9obj.f() // 1 最后由 obj 调用
obj.f()
等同于 window.obj.f()
最后由 obj 对象调用,因此 this 指向这个 obj
即便是这个对象的方法被赋值给一个变量并执行也是如此:
1const fn = obj.f
2fn() // 相当于 window.fn() 因此 this 仍然指向最后调用他的对象 window
20190306084716.png
call apply bind 的情况:
想要修改 this 指向的时候,我们通常使用上述方法改变 this 的指向
1a = 10
2obj = {
3 a: 1
4}
5function fn(...args) {
6 console.log(this.a, 'args length: ', args)
7}
8
9fn.call(obj, 1, 2)
10fn.apply(obj, [1, 2])
11fn.bind(obj, ...[1, 2])()
20190306090239.png
可以看到 this 全部被绑定在了 obj 对象上,打印的 this.a
也都为 1
new 操作符的情况:
new 操作符原理实际上就是创建了一个新的实例,被 new 的函数被称为构造函数,构造函数 new 出来的对象方法中的 this 永远指向这个新的对象:
1a = 10
2function fn(a) { this.a = a }
3b = new fn(1)
4b.a // 1
20190306090716.png
箭头函数的情况:
1a = 10
2fn = () => { console.log(this.a) }
3obj = { a: 20 }
4obj.fn = fn
5obj.fn()
6window.obj.fn()
7f = obj.fn
8f()
20190306091151.png
无论如何调用 fn 函数内的 this 永远被固定在了这个外层的作用域(上述例子中的 window 对象)
如果需要改变 this 的指向,有以下几种方法:
普通函数
1a = 10
2obj = {
3 a: 1,
4 f() { // this -> obj
5 function g() { // this -> window
6 console.log(this.a)
7 }
8 g()
9 }
10}
11
12obj.f() // 10
在 f 函数体内 g 函数所在的作用域中 this 的指向是 obj:
20190306094032.png
在 g 函数体内,this 则变成了 window:
20190306094118.png
改为箭头函数
1a = 10
2obj = {
3 a: 1,
4 f() { // this -> obj
5 const g = () => { // this -> obj
6 console.log(this.a)
7 }
8 g()
9 }
10}
11
12obj.f() // 1
在 f 函数体内 this 指向的是 obj:
20190306094446.png
在 g 函数体内 this 指向仍然是 obj:
20190306094528.png
这个方法曾经经常用,即手动缓存 this 给一个名为 _this
或 that
等其他变量,当需要使用时用后者代替
1a = 10
2obj = {
3 a: 20,
4 f() {
5 const _this = this
6 setTimeout(function() {
7 console.log(_this.a, this.a)
8 }, 0)
9 }
10}
11
12obj.f() // _this.a 指向 20 this.a 则指向 10
20190306095926.png
查看一下 this 和 _this 的指向,前者指向 window 后者则指向 obj 对象:
20190307081510.png
call 方法第一个参数为指定需要绑定的 this 对象;其他参数则为传递的值:
20190306100658.png
需要注意的是,第一个参数如果是:
f.call(1)
this 将指向 Number
,并且这个 Number 的 [[PrimitiveValue]]
值为 11obj = {
2 name: 'obj name'
3}
4
5{(function() {
6 console.log(this.name)
7}).call(obj)}
20190306103718.png
与 call 类似但第二个参数必须为数组:
1obj = {
2 name: 'obj name'
3}
4
5{(function (...args){
6 console.log(this.name, [...args])
7}).apply(obj, [1, 2, 3])}
20190306104048.png
比如常见的函数内包含一个异步方法:
1function foo() {
2 let _this = this // _this -> obj
3 setTimeout(function() {
4 console.log(_this.a) // _this.a -> obj.a
5 }, 0)
6}
7obj = {
8 a: 1
9}
10foo.call(obj) // this -> obj
11// 1
我们上面提到了可以使用缓存 this 的方法来固定 this 指向,那么使用 bind 代码看起来更加优雅:
1function foo() { // this -> obj
2 setTimeout(function () { // 如果不使用箭头函数,则需要用 bind 方法绑定 this
3 console.log(this.a) // this.a -> obj.a
4 }.bind(this), 100)
5}
6obj = {
7 a: 1
8}
9
10foo.call(obj) // this -> obj
11// 1
或者直接用箭头函数:
1function foo() { // this -> obj
2 setTimeout(() => { // 箭头函数没有 this 继承外部作用域的 this
3 console.log(this.a) // this.a -> obj.a
4 }, 100)
5}
6obj = {
7 a: 1
8}
9
10foo.call(obj) // this -> obj
11// 1
20190307082854.png
new 操作符实际上就是生成一个新的对象,这个对象就是原来对象的实例。因为箭头函数没有 this 所以函数不能作为构造函数,构造函数通过 new 操作符改变了 this 的指向。
1function Person(name) {
2 this.name = name // this -> new 生成的实例
3}
4p = new Person('oli')
5console.table(p)
this.name
表明了新创建的实例拥有一个 name 属性;当调用 new 操作符的时候,构造函数中的 this 就绑定在了实例对象上
20190306230406.png
文章上半部分讲解了 this 的指向以及如何使用 call bind apply 方法修改 this 指向;文章下半部分我们用 JS 去自己实现这三种方法
Function.prototype
上这样才能在函数上调用到自定义的 myCall 方法代码实现:
1Function.prototype.myCall = function(ctx) {
2 ctx.fn = this
3 ctx.fn()
4 delete ctx.fn
5}
20190306233008.png
最基本的 myCall 就实现了,ctx 代表的是需要绑定的对象,但这里有几个问题,如果 ctx 对象本身就拥有一个 fn 属性或方法就会导致冲突。为了解决这个问题,我们需要修改代码使用 Symbol 来避免属性的冲突:
1Function.prototype.myCall = function(ctx) {
2 const fn = Symbol('fn') // 使用 Symbol 避免属性名冲突
3 ctx[fn] = this
4 ctx[fn]()
5 delete ctx[fn]
6}
7obj = { fn: 'functionName' }
8function foo() { console.log(this.fn) }
9
10foo.myCall(obj)
20190306233305.png
同样的,我们还要解决参数传递的问题,上述代码中没有引入其他参数还要继续修改:
1Function.prototype.myCall = function(ctx, ...argv) {
2 const fn = Symbol('fn')
3 ctx[fn] = this
4 ctx[fn](...argv) // 传入参数
5 delete ctx[fn]
6}
7obj = { fn: 'functionName', a: 10 }
8function foo(name) { console.log(this[name]) }
9
10foo.myCall(obj, 'fn')
20190306233625.png
另外,我们还要检测传入的第一个值是否为对象:
1Function.prototype.myCall = function(ctx, ...argv) {
2 ctx = typeof ctx === 'object' ? ctx || window : {} // 当 ctx 是对象的时候默认设置为 ctx;如果为 null 则设置为 window 否则为空对象
3 const fn = Symbol('fn')
4 ctx[fn] = this
5 ctx[fn](...argv)
6 delete ctx[fn]
7}
8obj = { fn: 'functionName', a: 10 }
9function foo(name) { console.log(this[name]) }
10
11foo.myCall(null, 'a')
如果 ctx 为对象,那么检查 ctx 是否为 null 是则返回默认的 window 否则返回这个 ctx 对象;如果 ctx 不为对象那么将 ctx 设置为空对象(按照语法规则,需要将原始类型转化,为了简单说明原理这里就不考虑了)
执行效果如下:
20190306235453.png
这么一来自定义的 myCall 也就完成了
另外修改一下检测 ctx 是否为对象可以直接使用 Object;delete 对象的属性也可改为 ES6 的 Reflect:
1Function.prototype.myCall = function(ctx, ...argv) {
2 ctx = ctx ? Object(ctx) : window
3 const fn = Symbol('fn')
4 ctx[fn] = this
5 ctx[fn](...argv)
6 Reflect.deleteProperty(ctx, fn) // 等同于 delete 操作符
7 return result
8}
apply 效果跟 call 类似,将传入的数组通过扩展操作符传入函数即可
1Function.prototype.myApply = function(ctx, argv) {
2 ctx = ctx ? Object(ctx) : window
3 // 或者可以鉴别一下 argv 是不是数组
4 const fn = Symbol('fn')
5 ctx[fn] = this
6 ctx[fn](...argv)
7 Reflect.deleteProperty(ctx, fn) // 等同于 delete 操作符
8 return result
9}
bind 与 call 和 apply 不同的是,他不会立即调用这个函数,而是返回一个新的 this 改变后的函数。根据这一特点我们写一个自定义的 myBind:
1Function.prototype.myBind = function(ctx) {
2 return () => { // 要用箭头函数,否则 this 指向错误
3 return this.call(ctx)
4 }
5}
20190307224718.png
这里需要注意的是,this 的指向原因需要在返回一个箭头函数,箭头函数内部的 this 指向来自外部
然后考虑合并接收到的参数,因为 bind 可能有如下写法:
1f.bind(obj, 2)(2)
2// or
3f.bind(obj)(2, 2)
修改代码:
1Function.prototype.myBind = function(ctx, ...argv1) {
2 return (...argv2) => {
3 return this.call(ctx, ...argv1, ...argv2)
4 }
5}
20190307225732.png
另外补充一点,bind 后的函数还有可能会被使用 new 操作符创建对象。因此 this 理应被忽略但传入的参数却正常传入。
举个例子:
1obj = {
2 name: 'inner' // 首先定义一个包含 name 属性的对象
3}
4function foo(fname, lname) { // 然后定义一个函数
5 this.fname = fname
6 console.log(fname, this.name, lname) // 打印 name 属性
7}
8foo.prototype.age = 12
然后我们使用 bind 创建一个新的函数并用 new 调用返回新的对象:
1boundf = foo.bind(obj, 'oli', 'young')
2newObj = new boundf()
20190311095410.png
看图片得知,尽管我们定义了 obj.name 并且使用了 bind 方法绑定 this 但因使用了 new 操作符 this 被重新绑定在了 newObj 上。因此打印出来的 this.name 就是 undefined 了
因此我们还要继续修改我们的 myBind 方法:
1Function.prototype.myBind = function (ctx, ...argv1) {
2 let _this = this
3 let boundFunc = function (...argv2) { // 这里不能写成箭头函数了,因为要使用 new 操作符会报错
4 return _this.call(this instanceof boundFunc ? this : ctx, ...argv1, ...argv2) // 检查 this 是否为 boundFunc 的实例
5 }
6 return boundFunc
7}
然后我们使用看看效果如何:
20190311100213.png
this 指向问题解决了但 newObj 实例并未继承到绑定函数原型中的值,因此还要解决这个问题,那么我们直接修改代码增加一个 prototype 的连接:
1Function.prototype.myBind = function (ctx, ...argv1) {
2 let _this = this
3 let boundFunc = function (...argv2) {
4 return _this.call(this instanceof boundFunc ? this : ctx, ...argv1, ...argv2)
5 }
6 boundFunc.prototype = this.prototype // 连接 prototype 继承原型中的值
7 return boundFunc
8}
20190311100453.png
看起来不错,但还是有一个问题,尝试修改 boundf 的原型:
20190311103407.png
发现我们的 foo 中原型的值也被修改了,因为直接使用 = 操作符赋值,其实本质上还是原型的值,最后我们再修改一下,使用一个空的函数来重新 new 一个:
1Function.prototype.myBind = function (ctx, ...argv1) {
2 let _this = this
3 let temp = function() {} // 定义一个空的函数
4 let boundFunc = function (...argv2) {
5 return _this.call(this instanceof temp ? this : ctx, ...argv1, ...argv2)
6 }
7 temp.prototype = this.prototype // 继承绑定函数原型的值
8 boundFunc.prototype = new temp() // 使用 new 操作符创建实例并赋值
9 return boundFunc
10}
最后看下效果:
20190311103534.png
最后我们再来实现一个 new 操作符名为 myNew
new 操作符的原理是啥:
proto
必然要与构造函数的 prototype 相连接)代码实现:
1function myNew(Constructor) { // 接收一个 Constructor 构造函数
2 let newObj = {} // 创建一个新的对象
3 newObj.__proto__ = Constructor.prototype // 绑定对象的 __proto__ 到构造函数的 prototype
4 Constructor.call(newObj) // 修改 this 指向
5 return newObj // 返回这个对象
6}
20190307232044.png
然后考虑传入参数问题,继续修改代码:
1function myNew(Constructor, ...argv) { // 接收参数
2 let newObj = {}
3 newObj.__proto__ = Constructor.prototype
4 Constructor.call(newObj, ...argv) // 传入参数
5 return newObj
6}
20190307232419.png
到此为止
再遇到类似问题,基本常见的情况都能应付得来了
(完)
参考:
webgzh907247189
修改了一些代码实现