写在前面
JavaScript 中的作用域scope 和上下文 context 是这门语言的独到之处,每个函数有不同的变量上下文和作用域。这些概念是JavaScript 中一些强大的设计模式的后盾。在ES5规范里,我们可以遵循一个原则——每个function 内的上下文this 指向该function 的调用方。比如:
var Module = {
name: 'Jafeney',first: function() {
console.log(this); // this对象指向调用该方法的Module对象
var second = (function() {
console.log(this) // 由于变量提升,this对象指向Window对象
})()
},init: function() {
this.first()
}
}
Module.init()
但是,在ES6规范中,出现了一个逆天的箭头操作符 => ,它可以替代原先ES5里function 的作用,快速声明函数。那么,在没有了function 关键字,箭头函数内部的上下文this 是怎样一种情况呢?
ES6 中的箭头函数
在阮一峰老师的《ECMAScript 6 入门》 中,对箭头函数的做了如下介绍:
箭头函数的基本介绍
ES6 允许使用“箭头”=> 定义函数。
var f = v => v;
//上面的箭头函数等同于:
var f = function(v) {
return v;
};
-
如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分 var f = () => 5;
// 等同于
var f = function () { return 5 };
var sum = (num1,num2) => num1 + num2;
// 等同于
var sum = function(num1,num2) {
return num1 + num2;
};
-
如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return 语句返回(重要) var sum = (num1,num2) => { return num1 + num2; }
-
由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号(重要) var getTempItem = id => ({ id: id,name: "Temp" });
-
箭头函数可以与变量解构结合使用 const full = ({ first,last }) => first + ' ' + last;
// 等同于
function full(person) {
return person.first + ' ' + person.last;
}
-
箭头函数使得表达更加简洁 const isEven = n => n % 2 == 0;
const square = n => n * n;
上面代码只用了两行,就定义了两个简单的工具函数。如果不用箭头函数,可能就要占用多行,而且还不如现在这样写醒目。
-
箭头函数的一个用处是简化回调函数 // 正常函数写法
[1,2,3].map(function (x) {
return x * x;
});
// 箭头函数写法
[1,3].map(x => x * x);
箭头函数使用注意点
函数体内的this 对象,就是定义时所在的对象,而不是使用时所在的对象。
不可以当作构造函数,也就是说,不可以使用new 命令,否则会抛出一个错误。
不可以使用arguments 对象,该对象在函数体内不存在。如果要用,可以用Rest参数代替。
不可以使用yield 命令,因此箭头函数不能用作Generator 函数。
this 指向固定化
ES5规范中,this 对象的指向是可变的,但是在ES6的箭头函数中,它却是固定的。
function foo() {
setTimeout(() => {
console.log('id:',this.id);
},100);
}
var id = 21;
foo.call({ id: 42 }); // 输出 id: 42
注意:上面代码中,setTimeout 的参数是一个箭头函数,这个箭头函数的定义生效是在foo 函数生成时,而它的真正执行要等到100毫秒后。如果是普通函数,执行时this 应该指向全局对象window ,这时应该输出21。但是,箭头函数导致this总是指向函数定义生效时所在的对象(本例是{id: 42} ),所以输出的是42。
箭头函数的原理
this 指向的固定化,并不是因为箭头函数内部有绑定this 的机制,实际原因是箭头函数根本没有自己的this ,导致内部的this 就是外层代码块的this 。正是因为它没有this ,所以也就不能用作构造函数。所以,箭头函数转成ES5的代码如下:
// ES6
function foo() {
setTimeout(() => {
console.log('id:',100);
}
// ES5
function foo() {
var _this = this;
setTimeout(function () {
console.log('id:',_this.id);
},100);
}
上面代码中,转换后的ES5版本清楚地说明了,箭头函数里面根本没有自己的this ,而是引用外层的this 。
两道经典的面试题
// 请问下面有几个this
function foo() {
return () => {
return () => {
return () => {
console.log('id:',this.id);
};
};
};
}
var f = foo.call({id: 1});
var t1 = f.call({id: 2})()(); // 输出 id: 1
var t2 = f().call({id: 3})(); // 输出 id: 1
var t3 = f()().call({id: 4}); // 输出 id: 1
上面代码之中,其实只有一个this ,就是函数foo的this ,所以t1、t2、t3都输出同样的结果。因为所有的内层函数都是箭头函数,都没有自己的this ,它们的this其实都是最外层foo函数的this。另外,由于箭头函数没有自己的this,所以也不能用call() 、apply() 、bind() 这些方法去改变this的指向。
// 请问下面代码执行输出什么
(function() {
return [
(() => this.x).bind({ x: 'inner' })()
];
}).call({ x: 'outer' });
上面代码中,箭头函数没有自己的this ,所以bind 方法无效,内部的this 指向外部的this 。所以上面的代码最终输出 ['outer'] 。
函数绑定 ::
箭头函数可以绑定this 对象,大大减少了显式绑定this对象的写法(call 、apply 、bind )。但是,箭头函数并不适用于所有场合,所以ES7提出了“函数绑定”(function bind )运算符,用来取代call 、apply 、bind 调用。虽然该语法还是ES7的一个提案,但是Babel转码器已经支持。
函数绑定运算符是并排的两个双冒号(:: ),双冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象,作为上下文环境(即this 对象),绑定到右边的函数上面。
foo::bar;
// 等同于
bar.bind(foo);
foo::bar(...arguments);
// 等同于
bar.apply(foo,arguments);
const hasOwnProperty = Object.prototype.hasOwnProperty;
function hasOwn(obj,key) {
return obj::hasOwnProperty(key);
}
如果双冒号左边为空,右边是一个对象的方法,则等于将该方法绑定在该对象上面。
var method = obj::obj.foo;
// 等同于
var method = ::obj.foo;
let log = ::console.log;
// 等同于
var log = console.log.bind(console);
由于双冒号运算符返回的还是原对象,因此可以采用链式写法。
// 例一
import { map,takeWhile,forEach } from "iterlib";
getPlayers()
::map(x => x.character())
::takeWhile(x => x.strength > 100)
::forEach(x => console.log(x));
// 例二
let { find,html } = jake;
document.querySelectorAll("div.myClass")
::find("p")
::html("hahaha");
React 中的各种 this
目前React 的编写风格已经全面地启用了ES6和部分ES7规范,所以很多ES6的坑在React 里一个个浮现了。本篇重点介绍 this ,也是近期跌得最疼的一个。
Component 方法内部的 this
还是用具体的例子来解释吧,下面是我 Royal 项目里一个Table 组件(Royal 正在开发中,欢迎fork 贡献代码 ^_^)
import React,{ Component } from 'react'
import Checkbox from '../../FormControls/Checkbox/'
import './style.less'
class Table extends Component {
constructor(props) {
super(props)
this.state = {
dataSource: props.dataSource || [],columns: props.columns || [],wrapClass: props.wrapClass || null,wrapStyle: props.wrapStyle || null,style: props.style || null,className: props.className || null,}
this.renderRow = props.renderRow || null
}
onSelectAll() {
for (let ref in this.refs) {
if (ref!=='selectAll') {
this.refs[ref].setState({checked:true})
}
}
}
offSelectAll() {
for (let ref in this.refs) {
if (ref!=='selectAll') {
this.refs[ref].setState({checked:false})
}
}
}
_renderHead() {
return this.state.columns.map((item,i) => {
return [<th>{i===0?<Checkbox ref="selectAll" onConfirm={()=>this.onSelectAll()} onCancel={()=>this.offSelectAll()} />:''}{item.title}</th>]
})
}
_renderBody() {
let _renderRow = this.renderRow;
return this.state.dataSource.map((item) => {
return _renderRow && _renderRow(item)
})
}
render() {
let state = this.state;
return (
<div className={state.wrapClass} style={state.wrapStyle}>
<table
border="0"
style={state.style}
className={"ry-table " + (state.className && state.className : "")}>
<thead>
<tr>{this._renderHead()}</tr>
</thead>
<tbody>
{this._renderBody()}
</tbody>
</table>
</div>
)
}
}
export default Table
Component 是React 内的一个基类,用于继承和创建React 自定义组件。ES6规范下的面向对象实现起来非常精简,class 关键字 可以快速创建一个类,而Component 类内的所有属性和方法均可以通过this 访问。换而言之,在Component 内的任意方法内,可以通过this.xxx 的方式调用该Component 的其他属性和方法。
接着分析上面的代码,寥寥几行实现的是对一个Table组件的封装,借鉴了ReactNative 组件的设计思路,通过外部传递dataSource (数据源)、columns (表格的表头项)、renderRow (当行渲染的模板函数)来完成一个Table的构建,支持全选和取消全选的功能、允许外部传递className 和style 对象来修改样式。
从这个例子我们可以发现:只要不采用function 定义函数,Component 所有方法内部的this 对象始终指向该类自身。
container 调用 component 时传递的 this
还是继续上面的例子,下面在一个做为Demo的container 里调用之前 的Table 。
import Table from '../../components/Views/Table/'
接着编写renderRow 函数并传递给Table组件
_renderRow(row) {
// ------------ 注意:这里对callback函数的写法 -----------
let onEdit = (x)=> {
console.log(x+x)
},onDelete = (x)=> {
console.log(x*x)
}
// ---------------------------------------------------
return (
<tr>
<td><Checkbox ref={"item_" + row.key} />{row.key}</td>
<td>{row.name}</td>
<td>{row.age}</td>
<td>{row.birthday}</td>
<td>{row.job}</td>
<td>{row.address}</td>
<td>
<Button type="primary" callback={()=>onEdit(row.key)} text="编辑" />
<Button type="secondary" callback={()=>onDelete(row.key)} text="删除" />
</td>
</tr>
)
}
//... 省略一大堆代码
render() {
let dataSource = [{
key: '1',name: '胡彦斌',age: 32,birthday: '2016-12-29',job: '前端工程师',address: '西湖区湖底公园1号'
},{
key: '2',name: '胡彦祖',age: 42,address: '西湖区湖底公园1号'
}],columns = [{
title: '编号',dataIndex: 'key',key: 'key',},{
title: '姓名',dataIndex: 'name',key: 'name',{
title: '年龄',dataIndex: 'age',key: 'age',{
title: '生日',dataIndex: 'birthday',key: 'birthday',{
title: '职务',dataIndex: 'job',key: 'job',{
title: '住址',dataIndex: 'address',key: 'address',{
title: '操作',dataIndex: 'operate',key: 'operate',}];
return (
<div>
<Table dataSource={dataSource} columns={columns} renderRow={this._renderRow}/>
</div>
);
}
显示效果如下:
分析上面的代码,有几处容易出错的地方:
_renderRow 作为component 的方法来定义,然后在对应的render 函数内通过this 来调用。很重要的一点,这里this._renderRow 作为的是函数名方式传递。
-
_renderRow 内部Button 组件的callback 是按钮点击后触发的回调,也是一个函数,但是这个函数没有像上面一样放在component 的方法里定义,而是作为一个变量定义并通过匿名函数的方式传递给子组件: let onEdit = (x)=> {
console.log(x+x)
}
// .....
callback={()=>onEdit(row.key)} 这样就避开了使用this 时上下文变化的问题。这一点是很讲究的,如果沿用上面的写法很容易这样写: onEdit(x) {
console.log(x+x)
}
// ...
callback={()=>this.onEdit(row.key)} 但是很遗憾,这样写this 传递到子组件后会变成undefined ,从而报错。
-
父组件如要调用子组件的方法,有两种方式:
注意:如果要绑定的函数需要传参数,可以这么写: xxx.bind(this,arg1,arg2...)
参考
《ECMAScript 6 入门》
@欢迎关注我的 github 和 个人博客 -Jafeney (编辑:李大同)
【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!
|