1.动态绑定class类名的方法
答:js document.getElementByClassName("xx")
jqeury $(".xxx")
vue v-bind:class
2.计算属性和watch的区别?
答:computed:通过属性计算而得来的属性
1、computed内部的函数在调用时不加()。
2、computed是依赖vm中data的属性变化而变化的,当data中的属性发生改变的时候,当前函数才会执行,
data中的属性没有改变的时候,当前函数不会执行。
3、computed中的函数必须用return返回。
4、在computed中不要对data中的属性进行赋值操作。如果对data中的属性进行赋值操作了,就是data中的属性发生改变,
从而触发computed中的函数,形成死循环了。
5、当computed中的函数所依赖的属性没有发生改变,那么调用当前函数的时候会从缓存中读取。
watch:属性监听
1、watch中的函数名称必须要和data中的属性名一致,因为watch是依赖data中的属性,当data中的属性发生改变的时候,
watch中的函数就会执行。
2、watch中的函数有两个参数,前者是newVal,后者是oldVal。
3、watch中的函数是不需要调用的。
4、watch只会监听数据的值是否发生改变,而不会去监听数据的地址是否发生改变。也就是说,watch想要监听引用类型数据的变化,
需要进行深度监听。
“obj.name”(){}------如果obj的属性太多,这种方法的效率很低,
obj:{handler(newVal){},deep:true}------用handler+deep的方式进行深度监听。
5、特殊情况下,watch无法监听到数组的变化,特殊情况就是说更改数组中的数据时,数组已经更改,但是视图没有更新。
更改数组必须要用splice()
或者set.this.arr.splice(0,1,100 )−−−−−修 改 a r r 中 第 0 项 开 始 的 1 个 数 据 为 100 ,
this.set.this.arr.splice(0,1,100)-----修改arr中第0项开始的1个数据为100,
this.set.this.arr.splice(0,1,100)−−−−−修改arr中第0项开始的1个数据为100,
this.set(this.arr,0,100)-----修改arr第0项值为100。
6、immediate:true 页面首次加载的时候做一次监听。
3.怎么理解单项数据流?
答:单向数据流就是从一个组件单方向将数据流向它的内部组件,也就是父组件的数据流向子组件中,但子组件不能将这个数据修改掉,
如要返回到父组件中修改然后重新流向子组件,从而达到更新数据的原理
4.自定义组件的语法糖 v-model是怎么实现的?
答:
主要是通过input事件来触发input标签value值来实现我们说的“双向数据绑定”,其实它还是单向数据流。上面的实际相当于
<input type="text" :value="value" @input=v=>$emit('input', v)/>
在自定义组件中使用v-model的几种方法
我们在封装输入框input、下拉选择select、单选多选radio等多会使用到自定义v-model功能。下面介绍几种常用方法的使用:
1. prop + $emit
搞过vue开发的同志们都知道我们经常用prop 和 $emit进行组件间通信,这方面不在本文具体阐述,详细请自行到cn.vuejs.org了解
<template>
<input type="text" :value="value" @input="handleInput" :placeholder="placehodler" />
</template>
<script>
export default {
name: 'kInput',
props: {
value: ['String', 'Number'],
placeholder: String
},
methods: {
handleInput ($event) {
// 通过input标签的原生事件input将值emit出去,以达到值得改变实现双向绑定
this.$emit('input', $event.target.value)
}
}
}
</script>
<style scoped type="less">
</style>
2. prop + $emit + model选项
<template>
<input type="text" :value="value" @input="handleInput" />
</template>
<script>
export default {
name: 'kInput',
model: {
prop: 'value',
event: 'input'
},
props: {
value: ['String', 'Number'],
placeholder: String
},
methods: {
handleInput ($event) {
// 通过input标签的原生事件input将值emit出去,以达到值得改变实现双向绑定
this.$emit('input', $event.target.value)
}
}
}
</script>
<style scoped type="less">
</style>
3. prop + $emit + computed
<template>
<input type="text" :value="value2" />
/*<app-table :data="value2"></app-table>*/
</template>
<script>
import AppTable from '@/component/common/AppTable'
export default {
name: 'kInput',
props: {
value: ['String', 'Number'],
placeholder: String
},
computed: {
value2: {
get() {
const v = this.value
return v
},
set(val) {
const v = JSON.parse(JSON.stringify(this.value))//利用深拷贝原理使得修改prop值不会报错,因为prop是单向数据流,2.0版本上不允许在组件内部直接修改
v = val
this.$emit('input', v) //这里多用于子组件间没有input元素中,通过在computed属性中监听值变化事emit input事件
}
}
},
components: { AppTable }
}
</script>
<style scoped type="less">
</style>
三种方式则在父组件中使用
<template>
<div class="main">
<k-input v-model="search" placeholder="请输入搜索关键词"></k-input>
</div>
</template>
<script>
import kInput from '@/components/common/kInput' //引入这个自定义组件(根据自己项目具体位置引入)
export default {
data () {
return {
search: ''
}
},
components: {
kInput // 局部注册组件
}
}
</script>
5.vue-router 有哪几种钩子
答:第一种全局导航钩子
const router = new VueRouter({ ... });
router.beforeEach((to, from, next) => {
// do someting
});
这三个参数 to 、from 、next 分别的作用:
1.to: Route,代表要进入的目标,它是一个路由对象
2.from: Route,代表当前正要离开的路由,同样也是一个路由对象
3.next: Function,这是一个必须需要调用的方法,而具体的执行效果则依赖 next 方法调用的参数
1.next():进入管道中的下一个钩子,如果全部的钩子执行完了,则导航的状态就是 confirmed(确认的)
2.next(false):这代表中断掉当前的导航,即 to 代表的路由对象不会进入,被中断,此时该表 URL 地址会
被重置到 from 路由对应的地址
3.next(‘/’) 和 next({path: ‘/’}):在中断掉当前导航的同时,跳转到一个不同的地址
4.next(error):如果传入参数是一个 Error 实例,那么导航被终止的同时会将错误传递给 router.onError() 注册过的回调
第二种组件内导航钩子
组件内的导航钩子主要有这三种:beforeRouteEnter、beforeRouteUpdate、beforeRouteLeave。他们是直接在路由组件内部直接进行定义的
const File = {
template: `<div>This is file</div>`,
beforeRouteEnter(to, from, next) {
// do someting
// 在渲染该组件的对应路由被 confirm 前调用
},
beforeRouteUpdate(to, from, next) {
// do someting
// 在当前路由改变,但是依然渲染该组件是调用
},
beforeRouteLeave(to, from ,next) {
// do someting
// 导航离开该组件的对应路由时被调用
}
}
第三种单独路由独享组件
即单个路由独享的导航钩子,它是在路由配置上直接进行定义的:
const router = new VueRouter({
routes: [
{
path: '/file',
component: File,
beforeEnter: (to, from ,next) => {
// do someting
}
}
]
});
6.vue.js双向绑定原理
答:原理主要通过数据劫持和发布订阅模式实现的
通过Object.defineProperty()来劫持各个属性的setter,getter,监听数据的变化
在数据变动时发布消息给订阅者(watcher),订阅者触发响应的回调(update)更新视图。
一、什么是数据劫持
访问或者修改对象的某个属性时,都会触发相对应的函数,在这个函数里进行额外的操作或者修改返回结果。
在触发函数的时候,在函数中所做的操作,就是劫持操作。
Object.defineProperty
语法:
Object.defineProperty(obj,prop,descriptor)
参数:
obj:目标对象
prop:需要定义的属性或方法的名称
descriptor:目标属性所拥有的特性
value:属性的值
writable:如果为false,属性的值就不能被重写。
get:一旦目标属性被访问就会调回此方法,并将此方法的运算结果返回用户。
set:一旦目标属性被赋值,就会调回此方法。
configurable:如果为false,则任何尝试删除目标属性或修改属性性以下特性(writable, configurable, enumerable)的行为将被无效化。
enumerable:是否能在for...in循环中遍历出来或在Object.keys中列举出来。
7.怎么理解Vuex
答:1.vuex是什么?
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
2.vuex的五个核心概念
vue有五个核心概念,state、getters、mutations、actions、modules (plugins)。
Vuex是专门为Vue服务,用于管理页面的数据状态、提供统一数据操作的生态系统,相当于数据库mongoDB,MySQL等,任何组件都可以存取仓库中的数据。其中vuex类似的 还是有Redux,Redux大多用于React
1.小应用不建议使用Vuex,因为小项目使用 Vuex 可能会比较繁琐冗余;
2.中大型单页应用,因为要考虑如何更好地在组件外部管理状态,Vuex 将会成为自然而然的选择;
2.1 state: 存放基本数据 ----辅助函数mapState: 当一个组件需要获取多个状态时候,将这些状态都声明为计算属性会有些重复和冗余。
为了解决这个问题,我们可以使用 mapState 辅助函数帮助我们生成计算属性,让你少按几次键。
2.2 getters: 是从store中的state派生出来的状态,专门来计算state中的数据,相当于state中数据的计算属性
—辅助函数mapGetters辅助函数: mapGetters 辅助函数仅仅是将 store 中的 getters 映射到局部计算属性,与state类似
2.3 mutations 提交mutions是更改Vuex中的状态的唯一方法。mutations必须是同步的,如果要异步需要使用actions。
每一个mutations都有一个字符串作为第一个参数,提交载荷作为第二个参数。 —辅助函数mapMutations 将组建中的methods映射为store.commit调用。
2.4 actions 专门操作异步请求的数据和业务逻辑的地方,它不能直接变更state中的状态,而是通过commit来调用mutations里的方法来改变state里的数据。
辅助函数mapActions 将组建的methods映射为store.dispath调用
2.5 module 使用单一状态树,导致应用的所有状态几种到一个很大的对象,但是,当应用变得很大时,store对象会变得臃肿不堪,为了解决以上问题,Vuex允许我们将store分割到模块(modules)。
每个模块拥有自己的state、mutations、avtions、grtters。
8.this.$nextTick()
答:this.$nextTick 将回调延迟到下次DOM更新循环之后执行。在修改数据之后立即使用它,然后等待DOM更新。
this.$nextTick 跟全局方法 vue.nextTick 一样,不同的是,回调的 this 自动绑定到调用它的实例上。
总的来说,假设我们更改了某个 dom 元素内部的文本,而这时候我们想直接打印这个更改之后的文本是需要 dom 更新之后才会实现的,
就像我们把将要打印输出的代码放在 setTimeout(fn, 0) 中
9.slot插槽的理解
答:slot是组件内的一个占位符,该占位符可以在后期使用自己的标记语言填充。
作用:让父组件可以向子组件指定位置插入html结构,也是一种组件间通信方式,适用于父组件===>子组件
例子:
//父组件中
<Category>
<div>html结构</div>
</Category>
//子组件中:
<template>
<div>
<slot>插槽的默认内容</slot>
</div>
</template>
10.vue组件通信
答:方法一、props/$emit
父组件A通过props的方式向子组件B传递,B to A 通过在 B 组件中 $emit, A 组件中 v-on 的方式实现。
方法二、$emit/$on
这种方法通过一个空的Vue实例作为中央事件总线(事件中心),用它来触发事件和监听事件,巧妙而轻量地实现了任何组件间的通信,
包括父子、兄弟、跨级。当我们的项目比较大时,可以选择更好的状态管理解决方案vuex。
1.具体实现方式:
var Event=new Vue();
Event.$emit(事件名,数据);
Event.$on(事件名,data => {});
方法三、vuex
方法四、$attrs/$listeners
方法五、provide/inject
方法六、$parent / $children与 ref
ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例
$parent / $children:访问父 / 子实例
总结
常见使用场景可以分为三类:
父子通信:
父向子传递数据是通过 props,子向父是通过 events($emit);通过父链 / 子链也可以通信($parent / $children);
ref 也可以访问组件实例;provide / inject API;$attrs/$listeners
兄弟通信:
Bus;Vuex
跨级通信:
Bus;Vuex;provide / inject API、$attrs/$listeners
11.什么是MVVM,与MVC有什么区别
答:一、什么是mvvm?
mvvm是model--view--viewmodel的简写,即模型-视图-视图模型,M(model)即数据模型,V(view)视图看到的页面,VM(view model)
视图模型相当于一个桥梁作用,连接着view和model。VM和数据进行绑定,在数据M发生变化时,将数据转化成视图。
VM也监听着V视图,当视图页面发生变化时,会更新M的数据。
二、什么是mvc?
MVC 是 Model-View- Controller 的简写。即模型-视图-控制器。M 和 V 指的意思和 MVVM 中的 M 和 V 意思一样。
C 即 Controller 指的是页面业务逻辑,使用 MVC 的目的就是将 M 和 V 的代码分离。MVC 是单向通信。也就是 View 跟 Model,
必须通过 Controller 来承上启下。
三、使用场景
场景:数据操作比较多的场景,需要大量操作 DOM 元 素时,采用 MVVM 的开发方式,会更加便捷,让开发者更多的精力放在数据的变化上,
解放繁 琐的操作 DOM 元素。
四、mvc和mvvm的区别
区别:把MVC 中 Controller 演变成 MVVM 中的 viewModel,MVVM 主要解决了 MVC 中大量的 DOM 操作使页面渲染性能降低,加载速度变慢,
影响用户体验,vue 数据驱动,通 过数据来显示视图层而不是节点操作。MVC 和 MVVM 其实区别并不大,都是一种设计思想,
MVC 和 MVVM 的区别并不是 VM 完全取代了 C,只是在 MVC 的基础上增加了一层 VM,只不过是弱化了 C 的概念,
ViewModel 存在目的在于抽离 Controller 中展示的业务逻辑,而不是替代 Controller,其它视图 操作业务等还是应该放在 Controller 中实现,
也就是说 MVVM 实现的是业务逻辑组件的重用, 使开发更高效,结构更清晰,增加代码的复用性
12.路由跳转的方式?
答:路由跳转
方式一:path路径跳转。
传值可以使用params 传值和query传值
(缺点:不能传引用数据类型-数组,对象等)
//写法1
<router-link to="/artlist">小说列表</router-link>
//router-link解析出来其实是a标签
//写法2
<router-link :to="path1">小说列表</router-link>
data() {
return{
path1:'/artlist'
}
}
//写法3
<router-link :to="'/artlist'">小说列表</router-link>
data() {
return{
path1:'/artlist'
}
}
方式二:命名式路由跳转(name)。
传值可以使用params和query传值
(优点:可以传基本数据类型和数组,对象)
<router-link :to="{name:'shop',query:{city:cityObj}}">购物车</router-link>
...
//路由配置
{
path:'/shop',
//该path路径不能少。因为命名式路由跳转是通过name找到该path
name:'shop',
component:Shop
}
方式三:编程式路由跳转(最常用的,不受时机、条件的限制)。
传值可以用params 传值和query传值
(优点:可以传基本数据类型和数组,对象)
jumpHome() {
this.$router.push({
path:"/home",
query:{
id:this.id
}
})
}
...
//接收值如果进入另一个页面,一般在created中接收
this.$route.query.id
//路由配置
{ path: "/home", component: ()=>import("../Home") }
//或者name和params搭配,接收值 this.$route.params.id
路由传值
query查询参数传值
1.1 router-link的to属性或者js方式push方法里的参数由字符串更换成对象, 需要切换的路由由path字段负责, 传递的值由query字段负责
1.2 query方式传递的值会以键值对的形式拼接到网址的后面, 与get请求传递数据的格式类似
1.3 query方式传递的值, 刷新页面, 值不会消失
1.4 query方式传值, 不需要去配置routes数组里的对应对象
params路径参数传值
2.1 router-link的to属性或者js方式push方法里的参数由字符串更换成对象, 需要切换的路由由name字段负责, 传递的值由params字段负责
2.2 params方式传递的值会以路径的形式拼接到网址的后面
2.3 params方式传递的值, 刷新页面, 值会丢失
2.4 params方式传值, 需要去配置routes数组里的对应对象, 需要给对象多加一个name字段, 还需要将path字段修改成 “/路由/:值1/:值2/:…”
//传值
methods:{
goMyOrder(){
this.$router.push({name:"order", params:{orderId:"667788"}}).catch(err=>{});
}
}
//接收值
computed:{
getOrderId(){
return this.$route.params.orderId
}
}
//路由配置
{ path:"order/:orderId",
name:"order",
component: ()=>import("../views/Mine/MyOrder")
}
当进行路由重定向时, 也可以进行路由传值的, 如果需要传值, 将redirect的由字符串改成对象,例如:
{ path:" ", redirect: {path:"info", query:{userId:'112233'}} },
{ path:"info", component: ()=>import("../views/Mine/MyInfo") }
13.css选择器有哪些,并写明选择器的权重优先级
答:1、元素选择器
p,h2,span,a,div乃至html标签
2、类选择器
对文件元素添加一个class属性,点号”.”加上类名就组成了一个类选择器。
3、ID选择器
一个元素只能拥有一个唯一的ID属性。其次一个ID值在一个HTML文档中只能出现一次,
也就是一个ID只能唯一标识一个元素(不是一类元素,而是一个元素)。
4、属性选择器
[type="text"]
5、派生选择器
(1)后代选择器
(2)子元素选择器
一个子选择器由两个或多个由">"分隔的选择器组成。
(3)相邻兄弟选择器
6.全局选择器 *
7.伪元素 : first-line ,: first-letter ,: before,: after
8.伪类 :hover,: focus
权重优先级
1. 第一等:代表内联样式,如: style="",权值为1000。
2. 第二等:代表ID选择器,如:#content,权值为0100。
3. 第三等:代表类,伪类和属性选择器,如.content,权值为0010。
4. 第四等:代表类型选择器和伪元素选择器,如div p,权值为0001。
5. 通配符、子选择器、相邻选择器等的。如*、>、+,权值为0000。
6. 继承的样式没有权值。
14.px 和 em 的区别
答:px是固定的,em会继承父级
15.position的值有哪些,有什么区别
答:position属性有4种取值static、fixed、relative、absolute,其区别是:
1、static:静态定位,是position属性的默认值,表示无论怎么设置top、bottom、right、left属性元素的位置(与外部位置)都不会发生改变。
2、relative:相对定位,表示用top、bottom、right、left属性可以设置元素相对与其相对于初始位置的相对位置。
3、absolute:绝对定位,表示用top、bottom、right、left属性可以设置元素相对于其父元素(除了设置了static的父元素以外)左上角的位置,
如果父元素设置了static,子元素会继续追溯到祖辈元素一直到body。
4、relative:生成相对定位的元素,相对于元素本身的位置进行定位,它原本所占的空间仍然会保留。
static(静态定位)是默认值,元素出现在正常的流中。不会受到top, bottom, left, right影响。定位为absolute的层脱离正常文本流,
但与relative的区别是其在正常流中的位置不再存在。
如果父级没有设定position属性,那么当前的absolute则以浏览器左上角为原始点进行定位,位置将由偏移设置(top、bottom、left、right)决定;
(这与relative完全一致)。
16.盒子垂直水平居中怎么实现?
答:方法一:利用文本水平居中text-align: center和行高line-height进行实现
方法二:利用子绝父相和外边距margin实现
先为父盒子设置相对定位,再为子盒子设置绝对定位,且绝对定位的四个方向的位移都设为0,然后将外边距margin属性值设置为auto即可。
方法三:利用子绝父相和左、上外边距实现
先为父盒子设置相对定位,再为子盒子设置绝对定位,且将子盒子分别向右、向下移动父盒子的一半,然后利用外边距margin将子盒子分别向左、
向上移动子盒子的一半。
方法四:利用子绝父相和位移实现
先为父盒子设置相对定位,再为子盒子设置绝对定位,且将子盒子分别向右、向下移动父盒子的一半,然后利用位移transform: translate;
将子盒子分别向左、向上移动子盒子的一半。
方法五:利用flex布局实现
首先在父元素中添加display:flex;使其显示模式为flex布局模式,然后在父元素中添加主轴居中和侧轴居中即可。
17.this指向
答:(1)在全局环境中的this——window
无论是否在严格模式下,在全局执行环境中(在任何函数体外部)this 都指向全局对象。
"use strict"
console.log(this); //window
console.log(this===window);//true
(2)在函数中的this——window
在函数内部,this的值取决于函数被调用的方式。
function f() {
console.log(this); //window
console.log(this===window);//true
}
f()
console.log(f()===window.f()); //true
因为定义的函数在全局作用域下定义的
(3)函数在严格模式下——undefined
function f() {
"use strict"
console.log(this); //undefined
console.log(this === window); //false
}
f()
上面的f是直接调用的指向undefined
function f() {
"use strict"
console.log(this); //window
console.log(this === window); //true
}
window.f()
有一些浏览器最初在支持严格模式时没有正确实现这个功能,于是它们错误地返回了window对象。
(4)对象中的this——指向调用者
let obj = {
fn: function () {
console.log(this);
}
}
obj.fn() //指向obj这个对象
(5)栗子①
function fun() {
console.log(this.name);
}
let obj = {
name: '奥特曼',
fn: fun
}
var name = "怪兽"
obj.fn() //奥特曼
fun() //怪兽
obj.fn() 是obj 调用的所以去找obj里面的name
fun是window调用的所以去找全局里面的this.name
(6)栗子②
var obj1 = {
name: '怪兽',
f: function () {
console.log('姓名:' + this.name);
}
}
var obj2 = {
name: '奥特曼'
}
obj2.f = obj1.f
obj1.f() //姓名:怪兽
obj2.f() //姓名:奥特曼
把obj1.赋值给obj2.f obj2也有了f 方法
(7)栗子③
function foo() {
console.log(this.a);
}
var obj2 = {
a:2,
fn:foo
}
var obj1={
a:1,
o1:obj2
}
obj1.o1.fn() //2
obj1里面的o1是obj2 obj2里的fn是foo函数 在obj2里面调用的拿到obj2中的a
(8)事件绑定中的this
<button οnclick="Hclick()">点击事件</button>
<script>
function Hclick() {
console.log(this);
}
</script>
由于还是在当前window环境下运行的还是指向window
<button οnclick="console.log(this)">点击事件</button>
运行在节点对象中 指向当前dom
(9)动态绑定
<button>动态绑定</button>
<script>
let btn= document.getElementsByTagName('button')[0].οnclick=function(){
console.log(this);
}
</script>
指向当前dom
(10)addEventlistenr——当前dom
let btn = document.getElementsByTagName('button')[0].addEventListener('click',function () { console.log(this); })
指向当前dom <button>动态绑定</button>
let btn = document.getElementsByTagName('button')[0].addEventListener('click',()=>{
console.log(this);
})
换成箭头函数后 this指向当前作用域下的上级作用域的this window
(11)构造函数中的this——当前实例化对象
function Pro() {
this.x='1'
this.y=function(){ console.log(this);}
}
var p = new Pro()
p.y()
通过构造函数创建了一个新的实例对象 所以当前的this指向新的实例对象
(12)定时器中的this——window
setInterval(function () {console.log(this) },1000)
this指向当前window
18.什么是原型链
答:原型链:就是实例对象和原型对象之间的链接,每一个对象都有原型,原型本身又是对象,原型又有原型,以此类推形成一个链式结构.称为原型链
19.let var const的区别
答:var、let和const的区别
let和const是ES6新增的关键字,如果还不知道ES6的小伙伴们,建议好好去了解下。
区别1
let和var用来声明变量,const用来声明常量。
变量就是赋值后可以改变它的值,常量就是赋值后就不能改变它的值。
区别2
const不允许只声明不赋值,一旦声明就必须赋值
错误的写法
const num;
正确的写法
const num = 4;
区别3
var是函数作用域,let和const是块级作用域。
花括号{}就是块级作用域,函数作用域就是函数里面的内容。
对比这两段代码
{
let num = 4;
}
console.log(num);// num is not defined
{
var num = 4;
}
console.log(num); // 4
区别4
var有提升的功能,let和const没有
console.log(a); //undefined
var a = 4;
console.log(a); //a is not defined
let a = 4;
区别5
在最外层的作用域,也就是全局作用域,用var声明的变量,会作为window的一个属性
var a = 4;
function foo(){
var b = 5;
console.log("b=>"+b) // 5
console.log("window.b=>"+window.b) // undefined
console.log("window.a=>"+window.a) // 4
}
foo()
console.log("a=>"+a) // 4
console.log("window.a=>"+window.a) // 4
console.log("window.b=>"+window.b) // undefined
而用let和const声明的变量或常量,并不会作为window的属性
对比下面两段段代码
var a = 4;
function foo(){
/*
这里的this采用默认的规则,与window进行了绑定,所以实际上访问的是window.a
*/
console.log(this.a);// 4
}
foo()
let a = 4;
function foo(){
/*
在这种情况下,this.a 访问的是window.a,但是let定义的变量,并不会作为window的属性,所以访问不到
*/
console.log(this.a);// undefined
}
foo()