持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第 7 天,点击查看活动详情
关键词: this指向 call apply bind new
为什么要串起来讲?因为这几个知识点是一起的。相互印证之下更好理解。
this 是什么?
this 是指针,指向调用函数的对象。决定 this 指向的是函数的调用方式。
如何改变 this 指向?
首先必须了解到为什么要改变 this 指向:隐式传递对象的引用,而不是通过传参的方式。
如果不利用 this,我们会如何在不同的上下文对象中重复使用函数?
function fn(ctx) {
return ctx.name
}
function operate(ctx) {
console.log('hello ' + fn(ctx))
}
var a = {
name: 'a'
}
fn(a) // a
operate(a) // hello a
// 使用 this
var b = {
name: 'b',
operate: function () {
operate(this)
}
}
// 隐式传递对象的引用
b.operate() // hello b
然后我们需要了解到:this 是在函数被调用时发生绑定,所以决定 this 指向的是函数的调用方式。
this 绑定规则有四种(函数的调用方式):
- 默认绑定。作为函数,默认绑定在全局变量
window下。 - 隐式绑定。作为方法,关联在一个对象上
obj.fn。 - 显式绑定。通过
call/apply/bind显式绑定,指向绑定的对象。 new绑定。作为构造函数,实例化一个对象。。- 需要注意:箭头函数没有自己的
this,继承于上一层上下文的this(与声明所在的上下文相同)。
运行环境只针对浏览器且非严格模式。
// 默认绑定 - 函数挂在 window 下,实际是 window.fn()
var name = 'global name';
function fn(name) {
if (name) {
this.name = name
} else {
console.log(this.name);
}
}
fn() // global name
// 隐式绑定
var p = {
name: 'p name',
fn
}
p.fn() // p name
// 显式绑定
var newFn = p.fn;
newFn() // global name
newFn.apply(p) // p name
// new 绑定 -> 可以先看后文 如何实现 new
var fn1 = new fn('new name')
console.log(fn1.name) // new name
// 箭头函数是例外
var b = {
name: 'b name',
fn: () => {
console.log(this.name)
}
}
b.fn() // global name
通过手写代码加深理解
如何实现 call
call 实现的关键在于隐式改变 this 的指向。
实现要点:
- 如果不传入参数或者参数为
null,默认指向为window,值为原始值的指向该原始值的自动包装对象,如String、Number、Boolean。 - 为了避免函数名与上下文(context)的属性发生冲突,使用
Symbol类型作为唯一值 - 将函数作为传入的上下文(context)属性执行
- 函数执行完成后删除该属性
- 返回执行结果
/**
* 1. 将函数设为传入参数的属性
* 2. 指定 this 到函数并传入给定参数执行函数
* 3. 如果不传入参数或者参数为 null,默认指向为 window
* 4. 删除参数上的函数
*/
Function.prototype.myCall = function (context, ...args) {
let cxt = context || window;
// 将当前被调用的方法定义在cxt.func上.(为了能以对象调用形式绑定this)
// 新建一个唯一的Symbol变量避免重复
let func = Symbol()
cxt[func] = this;
args = args ? args : []
// 以对象调用形式调用func,此时this指向cxt 也就是传入的需要绑定的this指向
const res = args.length > 0 ? cxt[func](...args) : cxt[func]();
// 删除该方法,不然会对传入对象造成污染(添加该方法)
delete cxt[func];
return res;
}
测试代码:
// test code
const foo = {
name: 'kane'
}
const name = 'logger';
function bar(job, age) {
console.log(this.name);
console.log(job, age);
}
bar.myCall(foo, 'sb', 20);
bar.myCall(null, 'aho', 25);
如何实现 apply
apply 实现原理与 call 相同,差别在于参数的处理和判断。
实现要点:
this可能传入null,第二个参数可以不传,但类型必须为数组或者类数组 其他的则与call相同- 如果不传入参数或者参数为
null,默认指向为window,值为原始值的指向该原始值的自动包装对象,如String、Number、Boolean。 - 为了避免函数名与上下文(context)的属性发生冲突,使用
Symbol类型作为唯一值 - 将函数作为传入的上下文(context)属性执行
- 函数执行完成后删除该属性
- 返回执行结果
/**
* 第二个参数可以不传,但类型必须为数组或者类数组
*/
Function.prototype.myApply = function (context, args = []) {
let cxt = context || window;
// 将当前被调用的方法定义在cxt.func上.(为了能以对象调用形式绑定this)
// 新建一个唯一的Symbol变量避免重复
let func = Symbol()
cxt[func] = this;
// 以对象调用形式调用func,此时this指向cxt 也就是传入的需要绑定的this指向
const res = args.length > 0 ? cxt[func](...args) : cxt[func]();
delete cxt[func];
return res;
}
测试代码
const foo = {
name: 'Selina'
}
const name = 'Chirs';
function bar(job, age) {
console.log(this.name);
console.log(job, age);
}
bar.myApply(foo, ['programmer', 20]);
bar.myApply(null, ['teacher', 25]);
如何实现 bind
bind 在此基础上,增加了一些业务判断。整体实现较为复杂,我们可以分步骤来分析。
step 1: 绑定原型
Function.prototype.myBind = function() {}
step 2: 改变 this 指向
Function.prototype.myBind = function(target) {
const _this = this;
return function() {
_this.apply(target)
}
}
step 3: 支持柯里化
柯里化举例
function fn(x) {
return function (y) {
return x + y;
}
}
var fn1 = fn(1)(2);
fn1(3) // 6
柯里化使用了闭包,当执行 fn1 的时候,形成了闭包,函数内获取到了外层函数的 x。
实现步骤:
- 获取当前外部函数的
arguments, 去除绑定的对象,保存成变量args. return-> 再一次获取当前函数的arguments, 最终用finalArgs进行合并。
Function.prototype.myBind = function(target) {
const _this = this;
const args = [...arguments].slice(1)
return function (){
const finalArgs = [...args, ...arguments]
_this.apply(target, finalArgs)
}
}
step 4: new 的调用
通过 bind 绑定之后,依然是可以通过 new 来进行实例化的, new 的优先级会高于 bind。
new 关键字会进行如下的操作:
- 创建一个空的简单JavaScript对象(即
{}); - 链接该对象(设置该对象的
constructor)到另一个对象 ; - 将步骤1 新创建的对象作为
this的上下文 ; - 如果该函数没有返回对象,则返回
this。
Function.prototype.myBind = function(target) {
const _this = this;
const args = [...arguments].slice(1)
return function (){
const finalArgs = [...args, ...arguments];
if(new.target !== undefined) { // new.target 用来检测是否是被 new 调用
const result = _this.apply(target, finalArgs);
if(result instanceof Object) { // 判断改函数是否返回对象
return reuslt;
}
return this // 没有返回对象就返回 this
}else { // 不是 new
_this.apply(target, finalArgs)
}
}
}
step 5: 保留函数原型
Function.prototype.myBind = function (target) {
// 判断是否为函数调用
if (typeof target !== 'function' || Object.prototype.toString.call(target) !== '[object Function]') {
throw new TypeError(this + ' must be a function');
}
const _this = this;
const args = [...arguments].slice(1)
let wrapper;
const binder = function () {
const finalArgs = [...args, ...arguments];
if (new.target !== undefined) {
const result = _this.apply(target, finalArgs);
if (result instanceof Object) return reuslt;
return this
} else {
_this.apply(target, finalArgs)
}
}
const wrapperLength = Math.max(0, _this.length - args.length);
const wrapperArgs = [];
for (var i = 0; i < wrapperLength; i++) {
wrapperArgs.push('$' + i);
}
wrapper = Function('binder', 'return function (' + boundArgs.join(',') + '){ return binder.apply(this,arguments); }')(binder);
if (_this.prototype) {
// wrapper.prototype = _this.prototype
// _this.prototype 导致原函数的原型被修改 应使用 Object.create
wrapper.prototype = Object.create(_this.prototype);
wrapper.prototype.constructor = _this;
}
return wrapper
}
如何实现 new
new 运算符用来创建用户自定义的对象类型的实例或者具有构造函数的内置对象的实例。
- 创建一个新对象
- 这个新对象会被执行
__proto__原型链接 - 将构造函数的作用域赋值给新对象,即
this指向这个新对象 - 如果函数没有返回其他对象,那么
new表达式中的函数调用会自动返回这个新对象
function myNew() {
var obj = new Object() // 创建一个新对象
Constructor = [].shift.call(arguments);
obj.__proto__ = Constructor.prototype; // 创建一个新对象
var ret = Constructor.apply(obj, arguments); // //将构造函数绑定到obj中
// ret || obj 这里这么写,考虑了构造函数显示返回 null 的情况
return typeof ret === 'object' ? ret || obj : obj;
};
function person(name, age) {
this.name = name
this.age = age
}
let p = myNew(person, '布兰', 12)
console.log(p) // { name: '布兰', age: 12 }
参考资料:
《你不知道的 JavaScript》
《JavaScript 忍者秘籍》