对于每个执行上下文都有三个重要的属性,
- 变量对象
- 作用域链
- this。
为什么会有this
根据阮一峰的JavaScript 的 this 原理 ,this的由来与内存里面的数据结构有关系。比如:
var obj = {
bar: 5
foo: function(){}
};
对于这段代码,js引擎会生成一个对象,然后把这个对象的内存地址赋值给obj。在读取的时候引擎先从obj拿到内存地址,再从这个地址读出原始的对象,返回他的bar属性。原始对象是以字典结构保存,每个属性名对应一个属性描述对象。
{
bar:{
[[value]]:5
[[writable]]:true
[[enumerable]]:true
[[configurable]]:true
}
}
对于属性值为一个函数的情况,引擎会把函数单独保存在内存里面,然后将函数的地址赋给value属性。
所以我们可以看到,函数实际上可以在不同的环境执行。因为函数可以在不同的运行环境执行,所以this的目的就是在函数题的内部获得当前的运行环境(context)。
这里我们总结一下this的规则:
- this既不指向函数自身,也不指向函数的词法作用域
- this实际上是在函数被调用的时候发生的绑定,它指向什么完全取决于函数被调用的位置
this的绑定规则
- 默认绑定,this指向全局对象
- 隐式绑定,当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。不过隐式绑定经常丢失this绑定
- 显示绑定,call和apply方法,第一个参数是对象,是为this准备的,如果是原始类型则会被装箱。硬绑定即bind方法,创建一个包裹函数,返回一个显示指定上下文的函数。相应的还有API调用,如forEach(foo,obj)//把this绑定到obj
- new绑定,使用new时对函数的构造调用会创建一个全新的对象,这个新对象会绑定到函数调用的this,如果函数没有返回其他对象那么自动返回这个新对象。
绑定例外:
- 如果函数中确实使用了this,而传入了null,那么this将绑定到全局对象,可能造成不可预计的后果。更安全的this使用Object.create(null)创建一个新的对象,它和{}很像,不过不会创建一个Object.prototype这个委托
关于ES6箭头函数中的this
箭头函数的this的规则跟function定义的this不一样,箭头函数的this是在定义函数的时候绑定,而不是执行函数的时候绑定。 我们可以对比一下:
var x=11;
var obj={
x:22,
say:function(){
console.log(this.x)
}
}
obj.say();
//22
var x=11;
var obj={
x:22,
say:()=>{
console.log(this.x);
}
}
obj.say();
所谓的定义时候绑定,就是this是继承自父执行上下文!!中的this,比如这里的箭头函数中的this.x,箭头函数本身与say平级以key:value的形式,也就是箭头函数本身所在的对象为obj,而obj的父执行上下文就是window,因此这里的this.x实际上表示的是window.x,因此输出的是11。 需要强调的是箭头函数的this指向的是父执行上下文
模拟实现call和apply
call方法在使用一个指定的this值和若干个指定的参数值的前提下调用某个函数或方法。
先看看看call的用例:
var foo = {
value: 1
};
function bar() {
console.log(this.value);
}
bar.call(foo); // 1
所以我们可以看到,call函数有两个作用
- 将this的值指向foo
- 执行了bar函数。 而且还有两个要求:
- 当this传入参数为null的时候,this指向window
- call函数可以拥有返回值 所以根据上面this的指向讨论,我们的思路就可以变为:
- 把函数设成对象的属性
- 执行该函数
- 删除该属性 如下:
Function.prototype.MyCall = function(contex){
context.fn = this;//this指向调用call方法的函数
context.fn();
delete context.fn;
}
对于call函数可以根据给定参数指向函数的特性,我们可以通过arguments收集参数;
var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
所以我们的最终版函数就是:
//最终版函数
Function.prototype.MyCall = function(contex){
context = context || window;
context.fn = this;//this指向调用call方法的函数
var args = [];
for(var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
let result = eval('context.fn('+args+')');//eval自动把args转化为字符串。
delete context.fn;
return result;//call函数可以有返回值
}
apply与call函数类似,区别在与第二个参数是个数组。
//最终版函数
Function.prototype.MyApply = function(contex,arr){
context = context || window;
context.fn = this;//this指向调用call方法的函数
let result;
if(!arr){
result = context.fn()
}else{
var args = [];
for(var i = 0, len = arr.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
result = eval('context.fn(' + args + ')')
}
delete context.fn;
return result;//call函数可以有返回值
}
模拟实现bind
bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数
关于this,我们可以直接通过call或者apply来实现。
Function.prototype.MyBind = function (context){
const self = this;
return function (){
return self.apply(context)//函数可以有返回值
}
}
对于参数可以使用arguments来实现
Function.prototype.MyBind = function (context){
const self = this;
//bind函数的参数
const args = Array.prototype.slice.call(arguments,1);
return function (){
//生成函数运行时候的参数
const bingArgs = Array.prototype.slice.call(arguments);
return self.apply(context,args.concat(bingArgs))//函数可以有返回值
}
}
bind还有个最重要的特点:一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。
当bind返回的函数作为构造函数的时候,bind的this失效,但是传入的参数依然可用。
这个时候我们需要通过修改原型来实现这个方法:
//最终版函数
Function.prototype.MyBind = function (context){
if(typeof this !== "function"){
throw new Error("绑定this必须是个function")
}
var self = this;
//bind函数的参数
var args = Array.prototype.slice.call(arguments,1);
var fNOP = function() {};//用来中转,避免修改fBound的prototype的时候,也会修改绑定函数的prototype;
var fBound = function (){
//生成函数运行时候的参数
var bingArgs = Array.prototype.slice.call(arguments);
return self.apply(this instanceof fNOP?this:context,args.concat(bingArgs))//函数可以有返回值
}
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
}
参考文档: