JavaScript不具有 sleep() 函数,该函数会导致代码在恢复执行之前等待指定的时间段。如果需要JavaScript等待,该怎么做呢?
假设您想将三则消息记录到Javascript控制台,每条消息之间要延迟一秒钟。JavaScript中没有 sleep() 方法,所以你可以尝试使用下一个最好的方法 setTimeout()。
不幸的是,setTimeout() 不能像你期望的那样正常工作,这取决于你如何使用它。你可能已经在JavaScript循环中的某个点上试过了,看到 setTimeout() 似乎根本不起作用。
问题的产生是由于将 setTimeout() 误解为 sleep() 函数,而实际上它是按照自己的一套规则工作的。
在本文中,我将解释如何使用 setTimeout(),包括如何使用它来制作一个睡眠函数,使JavaScript暂停执行并在连续的代码行之间等待。
浏览一下 setTimeout() 的文档,它似乎需要一个 "延迟 "参数,以毫秒为单位。
回到原始问题,您尝试调用 setTimeout(1000) 在两次调用 console.log() 函数之间等待1秒。
不幸的是 setTimeout() 不能这样工作:
setTimeout(1000)
console.log(1)
setTimeout(1000)
console.log(2)
setTimeout(1000)
console.log(3)
for (let i = 0; i <= 3; i++) {
setTimeout(1000)
console.log(`#${i}`)
}
这段代码的结果完全没有延迟,就像 setTimeout() 不存在一样。
回顾文档,你会发现问题在于实际上第一个参数应该是函数调用,而不是延迟。毕竟,setTimeout() 实际上不是 sleep() 方法。
你重写代码以将回调函数作为第一个参数并将必需的延迟作为第二个参数:
setTimeout(() => console.log(1), 1000)
setTimeout(() => console.log(2), 1000)
setTimeout(() => console.log(3), 1000)
for (let i = 0; i <= 3; i++) {
setTimeout(() => console.log(`#${i}`), 1000)
}
这样一来,三个console.log的日志信息在经过1000ms(1秒)的单次延时后,会一起显示,而不是每次重复调用之间延时1秒的理想效果。
在讨论如何解决此问题之前,让我们更详细地研究一下 setTimeout() 函数。
检查setTimeout ()
你可能已经注意到上面第二个代码片段中使用了箭头函数。这些是必需的,因为你需要将匿名回调函数传递给 setTimeout(),该函数将在超时后运行要执行的代码。
在匿名函数中,你可以指定在超时时间后执行的任意代码:
// 使用箭头语法的匿名回调函数。
setTimeout(() => console.log("你好!"), 1000)
// 这等同于使用function关键字
setTimeout(function() { console.log("你好!") }, 1000)
理论上,你可以只传递函数作为第一个参数,回调函数的参数作为剩余的参数,但对我来说,这似乎从来没有正确的工作:
// 应该能用,但不能用
setTimeout(console.log, 1000, "你好")
人们使用字符串解决此问题,但是不建议这样做。从字符串执行JavaScript具有安全隐患,因为任何不当行为者都可以运行作为字符串注入的任意代码。
// 应该没用,但确实有用
setTimeout(`console.log("你好")`, 1000)
那么,为什么在我们的第一组代码示例中 setTimeout() 失败?好像我们在正确使用它,每次都重复了1000ms的延迟。
原因是 setTimeout() 作为同步代码执行,并且对 setTimeout() 的多次调用均同时运行。每次调用 setTimeout() 都会创建异步代码,该代码将在给定延迟后稍后执行。由于代码段中的每个延迟都是相同的(1000毫秒),因此所有排队的代码将在1秒钟的单个延迟后同时运行。
如前所述,setTimeout() 实际上不是 sleep() 函数,取而代之的是,它只是将异步代码排入队列以供以后执行。幸运的是,可以使用 setTimeout() 在JavaScript中创建自己的 sleep() 函数。
如何编写sleep函数
通过Promises,async 和 await 的功能,您可以编写一个 sleep() 函数,该函数将按预期运行。
但是,你只能从 async 函数中调用此自定义 sleep() 函数,并且需要将其与 await 关键字一起使用。
这段代码演示了如何编写一个 sleep() 函数:
const sleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay))
const repeatedGreetings = async () => {
await sleep(1000)
console.log(1)
await sleep(1000)
console.log(2)
await sleep(1000)
console.log(3)
}
repeatedGreetings()
此JavaScript sleep() 函数的功能与您预期的完全一样,因为 await 导致代码的同步执行暂停,直到Promise被解决为止。
一个简单的选择
另外,你可以在第一次调用 setTimeout() 时指定增加的超时时间。
以下代码等效于上一个示例:
setTimeout(() => console.log(1), 1000)
setTimeout(() => console.log(2), 2000)
setTimeout(() => console.log(3), 3000)
使用增加超时是可行的,因为代码是同时执行的,所以指定的回调函数将在同步代码执行的1、2和3秒后执行。
它会循环运行吗?
如你所料,以上两种暂停JavaScript执行的选项都可以在循环中正常工作。让我们看两个简单的例子。
这是使用自定义 sleep() 函数的代码段:
const sleep = (delay) => new Promise((resolve) => setTimeout(resolve, delay))
async function repeatGreetingsLoop() {
for (let i = 0; i <= 5; i++) {
await sleep(1000)
console.log(`Hello #${i}`)
}
}
repeatGreetingsLoop()
这是一个简单的使用增加超时的代码片段:
for (let i = 0; i <= 5; i++) {
setTimeout(() => console.log(`Hello #${i}`), 1000 * i)
}
我更喜欢后一种语法,特别是在循环中使用。
总结
JavaScript可能没有 sleep() 或 wait() 函数,但是使用内置的 setTimeout() 函数很容易创建一个JavaScript,只要你谨慎使用它即可。
就其本身而言,setTimeout() 不能用作 sleep() 函数,但是你可以使用 async 和 await 创建自定义JavaScript sleep() 函数。
采用不同的方法,可以将交错的(增加的)超时传递给 setTimeout() 来模拟 sleep() 函数。之所以可行,是因为所有对setTimeout() 的调用都是同步执行的,就像JavaScript通常一样。
希望这可以帮助你在代码中引入一些延迟——仅使用原始JavaScript,而无需外部库或框架。
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务
1. 随机排列
在开发者,有时候我们需要对数组的顺序进行重新的洗牌。 在 JS 中并没有提供数组随机排序的方法,这里提供一个随机排序的方法:
function shuffle(arr) {
var i, j, temp;
for (i = arr.length - 1; i > 0; i--) {
j = Math.floor(Math.random() * (i + 1));
temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
return arr;
}
2. 唯一值
在开发者,我们经常需要过滤重复的值,这里提供几种方式来过滤数组的重复值。
使用 Set 对象
使用 Set() 函数,此函数可与单个值数组一起使用。对于数组中嵌套的对象值而言,不是一个好的选择。
const numArray = [1,2,3,4,2,3,4,5,1,1,2,3,3,4,5,6,7,8,2,4,6];
// 使用 Array.from 方法
Array.from(new Set(numArray));
// 使用展开方式
[...new Set(numArray)]
使用 Array.filter
使用 filter 方法,我们可以对元素是对象的进行过滤。
const data = [
{id: 1, name: 'Lemon'},
{id: 2, name: 'Mint'},
{id: 3, name: 'Mango'},
{id: 4, name: 'Apple'},
{id: 5, name: 'Lemon'},
{id: 6, name: 'Mint'},
{id: 7, name: 'Mango'},
{id: 8, name: 'Apple'},
]
function findUnique(data) {
return data.filter((value, index, array) => {
if (array.findIndex(item => item.name === value.name) === index) {
return value;
}
})
}
3. 使用 loadsh 的 lodash 方法
import {uniqBy} from 'lodash'
const data = [
{id: 1, name: 'Lemon'},
{id: 2, name: 'Mint'},
{id: 3, name: 'Mango'},
{id: 4, name: 'Apple'},
{id: 5, name: 'Lemon'},
{id: 6, name: 'Mint'},
{id: 7, name: 'Mango'},
{id: 8, name: 'Apple'},
]
function findUnique(data) {
return uniqBy(data, e => {
return e.name
})
}
3. 按属性对 对象数组 进行排序
我们知道 JS 数组中的 sort 方法是按字典顺序进行排序的,所以对于字符串类, 该方法是可以很好的正常工作,但对于数据元素是对象类型,就不太好使了,这里我们需要自定义一个排序方法。
在比较函数中,我们将根据以下条件返回值:
小于0:A 在 B 之前
大于0 :B 在 A 之前
等于0 :A 和 B 彼此保持不变
const data = [
{id: 1, name: 'Lemon', type: 'fruit'},
{id: 2, name: 'Mint', type: 'vegetable'},
{id: 3, name: 'Mango', type: 'grain'},
{id: 4, name: 'Apple', type: 'fruit'},
{id: 5, name: 'Lemon', type: 'vegetable'},
{id: 6, name: 'Mint', type: 'fruit'},
{id: 7, name: 'Mango', type: 'fruit'},
{id: 8, name: 'Apple', type: 'grain'},
]
function compare(a, b) {
// Use toLowerCase() to ignore character casing
const typeA = a.type.toLowerCase();
const typeB = b.type.toLowerCase();
let comparison = 0;
if (typeA > typeB) {
comparison = 1;
} else if (typeA < typeB) {
comparison = -1;
}
return comparison;
}
data.sort(compare)
4. 把数组转成以指定符号分隔的字符串
JS 中有个方法可以做到这一点,就是使用数组中的 .join() 方法,我们可以传入指定的符号来做数组进行分隔。
const data = ['Mango', 'Apple', 'Banana', 'Peach']
data.join(',');
// return "Mango,Apple,Banana,Peach"
5. 从数组中选择一个元素
对于此任务,我们有多种方式,一种是使用 forEach 组合 if-else 的方式 ,另一种可以使用filter 方法,但是使用forEach 和filter的缺点是:
在forEach中,我们要额外的遍历其它不需要元素,并且还要使用 if 语句来提取所需的值。
在filter 方法中,我们有一个简单的比较操作,但是它将返回的是一个数组,而是我们想要是根据给定条件从数组中获得单个对象。
为了解决这个问题,我们可以使用 find函数从数组中找到确切的元素并返回该对象,这里我们不需要使用if-else语句来检查元素是否满足条件。
const data = [
{id: 1, name: 'Lemon'},
{id: 2, name: 'Mint'},
{id: 3, name: 'Mango'},
{id: 4, name: 'Apple'}
]
const value = data.find(item => item.name === 'Apple')
// value = {id: 4, name: 'Apple'}
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务
1. Vue 无法检测实例被创建时不存在于 data 中的 property
原因:由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。
场景:
var vm = new Vue({
data:{},
// 页面不会变化
template: '<div>{{message}}</div>'
})
vm.message = 'Hello!' // `vm.message` 不是响应式的
解决办法:
var vm = new Vue({
data: {
// 声明 a、b 为一个空值字符串
message: '',
},
template: '<div>{{ message }}</div>'
})
vm.message = 'Hello!'
2. Vue 无法检测对象 property 的添加或移除
原因:官方 - 由于 JavaScript(ES5) 的限制,Vue.js 不能检测到对象属性的添加或删除。因为 Vue.js 在初始化实例时将属性转为 getter/setter,所以属性必须在 data 对象上才能让 Vue.js 转换它,才能让它是响应的。
场景:
var vm = new Vue({
data:{
obj: {
id: 001
}
},
// 页面不会变化
template: '<div>{{ obj.message }}</div>'
})
vm.obj.message = 'hello' // 不是响应式的
delete vm.obj.id // 不是响应式的
解决办法:
// 动态添加 - Vue.set
Vue.set(vm.obj, propertyName, newValue)
// 动态添加 - vm.$set
vm.$set(vm.obj, propertyName, newValue)
// 动态添加多个
// 代替 Object.assign(this.obj, { a: 1, b: 2 })
this.obj = Object.assign({}, this.obj, { a: 1, b: 2 })
// 动态移除 - Vue.delete
Vue.delete(vm.obj, propertyName)
// 动态移除 - vm.$delete
vm.$delete(vm.obj, propertyName)
3. Vue 不能检测通过数组索引直接修改一个数组项
原因:官方 - 由于 JavaScript 的限制,Vue 不能检测数组和对象的变化;尤雨溪 - 性能代价和获得用户体验不成正比。
场景:
var vm = new Vue({
data: {
items: ['a', 'b', 'c']
}
})
vm.items[1] = 'x' // 不是响应性的
解决办法:
// Vue.set
Vue.set(vm.items, indexOfItem, newValue)
// vm.$set
vm.$set(vm.items, indexOfItem, newValue)
// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)
拓展:Object.defineProperty() 可以监测数组的变化
Object.defineProperty() 可以监测数组的变化。但对数组新增一个属性(index)不会监测到数据变化,因为无法监测到新增数组的下标(index),删除一个属性(index)也是。
场景:
var arr = [1, 2, 3, 4]
arr.forEach(function(item, index) {
Object.defineProperty(arr, index, {
set: function(value) {
console.log('触发 setter')
item = value
},
get: function() {
console.log('触发 getter')
return item
}
})
})
arr[1] = '123' // 触发 setter
arr[1] // 触发 getter 返回值为 "123"
arr[5] = 5 // 不会触发 setter 和 getter
4. Vue 不能监测直接修改数组长度的变化
原因:官方 - 由于 JavaScript 的限制,Vue 不能检测数组和对象的变化;尤雨溪 - 性能代价和获得用户体验不成正比。
场景:
var vm = new Vue({
data: {
items: ['a', 'b', 'c']
}
})
vm.items.length = 2 // 不是响应性的
解决办法:
vm.items.splice(newLength)
5. 在异步更新执行之前操作 DOM 数据不会变化
原因:Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。
场景:
<div id="example">{{message}}</div>
var vm = new Vue({
el: '#example',
data: {
message: '123'
}
})
vm.message = 'new message' // 更改数据
vm.$el.textContent === 'new message' // false
vm.$el.style.color = 'red' // 页面没有变化
解决办法:
var vm = new Vue({
el: '#example',
data: {
message: '123'
}
})
vm.message = 'new message' // 更改数据
//使用 Vue.nextTick(callback) callback 将在 DOM 更新完成后被调用
Vue.nextTick(function () {
vm.$el.textContent === 'new message' // true
vm.$el.style.color = 'red' // 文字颜色变成红色
})
拓展:异步更新带来的数据响应的误解
<!-- 页面显示:我更新啦! -->
<div id="example">{{message.text}}</div>
var vm = new Vue({
el: '#example',
data: {
message: {},
}
})
vm.$nextTick(function () {
this.message = {}
this.message.text = '我更新啦!'
})
上段代码中,我们在 data 对象中声明了一个 message 空对象,然后在下次 DOM 更新循环结束之后触发的异步回调中,执行了如下两段代码:
this.message = {};
this.message.text = '我更新啦!'
到这里,模版更新了,页面最后会显示 我更新啦!。
模板更新了,应该具有响应式特性,如果这么想那么你就已经走入了误区。
一开始我们在 data 对象中只是声明了一个 message 空对象,并不具有 text 属性,所以该 text 属性是不具有响应式特性的。
但模板切切实实已经更新了,这又是怎么回事呢?
那是因为 Vue.js 的 DOM 更新是异步的,即当 setter 操作发生后,指令并不会立马更新,指令的更新操作会有一个延迟,当指令更新真正执行的时候,此时 text 属性已经赋值,所以指令更新模板时得到的是新值。
模板中每个指令/数据绑定都有一个对应的 watcher 对象,在计算过程中它把属性记录为依赖。之后当依赖的 setter 被调用时,会触发 watcher 重新计算 ,也就会导致它的关联指令更新 DOM。
具体流程如下所示:
执行 this.message = {}; 时, setter 被调用。
Vue.js 追踪到 message 依赖的 setter 被调用后,会触发 watcher 重新计算。
this.message.text = '我更新啦!'; 对 text 属性进行赋值。
异步回调逻辑执行结束之后,就会导致它的关联指令更新 DOM,指令更新开始执行。
所以真正的触发模版更新的操作是 this.message = {};这一句引起的,因为触发了 setter,所以单看上述例子,具有响应式特性的数据只有 message 这一层,它的动态添加的属性是不具备的。
对应上述第二点 - Vue 无法检测对象 property 的添加或移除
6. 循环嵌套层级太深,视图不更新?
看到网上有些人说数据更新的层级太深,导致数据不更新或者更新缓慢从而导致试图不更新?
由于我没有遇到过这种情况,在我试图重现这种场景的情况下,发现并没有上述情况的发生,所以对于这一点不进行过多描述(如果有人在真实场景下遇到这种情况留个言吧)。
针对上述情况有人给出的解决方案是使用强制更新:
如果你发现你自己需要在 Vue 中做一次强制更新,99.9% 的情况,是你在某个地方做错了事。
vm.$forceUpdate()
7. 拓展:路由参数变化时,页面不更新(数据不更新)
拓展一个因为路由参数变化,而导致页面不更新的问题,页面不更新本质上就是数据没有更新。
原因:路由视图组件引用了相同组件时,当路由参会变化时,会导致该组件无法更新,也就是我们常说中的页面无法更新的问题。
场景:
<div id="app">
<ul>
<li><router-link to="/home/foo">To Foo</router-link></li>
<li><router-link to="/home/baz">To Baz</router-link></li>
<li><router-link to="/home/bar">To Bar</router-link></li>
</ul>
<router-view></router-view>
</div>
const Home = {
template: `<div>{{message}}</div>`,
data() {
return {
message: this.$route.params.name
}
}
}
const router = new VueRouter({
mode:'history',
routes: [
{path: '/home', component: Home },
{path: '/home/:name', component: Home }
]
})
new Vue({
el: '#app',
router
})
上段代码中,我们在路由构建选项 routes 中配置了一个动态路由 '/home/:name',它们共用一个路由组件 Home,这代表他们复用 RouterView 。
当进行路由切换时,页面只会渲染第一次路由匹配到的参数,之后再进行路由切换时,message 是没有变化的。
解决办法:
解决的办法有很多种,这里只列举我常用到几种方法。
通过 watch 监听 $route 的变化。
const Home = {
template: `<div>{{message}}</div>`,
data() {
return {
message: this.$route.params.name
}
},
watch: {
'$route': function() {
this.message = this.$route.params.name
}
}
}
...
new Vue({
el: '#app',
router
})
给 <router-view> 绑定 key 属性,这样 Vue 就会认为这是不同的 <router-view>。
弊端:如果从 /home 跳转到 /user 等其他路由下,我们是不用担心组件更新问题的,所以这个时候 key 属性是多余的。
<div id="app">
...
<router-view :key="key"></router-view>
</div>
前言
前面几篇我们就 Redux 展开了几篇文章,这次我们来实现 react-thunk,就不是叫实现 redux-thunk 了,直接上源码,因为源码就11行。如果对 Redux 中间件还不理解的,可以看我写的 Redux 文章。
实现一个迷你Redux(基础版)
实现一个Redux(完善版)
浅谈React的Context API
带你实现 react-redux
为什么要用 redux-thunk
在使用 Redux 过程,通过 dispatch 方法派发一个 action 对象。当我们使用 redux-thunk 后,可以 dispatch 一个 function。redux-thunk会自动调用这个 function,并且传递 dispatch, getState 方法作为参数。这样一来,我们就能在这个 function 里面处理异步逻辑,处理复杂逻辑,这是原来 Redux 做不到的,因为原来就只能 dispatch 一个简单对象。
用法
redux-thunk 作为 redux 的中间件,主要用来处理异步请求,比如:
export function fetchData() {
return (dispatch, getState) => {
// to do ...
axios.get('https://jsonplaceholder.typicode.com/todos/1').then(res => {
console.log(res)
})
}
}
redux-thunk 源码
redux-thunk 的源码比较简洁,实际就11行。前几篇我们说到 redux 的中间件形式,
本质上是对 store.dispatch 方法进行了增强改造,基本是类似这种形式:
const middleware = (store) => next => action => {}
在这里就不详细解释了,可以看 实现一个Redux(完善版)
先给个缩水版的实现:
const thunk = ({ getState, dispatch }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState)
}
return next(action)
}
export default thunk
原理:即当 action 为 function 的时候,就调用这个 function (传入 dispatch, getState)并返回;如果不是,就直接传给下一个中间件。
完整源码如下:
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
// 如果action是一个function,就返回action(dispatch, getState, extraArgument),否则返回next(action)。
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument)
}
// next为之前传入的store.dispatch,即改写前的dispatch
return next(action)
}
}
const thunk = createThunkMiddleware()
// 给thunk设置一个变量withExtraArgument,并且将createThunkMiddleware整个函数赋给它
thunk.withExtraArgument = createThunkMiddleware
export default thunk
我们发现其实还多了 extraArgument 传入,这个是自定义参数,如下用法:
const api = "https://jsonplaceholder.typicode.com/todos/1";
const whatever = 10;
const store = createStore(
reducer,
applyMiddleware(thunk.withExtraArgument({ api, whatever })),
);
// later
function fetchData() {
return (dispatch, getState, { api, whatever }) => {
// you can use api and something else here
};
}
总结
同 redux-thunk 非常流行的库 redux-saga 一样,都是在 redux 中做异步请求等副作用。Redux 相关的系列文章就暂时写到这部分为止,下次会写其他系列。
一、前言
前端的模块化规范包括 commonJS、AMD、CMD 和 ES6。其中 AMD 和 CMD 可以说是过渡期的产物,目前较为常见的是commonJS 和 ES6。在 TS 中这两种模块化方案的混用,往往会出现一些意想不到的问题。
二、import * as
考虑到兼容性,我们一般会将代码编译为 es5 标准,于是 tsconfig.json 会有以下配置:
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
}
}
代码编译后最终会以 commonJS 的形式输出。
使用 React 的时候,这种写法 import React from "react" 会收到一个莫名其妙的报错:
Module "react" has no default export
这时候你只能把代码改成这样:import * as React from "react"。
究其原因,React 是以 commonJS 的规范导出的,而 import React from "react" 这种写法会去找 React 模块中的 exports.default,而 React 并没有导出这个属性,于是就报了如上错误。而 import * as React 的写法会取 module.exports 中的值,这样使用起来就不会有任何问题。我们来看看 React 模块导出的代码到底是怎样的(精简过):
...
var React = {
Children: {
map: mapChildren,
forEach: forEachChildren,
count: countChildren,
toArray: toArray,
only: onlyChild
},
createRef: createRef,
Component: Component,
PureComponent: PureComponent,
...
}
module.exports = React;
可以看到,React 导出的是一个对象,自然也不会有 default 属性。
二、esModuleInterop
为了兼容这种这种情况,TS 提供了配置项 esModuleInterop 和 allowSyntheticDefaultImports,加上后就不会有报错了:
{
"compilerOptions": {
"module": "commonjs",
"target": "es5",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true
}
}
其中 allowSyntheticDefaultImports 这个字段的作用只是在静态类型检查时,把 import 没有 exports.default 的报错忽略掉。
而 esModuleInterop 会真正的在编译的过程中生成兼容代码,使模块能正确的导入。还是开始的代码:
import React from "react";
现在 TS 编译后是这样的:
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
var react_1 = __importDefault(require("react"));
编译器帮我们生成了一个新的对象,将模块赋值给它的 default 属性,运行时就不会报错了。
三、Tree Shaking
如果把 TS 按照 ES6 规范编译,就不需要加上 esModuleInterop,只需要 allowSyntheticDefaultImports,防止静态类型检查时报错。
{
"compilerOptions": {
"module": "es6",
"target": "es6",
"allowSyntheticDefaultImports": true
}
}
什么情况下我们会考虑导出成 ES6 规范呢?多数情况是为了使用 webpack 的 tree shaking 特性,因为它只对 ES6 的代码生效。
顺便再发散一下,讲讲 babel-plugin-component。
import { Button, Select } from 'element-ui'
上面的代码经过编译后,是下面这样的:
var a = require('element-ui');
var Button = a.Button;
var Select = a.Select;
var a = require('element-ui') 会引入整个组件库,即使只用了其中的 2 个组件。
babel-plugin-component 的作用是将代码做如下转换:
// 转换前
import { Button, Select } from 'element-ui'
// 转换后
import Button from 'element-ui/lib/button'
import Select from 'element-ui/lib/select'
最终编译出来是这个样子,只会加载用到的组件:
var Button = require('element-ui/lib/button');
var Select = require('element-ui/lib/select');
四、总结
本文讲解了 TypeScript 是如何导入不同模块标准打包的代码的。无论你导入的是 commonJS 还是 ES6 的代码,万无一失的方式是把 esModuleInterop 和 allowSyntheticDefaultImports 都配置上。
初始化
使用 https://github.com/XYShaoKang... 作为基础模板
gatsby new gatsby-project-config https://github.com/XYShaoKang/gatsby-hello-world
Prettier 配置
安装 VSCode 扩展
按 Ctrl + P (MAC 下: Cmd + P) 输入以下命令,按回车安装
ext install esbenp.prettier-vscode
安装依赖
yarn add -D prettier
Prettier 配置文件.prettierrc.js
// .prettierrc.js
module.exports = {
trailingComma: 'es5',
tabWidth: 2,
semi: false,
singleQuote: true,
endOfLine: 'lf',
printWidth: 50,
arrowParens: 'avoid',
}
ESLint 配置
安装 VSCode 扩展
按 Ctrl + P (MAC 下: Cmd + P) 输入以下命令,按回车安装
ext install dbaeumer.vscode-eslint
安装 ESLint 依赖
yarn add -D eslint babel-eslint eslint-config-google eslint-plugin-react eslint-plugin-filenames
ESLint 配置文件.eslintrc.js
使用官方仓库的配置,之后在根据需要修改
// https://github.com/gatsbyjs/gatsby/blob/master/.eslintrc.js
// .eslintrc.js
module.exports = {
parser: 'babel-eslint',
extends: [
'google',
'eslint:recommended',
'plugin:react/recommended',
],
plugins: ['react', 'filenames'],
parserOptions: {
ecmaVersion: 2016,
sourceType: 'module',
ecmaFeatures: {
jsx: true,
},
},
env: {
browser: true,
es6: true,
node: true,
jest: true,
},
globals: {
before: true,
after: true,
spyOn: true,
__PATH_PREFIX__: true,
__BASE_PATH__: true,
__ASSET_PREFIX__: true,
},
rules: {
'arrow-body-style': [
'error',
'as-needed',
{ requireReturnForObjectLiteral: true },
],
'no-unused-expressions': [
'error',
{
allowTaggedTemplates: true,
},
],
'consistent-return': ['error'],
'filenames/match-regex': [
'error',
'^[a-z-\\d\\.]+$',
true,
],
'no-console': 'off',
'no-inner-declarations': 'off',
quotes: ['error', 'backtick'],
'react/display-name': 'off',
'react/jsx-key': 'warn',
'react/no-unescaped-entities': 'off',
'react/prop-types': 'off',
'require-jsdoc': 'off',
'valid-jsdoc': 'off',
},
settings: {
react: {
version: '16.4.2',
},
},
}
解决 Prettier ESLint 规则冲突
推荐配置
安装依赖
yarn add -D eslint-config-prettier eslint-plugin-prettier
在.eslintrc.js中的extends添加'plugin:prettier/recommended'
module.exports = {
extends: ['plugin:prettier/recommended'],
}
VSCode 中 Prettier 和 ESLint 协作
方式一:使用 ESLint 扩展来格式化代码
配置.vscode/settings.json
// .vscode/settings.json
{
"eslint.format.enable": true,
"[javascript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"[javascriptreact]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
}
}
ESLint 扩展会默认忽略.开头的文件,比如.eslintrc.js
如果需要格式化.开头的文件,可以在.eslintignore中添加一个否定忽略来启用对应文件的格式化功能.
!.eslintrc.js
或者直接使用!.*,这样可以开启所有点文件的格式化功能
方式二:使用 Prettier 扩展来格式化代码
在版prettier-vscode@v5.0.0中已经删除了直接对linter的集成,所以版没法像之前那样,通过prettier-eslint来集成ESLint的修复了(一定要这样用的话,可以通过降级到prettier-vscode@4来使用了).如果要使用Prettier来格式化的话,就只能按照官方指南中的说的集成方法,让Prettier来处理格式,通过配置在保存时使用ESlint自动修复代码.只是这样必须要保存文件时,才能触发ESLint的修复了.
配置 VSCode 使用 Prettier 来格式化 js 和 jsx 文件
在项目中新建文件.vscode/settings.json
// .vscode/settings.json
{
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}
说实话这个体验很糟糕,之前直接一键格式化代码并且修复 ESLint 错误,可以对比格式化之前和格式化之后的代码,如果感觉不对可以直接撤销更改就好了.现在必须要通过保存,才能触发修复 ESlint 错误.而在开发过程中,通过监听文件改变来触发热加载或者重新编译是很常见的操作.这样之后每次想要去修复 ESLint 错误,还是只是想看看修复错误之后的样子,都必须要去触发热加载或重新编译,每次操作的成本就太高了.
我更推荐第一种方式使用 ESLint 扩展来对代码进行格式化.
调试 Gatsby 配置
调试构建过程
添加配置文件.vscode/launch.json
// .vscode/launch.json
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Gatsby develop",
"type": "node",
"request": "launch",
"protocol": "inspector",
"program": "${workspaceRoot}/node_modules/gatsby/dist/bin/gatsby",
"args": ["develop"],
"stopOnEntry": false,
"runtimeArgs": ["--nolazy"],
"sourceMaps": false,
"outputCapture": "std"
}
]
}
的gatsby@2.22.*版本中调试不能进到断点,解决办法是降级到2.21.*,yarn add gatsby@2.21.40,等待官方修复再使用版本的
调试客户端
需要安装 Debugger for Chrome 扩展
ext install msjsdiag.debugger-for-chrome
添加配置文件.vscode/launch.json
// .vscode/launch.json
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Gatsby Client Debug",
"url": "http://localhost:8000",
"webRoot": "${workspaceFolder}"
}
]
}
先启动 Gatsby,yarn develop,然后按 F5 开始调试.
收集了一些工作中常用的工具。
如果你有好用的工具或者有意思的工具网站,要留言哦!
React是Facebook开发的一款JS库,那么Facebook为什么要建造React呢,主要为了解决什么问题,通过这个又是如何解决的?
从这几个问题出发我就在网上搜查了一下,有这样的解释。
Facebook认为MVC无法满足他们的扩展需求,由于他们非常巨大的代码库和庞大的组织,使得MVC很快变得非常复复杂,每当需要添加一项新的功能或特性时,系统的复杂度就成级数增长,致使代码变得脆弱和不可预测,结果导致他们的MVC正在土崩瓦解。认为MVC不适合大规模应用,当系统中有很多的模型和相应的视图时,其复杂度就会迅速扩大,非常难以理解和调试,特别是模型和视图间可能存在的双向数据流动。
解决这个问题需要“以某种方式组织代码,使其更加可预测”,这通过他们(Facebook)提出的Flux和React已经完成。
Flux
是一个系统架构,用于推进应用中的数据单向流动。React
是一个JavaScript框架,用于构建“可预期的”和“声明式的”Web用户界面,它已经使Facebook更快地开发Web应用
对于Flux,目前还没怎么研究,不怎么懂,这里就先把Flux的图放上来,有兴趣或者了解的可以再分享下,这里主要说下React。
那么React是解决什么问题的,在官网可以找到这样一句话:
We built React to solve one problem: building large applications with data that changes over time.
构建那些数据会随时间改变的大型应用,做这些,React有两个主要的特点:
另外在React官网上,通过《Why did we build React?》为什么我们要建造React的文档中还可以了解到以下四点:
Virtual DOM 虚拟DOM
传统的web应用,操作DOM一般是直接更新操作的,但是我们知道DOM更新通常是比较昂贵的。而React为了尽可能减少对DOM的操作,提供了一种不同的而又强大的方式来更新DOM,代替直接的DOM操作。就是Virtual DOM
,一个轻量级的虚拟的DOM,就是React抽象出来的一个对象,描述dom应该什么样子的,应该如何呈现。通过这个Virtual DOM去更新真实的DOM,由这个Virtual DOM管理真实DOM的更新。
为什么通过这多一层的Virtual DOM操作就能更快呢? 这是因为React有个diff算法,更新Virtual DOM并不保证马上影响真实的DOM,React会等到事件循环结束,然后利用这个diff算法,通过当前新的dom表述与之前的作比较,计算出最小的步骤更新真实的DOM。
component 的使用在 React 里极为重要, 因为 components 的存在让计算 DOM diff 更。
State 和 Render
React是如何呈现真实的DOM,如何渲染组件,什么时候渲染,怎么同步更新的,这就需要简单了解下State和Render了。state属性包含定义组件所需要的一些数据,当数据发生变化时,将会调用Render重现渲染,这里只能通过提供的setState方法更新数据。
好了,说了这么多,下面看写代码吧,先看一个官网上提供的Hello World
的示例:
<!DOCTYPE html> <html> <head> <script src="http://fb.me/react-0.12.1.js"></script> <script src="http://fb.me/JSXTransformer-0.12.1.js"></script> </head> <body> <div id="example"></div> <script type="text/jsx"> React.render( <h1>Hello, world!</h1>,
document.getElementById('example')
); </script> </body> </html>
这个很简单,浏览器访问,可以看到Hello, world!
字样。JSXTransformer.js
是支持解析JSX语法的,JSX是可以在Javascript中写html代码的一种语法。如果不喜欢,React也提供原生Javascript的方法。
再来看下另外一个例子:
<html> <head> <title>Hello React</title> <script src="http://fb.me/react-0.12.1.js"></script> <script src="http://fb.me/JSXTransformer-0.12.1.js"></script> <script src="http://code.jquery.com/jquery-1.10.0.min.js"></script> <script src="http://cdnjs.cloudflare.com/ajax/libs/showdown/0.3.1/showdown.min.js"></script> <style> #content{ width: 800px; margin: 0 auto; padding: 5px 10px; background-color:#eee; } .commentBox h1{ background-color: #bbb; } .commentList{ border: 1px solid yellow; padding:10px; } .commentList .comment{ border: 1px solid #bbb; padding-left: 10px; margin-bottom:10px; } .commentList .commentAuthor{ font-size: 20px; } .commentForm{ margin-top: 20px; border: 1px solid red; padding:10px; } .commentForm textarea{ width:100%; height:50px; margin:10px 0 10px 2px; } </style> </head> <body> <div id="content"></div> <script type="text/jsx"> var staticData = [ {author: "张飞", text: "我在写一条评论~!"}, {author: "关羽", text: "2货,都知道你在写的是一条评论。。"}, {author: "刘备", text: "哎,咋跟这俩逗逼结拜了!"} ]; var converter = new Showdown.converter();//markdown /** 组件结构: <CommentBox> <CommentList> <Comment /> </CommentList> <CommentForm /> </CommentBox> */ //评论内容组件 var Comment = React.createClass({ render: function (){ var rawMarkup = converter.makeHtml(this.props.children.toString()); return ( <div className="comment"> <h2 className="commentAuthor"> {this.props.author}: </h2> <span dangerouslySetInnerHTML={{__html: rawMarkup}} /> </div> ); } }); //评论列表组件 var CommentList = React.createClass({ render: function (){ var commentNodes = this.props.data.map(function (comment){ return ( <Comment author={comment.author}> {comment.text} </Comment> ); }); return ( <div className="commentList"> {commentNodes} </div> ); } }); //评论表单组件 var CommentForm = React.createClass({ handleSubmit: function (e){ e.preventDefault(); var author = this.refs.author.getDOMNode().value.trim(); var text = this.refs.text.getDOMNode().value.trim(); if(!author || !text){ return; } this.props.onCommentSubmit({author: author, text: text}); this.refs.author.getDOMNode().value = ''; this.refs.text.getDOMNode().value = ''; return; }, render: function (){ return ( <form className="commentForm" onSubmit={this.handleSubmit}> <input type="text" placeholder="Your name" ref="author" /><br/> <textarea type="text" placeholder="Say something..." ref="text" ></textarea><br/> <input type="submit" value="Post" /> </form> ); } }); //评论块组件 var CommentBox = React.createClass({ loadCommentsFromServer: function (){ this.setState({data: staticData}); /* 方便起见,这里就不走服务端了,可以自己尝试 $.ajax({ url: this.props.url + "?_t=" + new Date().valueOf(), dataType: 'json', success: function (data){ this.setState({data: data}); }.bind(this), error: function (xhr, status, err){ console.error(this.props.url, status, err.toString()); }.bind(this) }); */ }, handleCommentSubmit: function (comment){ //TODO: submit to the server and refresh the list var comments = this.state.data; var newComments = comments.concat([comment]); //这里也不向后端提交了 staticData = newComments; this.setState({data: newComments}); }, //初始化 相当于构造函数 getInitialState: function (){ return {data: []}; }, //组件添加的时候运行 componentDidMount: function (){ this.loadCommentsFromServer(); this.interval = setInterval(this.loadCommentsFromServer, this.props.pollInterval); }, //组件删除的时候运行 componentWillUnmount: function() { clearInterval(this.interval); }, //调用setState或者父级组件重新渲染不同的props时才会重新调用 render: function (){ return ( <div className="commentBox"> <h1>Comments</h1> <CommentList data={this.state.data}/> <CommentForm onCommentSubmit={this.handleCommentSubmit} /> </div> ); } }); //当前目录需要有comments.json文件 //这里定义属性,如url、pollInterval,包含在props属性中 React.render( <CommentBox url="comments.json" pollInterval="2000" />, document.getElementById("content") ); </script> </body> </html>
乍一看挺多,主要看脚本部分就可以了。方便起见,这里都没有走后端。定义了一个全局的变量staticData
,可权当是走服务端,通过浏览器的控制台改变staticData
的值,查看下效果,提交一条评论,查看下staticData的值的变化。
国外应用的较多,facebook、Yahoo、Reddit等。在github可以看到一个列表Sites-Using-React,国内的话,查了查,貌似比较少,目前知道的有一个杭州大搜车。大多技术要在国内应用起来一般是较慢的,不过React确实感觉比较特殊,特别是UI的组件化和Virtual DOM的思想,我个人比较看好,有兴趣继续研究研究。
和其他一些js框架相比,React怎样,比如Backbone、Angular等。
闭包是一个让初级JavaScript
使用者既熟悉又陌生的一个概念。因为闭包在我们书写JavaScript
代码时,随处可见,但是我们又不知道哪里用了闭包。
关于闭包的定义,网上(书上)的解释总是千奇百怪,我们也只能“取其精华去其糟粕”去总结一下。
ECMAScript中,闭包指的是:
从实践角度:一下才算是闭包:
闭包跟词法作用域,作用域链,执行上下文这几个JavaScript
中重要的概念都有关系,因此要想真的理解闭包,至少要对那几个概念不陌生。
闭包的优点:
闭包的缺点:
我们来一步一步引出闭包。
自执行函数也叫立即调用函数(IIFE),是一个在定义时就执行的函数。
var a=1;
(function() { console.log(a)
})()
上述代码是一个最简单的自执行函数。
在ES6之前,是没有块级作用域的,只有全局作用域和函数作用域,因此自执行函数还能在ES6之前实现块级作用域。
// ES6 块级作用域 var a = 1; if(true) { let a=111; console.log(a); // 111 } console.log(a); // 1
这里 if{} 中用let声明了一个 a。这个 a 就具有块级作用域,在这个 {} 中访问 a ,永远访问的都是 let 声明的a,跟全局作用域中的a没有关系。如果我们把 let 换成 var ,就会污染全局变量 a 。
如果用自执行函数来实现:
var a = 1;
(function() { if(true) { var a=111; console.log(a); // 111 }
})() console.log(a); // 1
为什么要在这里要引入自执行函数的概念呢?因为通常我们会用自执行函数来创建闭包,实现一定的效果。
来看一个基本上面试提问题:
for(var i=0;i<5;i++) {
setTimeout(function() { console.log(i);
},1000)
}
在理想状态下我们期望输出的是 0 ,1 ,2 ,3 ,4。但是实际上输出的是5 ,5 ,5 ,5 ,5。为什么是这样呢?其实这里不仅仅涉及到作用域,作用域链还涉及到Event Loop、微任务、宏任务。但是在这里不讲这些。
下面我们先解释它为什么会输出 5个5,然后再用自执行函数来修改它,以达到我们预期的结果。
提示:for 循环中,每一次的都声明一个同名变量,下一个变量的值为上一次循环执行完同名变量的值。
首先用var声明变量 for 是不会产生块级作用域的,所以在 () 中声明的 i 为全局变量。相当于:
// 伪代码 var i; for(i=0;i<5;i++) {
setTimeout(function() { console.log(i);
},1000)
}
setTimeout中的第一个参数为一个全局的匿名函数。相当于:
// 伪代码 var i; var f = function() { console.log(i);
} for(i=0;i<5;i++) {
setTimeout(f,1000)
}
由于setTimeout是在1秒之后执行的,这个时候for循环已经执行完毕,此时的全局变量 i 已经变成了 5 。1秒后5个setTimeout中的匿名函数会同时执行,也就是5个 f 函数执行。这个时候 f 函数使用的变量 i 根据作用域链的查找规则找到了全局作用域中的 i 。因此会输出 5 个5。
那我们怎样来修改它呢?
for(var i=0;i<5;i++) {
(function (){ setTimeout(function() { console.log(i);
},1000)
})();
}
上述例子会输出我们期望的值吗?答案是否。为什么呢?我们虽然把 setTimeout 包裹在一个匿名函数中了,但是当setTimeout中匿名函数执行时,首先去匿名函数中查找 i 的值,找不到还是会找到全局作用域中,最终 i 的值仍然是全局变量中的 i ,仍然为 5个5.
那我们把外层的匿名函数中声明一个变量 j 让setTimeout中的匿名函数访问这个 j 不就找不到全局变量中的变量了吗。
for(var i=0;i<5;i++) {
(function (){ var j = i;
setTimeout(function() { console.log(j);
},1000)
})();
}
这个时候才达到了我们预期的结果:0 1 2 3 4。
我们来优化一下:
for(var i=0;i<5;i++) {
(function (i){ setTimeout(function() { console.log(i);
},1000)
})(i);
}
*思路2:用 let 声明变量,产生块级作用域。
for(let i=0;i<5;i++) {
setTimeout(function() { console.log(i);
},1000)
}
这时for循环5次,产生 5 个块级作用域,也会声明 5 个具有块级作用域的变量 i ,因此setTimeout中的匿名函数每次执行时,访问的 i 都是当前块级作用域中的变量 i 。
什么是理论中的闭包?就是看似像闭包,其实并不是闭包。它只是类似于闭包。
function foo() { var a=2; function bar() { console.log(a); // 2 }
bar();
}
foo();
上述代码根据最上面我们对闭包的定义,它并不完全是闭包,虽然是一个函数可以访问另一个函数中的变量,但是被嵌套的函数是在当前词法作用域中被调用的。
我们怎样把上述代码foo 函数中的bar函数,在它所在的词法作用域外执行呢?
下面的代码就清晰的展示了闭包:
function foo() { var a=2; function bar() { console.log(a);
} return bar;
} var baz=foo();
baz(); // 2 —— 朋友,这就是闭包的效果。
上述代码中 bar 被当做 foo函数返回值。foo函数执行后把返回值也就是 bar函数 赋值给了全局变量 baz。当 baz 执行时,实际上也就是 bar 函数的执行。我们知道 foo 函数在执行后,foo 的内部作用域会被销毁,因为引擎有垃圾回收期来释放不再使用的内存空间。所以在bar函数执行时,实际上foo函数内部的作用域已经不存在了,理应来说 bar函数 内部再访问 a 变量时是找不到的。但是闭包的神奇之处就在这里。由于 bar 是在 foo 作用域中被声明的,所以 bar函数 会一直保存着对 foo 作用域的引用。这时就形成了闭包。
我们先看个例子:
var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope;
} return f;
} var foo = checkscope();
foo();
我们用伪代码来解释JavaScript
引擎在执行上述代码时的步骤:
JavaScript
引擎遇到可执行代码时,就会进入一个执行上下文(环境)
但是我们想一个问题,checkscope函数执行完毕,它的执行上下文从栈中弹出,也就是销毁了不存在了,f 函数还能访问包裹函数的作用域中的变量(scope)吗?答案是可以。
理由是在第6步,我们说过当checkscope 执行函数执行完毕时,它的执行上下文会从栈中弹出,此时活动对象也会被回收,按理说当 f 在访问checkscope的活动对象时是访问不到的。
其实这里还有个概念,叫做作用域链:当 checkscope 函数被创建时,会创建对应的作用域链,里面值存放着包裹它的作用域对应执行上下文的变量对象,在这里只是全局执行上下文的变量对象,当checkscope执行时,此时的作用域链变化了 ,里面存放的是变量对象(活动对象)的集合,最顶端是当前函数的执行上下文的活动对象。端是全局执行上下文的变量对象。类似于:
checkscope.scopeChain = [
checkscope.AO
global.VO
]
当checkscope执行碰到了 f 函数的创建,因此 f 函数也会创建对应的作用域链,默认以包裹它的函数执行时对应的作用域链为基础。因此此时 f 函数创建时的作用域链如下:
checkscope.scopeChain = [
checkscope.AO
global.VO
]
当 f 函数执行时,此时的作用域链变化如下:
checkscope.scopeChain = [
f.AO
checkscope.AO
global.VO
]
当checkscope函数执行完毕,内部作用域会被回收,但是 f函数 的作用域链还是存在的,里面存放着 checkscope函数的活动对象,因此在f函数执行时会从作用域链中查找内部使用的 scope 标识符,从而在作用域链的第二位找到了,也就是在 checkscope.AO 找到了变量scope的值。
正是因为JavaScript
做到了这一点,因此才会有闭包的概念。还有人说闭包并不是为了拥有它采取设计它的,而是设计作用域链时的副作用产物。
闭包是JavaScript
中最难的点,也是平常面试中常问的问题,我们必须要真正的去理解它,如果只靠死记硬背是经不起考验的。
this
this是我们在书写代码时最常用的关键词之一,即使如此,它也是JavaScript最容易被最头疼的关键词。那么this到底是什么呢?
如果你了解执行上下文,那么你就会知道,其实this是执行上下文对象的一个属性:
executionContext = {
scopeChain:[ ... ],
VO:{
...
},
this: ?
}
执行上下文中有三个重要的属性,作用域链(scopeChain)、变量对象(VO)和this。
this是在进入执行上下文时确定的,也就是在函数执行时才确定,并且在运行期间不允许修改并且是永久不变的
在全局代码中的this
在全局代码中this 是不变的,this始终是全局对象本身。
var a = 10;
this.b = 20;
window.c = 30;
console.log(this.a);
console.log(b);
console.log(this.c);
console.log(this === window) // true
// 由于this就是全局对象window,所以上述 a ,b ,c 都相当于在全局对象上添加相应的属性
如果我们在代码运行期尝试修改this的值,就会抛出错误:
this = { a : 1 } ; // Uncaught SyntaxError: Invalid left-hand side in assignment
console.log(this === window) // true
函数代码中的this
在函数代码中使用this,才是令我们最容易困惑的,这里我们主要是对函数代码中的this进行分析。
我们在上面说过this的值是,进入当前执行上下文时确定的,也就是在函数执行时并且是执行前确定的。但是同一个函数,作用域中的this指向可能完全不同,但是不管怎样,函数在运行时的this的指向是不变的,而且不能被赋值。
function foo() {
console.log(this);
}
foo(); // window
var obj={
a: 1,
bar: foo,
}
obj.bar(); // obj
函数中this的指向丰富的多,它可以是全局对象、当前对象、或者是任意对象,当然这取决于函数的调用方式。在JavaScript中函数的调用方式有一下几种方式:作为函数调用、作为对象属性调用、作为构造函数调用、使用apply或call调用。下面我们将按照这几种调用方式一一讨论this的含义。
作为函数调用
什么是作为函数调用:就是独立的函数调用,不加任何修饰符。
function foo(){
console.log(this === window); // true
this.a = 1;
console.log(b); // 2
}
var b = 2;
foo();
console.log(a); // 1
上述代码中this绑定到了全局对象window。this.a相当于在全局对象上添加一个属性 a 。
在严格模式下,独立函数调用,this的绑定不再是window,而是undefined。
function foo() {
"use strict";
console.log(this===window); // false
console.log(this===undefined); // true
}
foo();
这里要注意,如果函数调用在严格模式下,而内部代码执行在非严格模式下,this 还是会默认绑定为 window。
function foo() {
console.log(this===window); // true
}
(function() {
"use strict";
foo();
})()
对于在函数内部的函数独立调用 this 又指向了谁呢?
function foo() {
function bar() {
this.a=1;
console.log(this===window); // true
}
bar()
}
foo();
console.log(a); // 1
上述代码中,在函数内部的函数独立调用,此时this还是被绑定到了window。
总结:当函数作为独立函数被调用时,内部this被默认绑定为(指向)全局对象window,但是在严格模式下会有区别,在严格模式下this被绑定为undefined。
作为对象属性调用
var a=1;
var obj={
a: 2,
foo: function() {
console.log(this===obj); // true
console.log(this.a); // 2
}
}
obj.foo();
上述代码中 foo属性的值为一个函数。这里称 foo 为 对象obj 的方法。foo的调用方式为 对象 . 方法 调用。此时 this 被绑定到当前调用方法的对象。在这里为 obj 对象。
再看一个例子:
var a=1;
var obj={
a: 2,
bar: {
a: 3,
foo: function() {
console.log(this===bar); // true
console.log(this.a); // 3
}
}
}
obj.bar.foo();
遵循上面说的规则 对象 . 属性 。这里的对象为 obj.bar 。此时 foo 内部this被绑定到了 obj.bar 。 因此 this.a 即为 obj.bar.a 。
再来看一个例子:
var a=1;
var obj={
a: 2,
foo: function() {
console.log(this===obj); // false
console.log(this===window); // true
console.log(this.a); // 1
}
}
var baz=obj.foo;
baz();
这里 foo 函数虽然作为对象obj 的方法。但是它被赋值给变量 baz 。当baz调用时,相当于 foo 函数独立调用,因此内部 this被绑定到 window。
使用apply或call调用
apply和call为函数原型上的方法。它可以更改函数内部this的指向。
var a=1;
function foo() {
console.log(this.a);
}
var obj1={
a: 2
}
var obj2={
a: 3
}
var obj3={
a: 4
}
var bar=foo.bind(obj1);
bar();// 2 this => obj1
foo(); // 1 this => window
foo.call(obj2); // 3 this => obj2
foo.call(obj3); // 4 this => obj3
当函数foo 作为独立函数调用时,this被绑定到了全局对象window,当使用bind、call或者apply方法调用时,this 被分别绑定到了不同的对象。
作为构造函数调用
var a=1;
function Person() {
this.a=2; // this => p;
}
var p=new Person();
console.log(p.a); // 2
上述代码中,构造函数 Person 内部的 this 被绑定为 Person的一个实例。
总结:
当我们要判断当前函数内部的this绑定,可以依照下面的原则:
函数是否在是通过 new 操作符调用?如果是,this 绑定为新创建的对象
var bar = new foo(); // this => bar;
函数是否通过call或者apply调用?如果是,this 绑定为指定的对象
foo.call(obj1); // this => obj1;
foo.apply(obj2); // this => obj2;
函数是否通过 对象 . 方法调用?如果是,this 绑定为当前对象
obj.foo(); // this => obj;
函数是否独立调用?如果是,this 绑定为全局对象。
foo(); // this => window
DOM事件处理函数中的this
1). 事件绑定
<button id="btn">点击我</button>
// 事件绑定
function handleClick(e) {
console.log(this); // <button id="btn">点击我</button>
}
document.getElementById('btn').addEventListener('click',handleClick,false); // <button id="btn">点击我</button>
document.getElementById('btn').onclick= handleClick; // <button id="btn">点击我</button>
根据上述代码我们可以得出:当通过事件绑定来给DOM元素添加事件,事件将被绑定为当前DOM对象。
2).内联事件
<button onclick="handleClick()" id="btn1">点击我</button>
<button onclick="console.log(this)" id="btn2">点击我</button>
function handleClick(e) {
console.log(this); // window
}
//第二个 button 打印的是 <button id="btn">点击我</button>
我认为内联事件可以这样理解:
//伪代码
<button onclick=function(){ handleClick() } id="btn1">点击我</button>
<button onclick=function() { console.log(this) } id="btn2">点击我</button>
这样我们就能理解上述代码中为什么内联事件一个指向window,一个指向当前DOM元素。(当然浏览器处理内联事件时并不是这样的)
定时器中的this
定时器中的 this 指向哪里呢?
function foo() {
setTimeout(function() {
console.log(this); // window
},1000)
}
foo();
再来看一个例子
var name="chen";
var obj={
name: "erdong",
foo: function() {
console.log(this.name); // erdong
setTimeout(function() {
console.log(this.name); // chen
},1000)
}
}
obj.foo();
到这里我们可以看到,函数 foo 内部this指向为调用它的对象,即:obj 。定时器中的this指向为 window。那么有什么办法让定时器中的this跟包裹它的函数绑定为同一个对象呢?
1). 利用闭包:
var name="chen";
var obj={
name: "erdong",
foo: function() {
console.log(this.name) // erdong
var that=this;
setTimeout(function() {
// that => obj
console.log(that.name); // erdong
},1000)
}
}
obj.foo();
利用闭包的特性,函数内部的函数可以访问含义访问当前词法作用域中的变量,此时定时器中的 that 即为包裹它的函数中的 this 绑定的对象。在下面我们会介绍利用 ES6的箭头函数实现这一功能。
当然这里也可以适用bind来实现:
var name="chen";
var obj={
name: "erdong",
foo: function() {
console.log(this.name); // erdong
setTimeout(function() {
// this => obj
console.log(this.name); // erdong
}.bind(this),1000)
}
}
obj.foo();
被忽略的this
如果你把 null 或者 undefined 作为 this 的绑定对象传入 call 、apply或者bind,这些值在调用时会被忽略,实例 this 被绑定为对应上述规则。
var a=1;
function foo() {
console.log(this.a); // 1 this => window
}
var obj={
a: 2
}
foo.call(null);
var a=1;
function foo() {
console.log(this.a); // 1 this => window
}
var obj={
a: 2
}
foo.apply(null);
var a=1;
function foo() {
console.log(this.a); // 1 this => window
}
var obj={
a: 2
}
var bar = foo.bind(null);
bar();
bind 也可以实现函数柯里化:
function foo(a,b) {
console.log(a,b); // 2 3
}
var bar=foo.bind(null,2);
bar(3);
更复杂的例子:
var foo={
bar: function() {
console.log(this);
}
};
foo.bar(); // foo
(foo.bar)(); // foo
(foo.bar=foo.bar)(); // window
(false||foo.bar)(); // window
(foo.bar,foo.bar)(); // window
上述代码中:
foo.bar()为对象的方法调用,因此 this 绑定为 foo 对象。
(foo.bar)() 前一个() 中的内容不计算,因此还是 foo.bar()
(foo.bar=foo.bar)() 前一个 () 中的内容计算后为 function() { console.log(this); } 所以这里为匿名函数自执行,因此 this 绑定为 全局对象 window
后面两个实例同上。
这样理解会比较好:
(foo.bar=foo.bar) 括号中的表达式执行为 先计算,再赋值,再返回值。
(false||foo.bar)() 括号中的表达式执行为 判断前者是否为 true ,若为true,不计算后者,若为false,计算后者并返回后者的值。
(foo.bar,foo.bar) 括号中的表达式之行为分别计算 “,” 操作符两边,然后返回 “,” 操作符后面的值。
箭头函数中的this
箭头函数时ES6新增的语法。
有两个作用:
更简洁的函数
本身不绑定this
代码格式为:
// 普通函数
function foo(a){
// ......
}
//箭头函数
var foo = a => {
// ......
}
//如果没有参数或者参数为多个
var foo = (a,b,c,d) => {
// ......
}
我们在使用普通函数之前对于函数的this绑定,需要根据这个函数如何被调用来确定其内部this的绑定对象。而且常常因为调用链的数量或者是找不到其真正的调用者对 this 的指向模糊不清。在箭头函数出现后其内部的 this 指向不需要再依靠调用的方式来确定。
箭头函数有几个特点(与普通函数的区别)
箭头函数不绑定 this 。它只会从作用域链的上一层继承 this。
箭头函数不绑定arguments,使用reset参数来获取实参的数量。
箭头函数是匿名函数,不能作为构造函数。
箭头函数没有prototype属性。
不能使用 yield 关键字,因此箭头函数不能作为函数生成器。
这里我们只讨论箭头函数中的this绑定。
用一个例子来对比普通函数与箭头函数中的this绑定:
var obj={
foo: function() {
console.log(this); // obj
},
bar: () => {
console.log(this); // window
}
}
obj.foo();
obj.bar();
上述代码中,同样是通过对象 . 方法调用一个函数,但是函数内部this绑定确是不同,只因一个数普通函数一个是箭头函数。
用一句话来总结箭头函数中的this绑定:
个人上面说的它会从作用域链的上一层继承 this ,说法并不是很正确。作用域中存放的是这个函数当前执行上下文与所有父级执行上下文的变量对象的集合。因此在作用域链中并不存在 this 。应该说是作用域链上一层对应的执行上下文中继承 this 。
箭头函数中的this继承于作用域链上一层对应的执行上下文中的this
var obj={
foo: function() {
console.log(this); // obj
},
bar: () => {
console.log(this); // window
}
}
obj.bar();
上述代码中obj.bar执行时的作用域链为:
scopeChain = [
obj.bar.AO,
global.VO
]
根据上面的规则,此时bar函数中的this指向为全局执行上下文中的this,即:window。
再来看一个例子:
var obj={
foo: function() {
console.log(this); // obj
var bar=() => {
console.log(this); // obj
}
bar();
}
}
obj.foo();
在普通函数中,bar 执行时内部this被绑定为全局对象,因为它是作为独立函数调用。但是在箭头函数中呢,它却绑定为 obj 。跟父级函数中的 this 绑定为同一对象。
此时它的作用域链为:
scopeChain = [
bar.AO,
obj.foo.AO,
global.VO
]
这个时候我们就差不多知道了箭头函数中的this绑定。
继续看例子:
var obj={
foo: () => {
console.log(this); // window
var bar=() => {
console.log(this); // window
}
bar();
}
}
obj.foo();
这个时候怎么又指向了window了呢?
我们还看当 bar 执行时的作用域链:
scopeChain = [
bar.AO,
obj.foo.AO,
global.VO
]
当我们找bar函数中的this绑定时,就会去找foo函数中的this绑定。因为它是继承于它的。这时 foo 函数也是箭头函数,此时foo中的this绑定为window而不是调用它的obj对象。因此 bar函数中的this绑定也为全局对象window。
我们在回头看上面关于定时器中的this的例子:
var name="chen";
var obj={
name: "erdong",
foo: function() {
console.log(this.name); // erdong
setTimeout(function() {
console.log(this); // chen
},1000)
}
}
obj.foo();
这时我们就可以很简单的让定时器中的this与foo中的this绑定为同一对象:
var name="chen";
var obj={
name: "erdong",
foo: function() {
// this => obj
console.log(this.name); // erdong
setTimeout(() => {
// this => foo中的this => obj
console.log(this.name); // erdong
},1000)
}
}
obj.foo();
蓝蓝设计的小编 http://www.lanlanwork.com