深入学习js函数的隐式参数 arguments 和 this


Posted in Javascript onJune 24, 2019

前言

在函数调用时,arguments和this会被静默的传递给函数,并可以在函数体内引用它们,借以访问函数相关的一些信息。
其中arguments是一个类数组结构,它保存了调用时传递给函数的所有实参;this是函数执行时的上下文对象, 这个对象有些让人感到困惑的行为。 下面分别对他们进行讨论。

1. arguments

1.1 背景

JavaScript 允许函数在调用时传入的实参个数和函数定义时的形参个数不一致, 比如函数在定义时声明了 n 个参数, 在调用函数时不一定非要传入 n 个参数,例如:

// 1. 定义有一个形参的函数fn()
function fn(arg){}
// 2. 在调用时传入 0 个或 多个参数,并不会报错
fn(); // 传入 0 个参数
fn(1,'a',3); // 传入多个参数

1.2 arguments 与 形参的对应关系

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]来获取了, 例如:

// 定义只有一个形参的函数
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

1.3 arguments 与 形参的值相互对应

在非严格模式下, 修改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和形参的值之间失去了对应的关系:

'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 的行为和属性虽然很像数组, 但它并不是数组,只是一种类数组结构:

function fn(){
console.log(typeof arguments); // object
console.log(arguments instanceof Array); // false
}
fn();

1.4 为什么要了解 arguments

在ES6中, 可以用灵活性更强的解构的方式(...符号)获得函数调用时传入的实参,而且通过这种方式获得的实参是保存在真正的数组中的,例如:

function fn(...args){ // 通过解构的方式得到实参
console.log(args instanceof Array); // args 是真正的数组
console.log(args); // 而且 args 中也保存了传入的实参
}
fn(1,2,3);
// true
// Array(3) [1, 2, 3]

那么在有了上面这种更加灵活的方式以后,为什么还要了解arguments呢? 原因是在维护老代码的时候可能不得不用到它。

2. 函数上下文: this

在函数调用时, 函数体内也可以访问到 this 参数, 它代表了和函数调用相关联的对象,被称为函数上下文。
this的指向受到函数调用方式的影响, 而函数的调用方式可以分成以下4种:

  1. 直接调用, 例如: fn()
  2. 作为对象的方法被调用, 例如: obj.fn()
  3. 被当做一个构造函数来使用, 例如: new Fn()
  4. 通过函数 call() 或者 apply() 调用, 例如: obj.apply(fn) / obj.call(fn)

下面分别讨论以上 4 种调用方式下 this 的指向.

2.1 直接调用一个函数时 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.

2.2 被一个对象当做方法调用

当函数被一个对象当成方法调用时, 这个函数的 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()); // 小明 小明

2.3 被作为构造函数来调用时

2.3.1 不要像使用普通函数一样使用构造函数

构造函数本质上是函数, 只是在被 new 操作符调用时一个函数才被称为构造函数。然而话虽如此, 但是由于写出一个构造函数的目的是用他来创建一个对象, 所以还要有一些约定俗成的东西来限制这个概念, 避免把构造函数当成普通函数来使用。例如, 构造函数虽然能被直接调用, 但是不要这样做,因为这是一个普通函数就可以做到的事情,例如:

function Person(name){
this.name = name;
return 1; // 不要这样对待构造函数
}
let n = Person(); // 不要这样使用构造函数

2.3.2 使用构造函数创建对象时发生了什么
当使用 new 关键字来调用构造函数的最终结果是产生了一个新对象, 而产生新对象的过程如下:

  1. 创建一个空对象 {}
  2. 将该对象的prototype链接到构造函数的prototype上
  3. 将这个新对象作为 this 的指向
  4. 如果这个构造函数没有返回一个引用类型的值, 则将上面构造的新对象返回

上面的内容如果需要完全理解, 还需要了解原型相关的内容。这里只需要关注第3、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

2.3.3 结论

从上面的内容可以得到如下的结论: 当函数作为构造函数使用时, this 指向返回的新对象

2.4 通过 call() 或者 apply() 调用时

使用函数 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部分 的内容得到。

2.4.1 call 和 apply 的区别

call 和 apply 的区别仅仅是要传给fn的参数的形式不同:对于apply,传给fn的参数argument是个数组,数组由所有参数组成;对于call,传给fn的参数argument直接是所有参数的排列, 直接一个个写入就可以。

例如要传给函数fn三个参数: 1、2、3. 则对于 call和apply调用的方法分别是:

