this 的概念与指向
this 的概念
通常来讲,this 的值是在执行的时候才能确认的,定义的时候不能确认。因为 this 是执行上下文环境的一部分,而执行上下文需要在代码执行前确定,而不是定义的时候。所以 this 永远指向最后调用它的那个对象。
但,这只是通常来讲。apply、call、bind、箭头函数都会改变 this 的指向。
作为一个函数调用
// 情况1
function foo() {
console.log(this.a) //1
}
var a = 1
foo() //this -> window
作为方法调用
// 情况2
function fn(){
console.log(this);
}
var obj={fn:fn};
obj.fn(); //this -> obj
构造函数中
// 情况3
function CreateJsPerson(name,age){
// this是当前类的一个实例p1
this.name = name; // => p1.name=name
this.age = age; // => p1.age=age
}
var p1 = new CreateJsPerson("尹华芝",48);
call、apply、bind
// 情况4
function add(c, d){
return this.a + this.b + c + d;
}
var o = {a:1, b:3};
add.call(o, 5, 7); // 1 + 3 + 5 + 7 = 16 //这里使用了 call 对 this 进行了重定向
add.apply(o, [10, 20]); // 1 + 3 + 10 + 20 = 34 //这里使用了 apply 对 this 进行了重定向
箭头函数
// 情况5
<button id="btn1">箭头函数this</button>
<script type="text/javascript">
let btn1 = document.getElementById('btn1');
let obj = {
name: 'kobe',
age: 39,
getName: function () {
btn1.onclick = () => {
console.log(this);//obj
};
}
};
obj.getName();
</script>
箭头函数在自己的作用域内不绑定 this,即没有自己的 this,如果要使用 this ,就会指向定义时所在的作用域的 this 值。
在 《ES6 标准入门中》:箭头函数的this,总是指向定义时所在的对象,而不是运行时所在的对象。
由于箭头函数的 this 是在定义时确定的,所以我们不能在构造函数中使用箭头函数,构造函数的 this 要指向实例才行,因此只能使用一般的函数。
总结
- 对于直接调用 foo 来说,不管 foo 函数被放在了生命地方,this 一定是 window
- 对于 obj.foo() 来说,我们要记住,谁调用了函数,谁就是 this,所以在这个场景下,foo 函数中的 this 就是 obj 对象
- 在构造函数模式中,类中(函数体中)出现的 this.xxx = xxx 中的 this 就是当前类的一个实例
- call、apply、bind 的 this 是第一个参数(call 的接收和 apply 不同,apply 接收数组,而 call 则是用“,”分隔,进行接收)
- 箭头函数没有自己的this,看其外层是否有函数,如果有,外层函数的 this 就是内部箭头函数的 this,如果没有,this 就是 window。需要注意的是:箭头函数的 this 始终指向函数定义时的 this,而非执行时。
call、apply 与 bind
call、apply、bind 本质都是改变 this 的指向,不同点 call、apply 是直接调用函数,bind 是返回一个新的函数。call 跟 apply 就只有参数上不同。
call 手写代码
call() 让函数执行,第一个参数让 this 的指向改为传进去的参数,后面的当参数传进函数里面。
返回值为原函数的返回值,如果不传第一个参数为 this 就指向 window。
ES6 版:
Function.prototype.ca112 = function (context, ...arrs ){
context = context || window; // 因为传递过来的 context 很可能是 null
context.fn = this; // 让 fn 的上下文是 context
const result = context.fn(.. .arrs);
delete context.fn;
return result;
}
ES5 版:
Function.prototype. call2 = function (context) {
var context = context || window; // 因为传递过来的 context 很可能是 null
context.fn = this;
var args = [];
for (var i = 1; i< arguments.length; i++){
// 不这样的话,字符串的引号会被去掉,变成变量
args.push("arguments[" + i + "]");
}
args = args.join(","); // 把数组变成字符串
// 相当于执行 context.fn(arguments[1], arguments[2])
var result = eval("contest.fn(" + args + ")");
delete context.fn;
return result;
}
ES5 版本的使用 eval 来执行语句,这样会又一定的性能影响,但是这样做兼容性好
因为不知道会输入多少个,所以这里直接使用 arguments 来遍历好了,先把 arguments 转成数组,再转成字符串,然后利用 eval 执行代码(看见网上说 eval 有安全性问题,不过这里这样就够了。)
apply 手写代码
ES6 版:
Function.prototype.apply2 = function(context, arr) {
context = context || window; // 因为传递的可能是 null
context.fn = this; // 让 fn 的上下文成为 context
arr = arr || [];
const result = context.fn(...arr);
delete context.fn;
return result; // 因为有可能 this 函数会有返回值
};
ES5 版:
Function.prototype.apply2 = function(context, arr) {
var context = context || window;
context.fn = this;
var args = [];
var params = arr || [];
for(var i = 0; i < params.length; i++) {
args.push("params[" + i + "]");
}
args = args.join(",");
var result = eval("contest.fn(" + args + ")");
delete context.fn;
return result;
}
bind 手写代码
bind 是封装了 call 的方法改变了 this 的指向并返回一个新的函数
ES6 版:
Function.prototype.bind2 = function(context, ...arrs) {
let _this = this;
return function() {
_this.call(context, ...arrs, ...arguments);
}
}
ES5 版:
Function.prototype.bind2 = function(context) {
var _this = this;
var argsParent = Array.prototype.slice.call(arguments, 1);
return function() {
var args = argsParent.concat(Array.prototype.slice.call(arguments));
_this.apply(context, args);
};
}
三者的同异性总结
call、apply、bind
- 三者都是用来改变函数的 this 对象的指向的
- 第一个参数都是 this 要指向的对象
- 都可以利用后续参数进行传参
- 参数传递
call 方法传参是传一个或多个参数,第一个参数是指定的对象
func.call(thisArg,arg1,arf2,……)
apply 方法传参是传一个或两个对象,第一个参数是指定的对象,第二个参数是一个数组或类数组(说到类数组就想起了 arguments)
func.apply(thisArg,【argsArray】)
bind 方法传参是传一个或者多个参数,跟 call 方法传递参数一样。
func.bind(this.thisArg,arg1,arg2,arg3……)
- 调用后是否立即执行
call 和 apply 在函数调用它们之后,就会立即执行这个函数;
而函数调用了 bind 后,会返回调用函数的引用,如果要执行的话,需要执行返回函数的引用。
let name = 'window name';
let obj = {
name: 'call_me_R'
};
function sayName() {
console.log(this.name);
}
sayName(); // window name
sayName.call(obj); // call_me_R
sayName.apply(obj); // call_me_R
let _sayName = sayName.bind(obj);
_syaName(); // call_me_R
执行的区别在与 ball 和 apply 都是立即执行的,bind 会返回回调函数,手动执行回调函数以执行。
new 与 Object.create()
new 做的事情
New 关键字会进行如下的操作
-
创建一个空的简单 JavaScript 对象(即 {})
-
链接该对象(即设置该对象的构造函数)到另一个对象
-
将创建的新的对象作为 this 的上下文
-
如果该函数没有返回对象,则返回 this
-
new 会创建一个新的对象,并且这个新对象继承构造函数的 prototype,也就是说创建的实例的 proto 指向构造函数的 prototype
-
new Object()会创建一个实例,该实例的 proto 指向 Object 的 prototype
手写 new 四个步骤:
- 创建一个空对象,并且 this 变量引用该对象
- 继承函数的原型
- 属性和方法加入 this 引用的对象中,并执行函数
- 新创建的对象有 this 所引用,并且最后隐式返回 this
function _new(func){
let target = {},
target.__proto__ = func.prototype;
let res = func.call(target);
if(typeof(res)=='object' || typeof(res)=='function'){
return res;
}
return target;
}
简单来说:
new 做的三件事情:
- 指定 prototype
- 用 call 调用对象
- 返回 this
function myNew (fun) {
return function () {
// 创建一个新对象且将其隐式原型指向构造函数原型
let obj = {
__proto__ : fun.prototype
}
// 执行构造函数
fun.call(obj, ...arguments)
// 返回该对象
return obj
}
}
function person(name, age) {
this.name = name
this.age = age
}
let obj = myNew(person)('chen', 18) // {name: "chen", age: 18}
Object.Create() 基本实现及其原理
// 思路:将传入的对象作为原型
function create(obj) {
function F() {}
F.prototype = obj
return new F()
}
Object.Create 是创建了一个新的对象并返回,这个新对象的原型指向了拷贝的对象,当我们通过 b.a 访问 obj.a 时,是通过原型进行访问的。
但是要注意的是,Object.Create 并不是深拷贝,Object.Create() 新建的对象共享的是拷贝的对象的引用类型的地址(浅拷贝)。
所以如果修改的是引用类型,还是会变化。
由于 Object.create() 还可以传递第二个参数,所以更好的实现是:
function myCreate(proto, properties) {
// 新对象
let fn = function() {};
fn.prototype = proto;
if(properties) {
// defineProperties 在新对象上定义新的属性或修改现有属性
Object.defineProperties(fn, properties)
}
return new fn();
}
new 与 object.create 的异同
- new Object() 继承内置对象 Object,Object.create 继承指定对象
- 可以通过 Object.create(null) 创建一个干净的对象,也就是没有原型,而 new Object() 创建的对象是 Object 的实例,原型永远指向 Object.prototype
Object.create 接受两个参数,即 object.create(proto,propertiesObject)
proto:现有的对象,即新对象的原型对象(新创建的对象 proto 将指向该对象)。如果 proto 为 null,那么创建出来的对象是一个 {} 并且没有原型。
propertiesObject 可选,给新对象添加新属性以及描述器。如果没有指定即创建一个 {},有原型也有继承 Object.prototype 上的方法。可参考 Object.defineProperties()的第二个参数。