在函数调用时,arguments和this会被静默的传递给函数,并能够在函数体内引用它们,借以访问函数相关的一些信息。
其中arguments是一个类数组结构,它保存了调用时传递给函数的全部实参;this是函数执行时的上下文对象, 这个对象有些让人感到困惑的行为。 下面分别对他们进行讨论。javascript
JavaScript 容许函数在调用时传入的实参个数和函数定义时的形参个数不一致, 好比函数在定义时声明了 n 个参数, 在调用函数时不必定非要传入 n 个参数,例如:java
// 1. 定义有一个形参的函数fn()
function fn(arg){}
// 2. 在调用时传入 0 个或 多个参数,并不会报错
fn(); // 传入 0 个参数
fn(1,'a',3); // 传入多个参数
复制代码
arguments是个类数组结构,它存储了函数在调用时传入的全部实参, 经过访问它的length
属性能够获得其中保存的实参的个数,并能够经过arguments[n]
按顺序取出传入的每一个参数(n=1,2,..,arguments.length-1
)。数组
参数在arguments中保存的顺序和传入的顺序相同, 同时也和形参声明的顺序相同,例如:浏览器
function fn(arg1, arg2, arg3){
console.log(arg1 === arguments[0]); // true
console.log(arg2 === arguments[1]); // true
console.log(arg3 === arguments[2]); // true
}
fn(1,2,3); // 调用
复制代码
当传入的实参多于形参个数时,想要得到多余出的实参,就能够用arguments[n]来获取了, 例如:app
// 定义只有一个形参的函数
function fn(arg1){
console.log('length of arguments is:',arguments.length);
console.log('arguments[0] is:', arguments[0]); // 获取传入的第一个实参, 也就是形参 arg1 的值
console.log('arguments[1] is:', arguments[1]); // 获取第二个实参的值, 没有形参与其对应
console.log('arguments[2] is:', arguments[2]); // 获取第二个实参的值, 没有形参与其对应
}
fn(1,2,3); // 传入 3 个实参
// 能够获得实际上传入的实参的个数并取出全部实参
// length of arguments is: 3
// arguments[0] is: 1
// arguments[1] is: 2
// arguments[2] is: 3
复制代码
在非严格模式下, 修改arguments中的元素值会修改对应的形参值;一样的,修改形参的值也会修改对应的arguments中保存的值。下面的实验能够说明:函数
function fn(arg1, arg2){
// 1. 修改arguments元素,对应的形参也会被修改
arguments[0] = '修改了arguments';
console.log(arg1);
// 2. 修改形参值,对应的arguments也会被修改
arg2 = '修改了形参值';
console.log(arguments[1]);
}
fn(1,2);
// '修改了arguments'
// '修改了形参值'
复制代码
可是,在严格模式下不存在这种状况, 严格模式下的arguments和形参的值之间失去了对应的关系:ui
'use strict'; // 启用严格模式
function fn(arg1, arg2){
// 修改arguments元素,对应的形参也会被修改
arguments[0] = '修改了arguments';
console.log(arg1);
// 修改形参值,对应的arguments也会被修改
arg2 = '修改了形参值';
console.log(arguments[1]);
}
fn(1,2);
// 1
// 2
复制代码
注意: arguments 的行为和属性虽然很像数组, 但它并非数组,只是一种类数组结构:this
function fn(){
console.log(typeof arguments); // object
console.log(arguments instanceof Array); // false
}
fn();
复制代码
在ES6中, 能够用灵活性更强的解构的方式(...符号)得到函数调用时传入的实参,并且经过这种方式得到的实参是保存在真正的数组中的,例如:spa
function fn(...args){ // 经过解构的方式获得实参
console.log(args instanceof Array); // args 是真正的数组
console.log(args); // 并且 args 中也保存了传入的实参
}
fn(1,2,3);
// true
// Array(3) [1, 2, 3]
复制代码
那么在有了上面这种更加灵活的方式之后,为何还要了解arguments呢? 缘由是在维护老代码的时候可能不得不用到它。prototype
在函数调用时, 函数体内也能够访问到 this 参数, 它表明了和函数调用相关联的对象,被称为函数上下文。
this的指向受到函数调用方式的影响, 而函数的调用方式能够分红如下4种:
下面分别讨论以上 4 种调用方式下 this 的指向.
有些资料说在直接调用一个函数时, 这个函数的 this 指向 window, 这种说法是片面的, 只有在非严格模式下并且是浏览器环境下才成立, 更准确的说法是:在非严格模式下, this值会指向全局上下文(例如在浏览器中是window, Node.js环境下是global)。而在严格模式下, this 的值是 undefined。实验代码以下:
// 非严格模式
function fn(){
console.log(this);
}
fn(); // global || Window
复制代码
严格模式下:
'use strict';
function fn(){
console.log(this);
}
fn(); // undefined
复制代码
总结: 在直接调用一个函数时, 它的 this 指向分红两种状况: 在非严格模式下指向全局上下文, 在严格模式下指向
undefined
.
当函数被一个对象当成方法调用时, 这个函数的 this 会指向调用它的对象。代码验证以下:
// 定义一个对象
let xm = {
getThis (){ // 定义一个函数
return this; // 这个函数返回本身的 this 指向
}
}
let thisOfFunc = xm.getThis(); // 经过对象调用函数获得函数的 this 指向
console.log(thisOfFunc === xm); // true, 函数的this指向调用它的对象自己
复制代码
由于这个缘由, 对象的属性能够经过this来访问, 若是给 xm 加上一个 name 属性, 则经过 xm.name能够获得这个属性值, 也能够在函数中经过 this.name 获得属性值, 即 this.name
就是 vm.name
, 进一步, this===xm
。 实验以下:
let xm = {
name: '小明', // 给 xm 加一个属性, 能够经过 xm.name 访问到
getName (){
return this.name; // 返回 this 的指向的 name 属性
}
}
console.log(xm.name, xm.getName()); // 小明 小明
复制代码
构造函数本质上是函数, 只是在被 new
操做符调用时一个函数才被称为构造函数。然而话虽如此, 可是因为写出一个构造函数的目的是用他来建立一个对象, 因此还要有一些约定俗成的东西来限制这个概念, 避免把构造函数当成普通函数来使用。例如, 构造函数虽然能被直接调用, 可是不要这样作,由于这是一个普通函数就能够作到的事情,例如:
function Person(name){
this.name = name;
return 1; // 不要这样对待构造函数
}
let n = Person(); // 不要这样使用构造函数
复制代码
当使用 new
关键字来调用构造函数的最终结果是产生了一个新对象, 而产生新对象的过程以下:
prototype
连接到构造函数的prototype
上上面的内容若是须要彻底理解, 还须要了解原型相关的内容。这里只须要关注第三、4步就能够了,即:将this绑定到生成到的新对象上,并将这个新对象返回, 进一步下结论为:使用构造函数时, this 指向生成的对象, 实验结果以下:
function Person(){
this.getThis = function(){ // 这个函数返回 this
return this;
}
}
let p1 = new Person(); // 调用了构造函数并返回了一个新的对象
console.log(p1.getThis() === p1); // true
let p2 = new Person();
console.log(p2.getThis() === p2); // true
复制代码
从上面的内容能够获得以下的结论: 当函数做为构造函数使用时, this 指向返回的新对象
使用函数 call
和 apply
能够在调用一个函数时指定这个函数的 this 的指向, 语法是:
fn.call(targetThis, arg1, arg2,..., argN)
fn.apply(targetThis, [arg1, arg2,.., argN])
fn: 要调用的函数
targetThis: 要把 fn 的 this 设置到的目标
argument: 要给 fn 传的实参
复制代码
例如定义一个对象以下:
let xm = {
name: '小明',
sayName(){
console.log(this.name);
}
};
xm.sayName(); // 对象调用函数输出 '小明'
复制代码
上面定义了一个对象, 对象的 name 属性为'小明'; sayName 属性是个函数, 功能是输出对象的 name 属性的值。根据2.2部分可知 sayName 这个函数的 this
指向 xm
对象, this.name
就是 xm.name
。下面定义一个新对象, 并把 xm.sayName 这个函数的 this 指向新定义的对象。
新定义一个对象 xh:
let xh = {
name: '小红'
};
复制代码
对象 xh
只有 name 属性, 没有 sayName 属性, 若是想让 xh 也使用 sayName 函数来输出本身的名字, 那么就要在调用 sayName 时让它的 this 指向小红, 以达到 this.name
等于 xh.name
的目的。 这个目的就能够经过 call 和 apply 两个函数来实现。 以call
函数为例来实现这个需求, 只须要这样写就能够了:
xm.sayName.call(xh); // 小红
xm.sayName.apply(xh); // 小红
复制代码
其中fn为xm.sayName; targetThis为xh, 这是由于targetThis的指向就是xh, 此结论能够由 2.2部分 的内容获得。
call 和 apply 的区别仅仅是要传给fn的参数的形式不一样:对于apply,传给fn的参数argument是个数组,数组由全部参数组成;对于call,传给fn的参数argument直接是全部参数的排列, 直接一个个写入就能够。 例如要传给函数fn三个参数: 一、二、3. 则对于 call和apply调用的方法分别是:
fn.call(targetThis, 1, 2, 3); // 把 1,2,3直接传入
fn.apply(targetThis, [1,2,3]); // 把1,2,3合成数组后做为参数
复制代码
bind
函数箭头函数和bind函数对于this的处理与普通函数不一样, 要单独拿出来讲。
与传统函数不一样, 箭头函数自己不包含this, 它的 this 继承自它定义时的做用域链的上一层。并且箭头函数不能做为构造函数,它也没有文章 第1部分 所说的arguments属性。
下面用一个例子引出箭头函数中this的来源:
function Person(){
this.age = 24;
setTimeout(function(){
console.log(this.age); // undefined
console.log(this === window); // true
}, 1000);
}
var p = new Person(); // 建立一个实例的时候就当即执行了定时器
复制代码
能够看到, 在定时器内定义的普通匿名函数没法访问到 Person 的 age 属性, 这是由于setTimeout是个全局函数, 它的内部的this指向的是window, 而 window 上没有 age 这个属性, 因此就获得了 undefined。 从下面this === window
为 true
也说明了匿名函数中this指向的是window。
将普通的函数换成箭头函数以后能够看到以下结果:
function Person(){
this.age = 24;
setTimeout(() => {
console.log(this.age); // 24
console.log(this === p); // true
}, 1000);
}
var p = new Person();
复制代码
由上面的代码能够看出箭头函数内的 this 指向实例 p, 即它的 this 指向的是定义时候的做用域链的上一层。
说明: 这个例子仅用来引出箭头函数的this指向的来源, 不要像这样使用构造函数。
bind
函数的做用是根据一个旧函数而建立一个新函数,语法为newFn = oldFn.bind(thisTarget)
。它会将旧函数复制一份做为新函数, 而后将新函数的this永远绑定到thisTarget指向的上下文中, 而后返回这个新函数, 之后每次调用这个新函数时, 不管用什么方法都没法改变这个新函数的 this 指向。例如:
// 建立一个对象有 name 和 sayName 属性
let p1 = {
name: 'P1',
sayName(){
console.log(this.name); // 访问函数指向的 this 的 name 属性
}
}
p1.sayName(); // P1
// 建立一个对象 p2, 并把这个对象做为bind函数绑定的this
let p2 = {
name: 'P2'
}
// 将p1的 sayName 函数的 this 绑定到 p2 上, 生成新函数 sayP2Name 并返回
let sayP2Name = p1.sayName.bind(p2);
// 因为此时 sayP2Name 的内部 this 已经绑定了 p2,
// 因此即便是按 文章2.1部分 所说的直接调用 sayP2Name, 它的 this 也是指向 p2 的, 并非指向全局上下文或者 undefined
sayP2Name(); // P2
// 定义新对象, 尝试将 sayP2Name 的 this 指向到 p3 上
let p3 = {
name: 'P3'
}
// 尝试使用 call和apply 函数来将 sayP2Name 函数的 this 指向p3,
// 可是因为 sayP2Name 函数的this 已经被bind函数永远绑定到p2上了, 因此this.name仍然是p2.name
sayP2Name.call(p3); // P2
sayP2Name.apply(p3); // P2
复制代码
经过以上内容可知一旦经过 bind 函数绑定了 this, 就再也没法改变 this 的指向了.
若有错误, 多谢指出!
参考文章:
<< JavaScript 忍者秘籍 >>
JavaScript new Keyword
MDN