fn.call(targetThis, 1, 2, 3); // 把 1,2,3直接传入
fn.apply(targetThis, [1,2,3]); // 把1,2,3合成数组后作为参数

2.5 箭头函数 和 bind 函数

箭头函数和bind函数对于this的处理与普通函数不同, 要单独拿出来说。

2.5.1 箭头函数

与传统函数不同, 箭头函数本身不包含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指向的来源, 不要像这样使用构造函数。

2.5.2 bind函数

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 相关文章推荐
jquery ui resizable bug解决方法
Oct 26 Javascript
js使用函数绑定技术改变事件处理程序的作用域
Dec 26 Javascript
js内存泄露的几种情况详细探讨
May 31 Javascript
js取模(求余数)隔行变色
May 15 Javascript
jQuery实现“扫码阅读”功能
Jan 21 Javascript
深入分析Javascript跨域问题
Apr 17 Javascript
工作中常用的js、jquery自定义扩展函数代码片段汇总
Dec 22 Javascript
jQuery实现弹窗居中效果类似alert()
Feb 27 Javascript
javaScript实现滚动条事件详解
Mar 24 Javascript
微信小程序在text文本实现多种字体样式
Nov 08 Javascript
JS深入学习之数组对象排序操作示例
May 01 Javascript
Echarts在Taro微信小程序开发中的踩坑记录
Nov 09 Javascript
前端天气插件tpwidget使用方法详解
Jun 24 #Javascript
JavaScript深入V8引擎以及编写优化代码的5个技巧
Jun 24 #Javascript
JS实现给数组对象排序的方法分析
Jun 24 #Javascript
新手快速入门JavaScript装饰者模式与AOP
Jun 24 #Javascript
Electron + vue 打包桌面操作流程详解
Jun 24 #Javascript
JS字符串常用操作方法实例小结
Jun 24 #Javascript
新手入门带你学习JavaScript引擎运行原理
Jun 24 #Javascript
You might like
PHP Zip压缩 在线对文件进行压缩的函数
2010/05/26 PHP
php Xdebug的安装与使用详解
2013/06/20 PHP
PHP保存session到memcache服务器的方法
2016/01/19 PHP
php简单中奖算法(实例)
2017/08/15 PHP
JavaScript 精粹读书笔记(1,2)
2010/02/07 Javascript
js 幻灯片的实现
2011/12/06 Javascript
javascript的propertyIsEnumerable()方法使用介绍
2014/04/09 Javascript
JavaScript中string对象
2015/06/12 Javascript
JavaScript实现表格快速变色效果代码
2015/08/19 Javascript
使用 JavaScript 进行函数式编程 (一) 翻译
2015/10/02 Javascript
JS实现状态栏跑马灯文字效果代码
2015/10/24 Javascript
jQuery使用deferreds串行多个ajax请求
2016/08/22 Javascript
探讨跨域请求资源的几种方式(总结)
2016/12/02 Javascript
jquery 删除节点 添加节点 找兄弟节点的简单实现
2016/12/07 Javascript
原生js实现鼠标跟随效果
2017/02/28 Javascript
利用Javascript裁剪图片并存储的简单实现
2017/03/13 Javascript
微信小程序request出现400的问题解决办法
2017/05/23 Javascript
JavaScript之underscore_动力节点Java学院整理
2017/07/03 Javascript
jQuery使用ajax_动力节点Java学院整理
2017/07/05 jQuery
详解Vue双向数据绑定原理解析
2017/09/11 Javascript
vue+VeeValidate 校验范围实例详解(部分校验,全部校验)
2018/10/19 Javascript
微信小程序环境下将文件上传到OSS的方法步骤
2019/05/31 Javascript
Vue+Koa2 打包后进行线上部署的教程详解
2019/07/31 Javascript
React-redux实现小案例(todolist)的过程
2019/09/29 Javascript
python导入不同目录下的自定义模块过程解析
2019/11/18 Python
使用CSS3 制作一个material-design 风格登录界面实例
2016/12/12 HTML / CSS
美国木工工具和用品商店:Woodcraft
2019/10/30 全球购物
前台文员我鉴定
2014/01/12 职场文书
情人节活动策划方案
2014/02/27 职场文书
2014年人事专员工作总结
2014/11/19 职场文书
2015年文明创建工作总结
2015/04/30 职场文书
入党积极分子半年考察意见
2015/06/02 职场文书
舌尖上的中国观后感
2015/06/02 职场文书
叶问观后感
2015/06/15 职场文书
七一慰问简报
2015/07/20 职场文书
《半截蜡烛》教学反思
2016/02/19 职场文书