作用域
1.包括全局作用域和局部作用域,全局作用域作用于整个script标签内部,局部作用域作用域函数内部。
2.Window对象是一个全局对象,在全局作用域中创建的变量都是window对象的属性,创建的所有函数都是window对象的方法.
3.全局作用域中的变量称为全局变量,可以在全局作用域中的任何一个部分访问这个变量,也可以在函数内部访问这个变量。在局部作用域也就是函数中声明的变量叫做局部变量,只能在函数内部访问这个变量,如果在函数外部访问,会报错。(函数内部没有声明而直接赋值的变量也可以看做全局变量,即,可以在函数外调用,但是不建议这样使用)
预解析
1. 浏览器解析js之前会进行预解析,即 使用var 声明的变量,声明会被提前,但是赋值不会提前,使用函数声明 即 function fn ( ) { }声明的函数会被提前,函数调用不会提前,但使用函数表达式创建的函数 即var fn = function(){ }不会被提前
2. 作用域链:内部函数访问外部函数的变量,采取链式查找的方式决定取哪个值,采取就近原则
预编译
1.两个规律:
(1)如果一个变量没有声明,而是直接赋值(无论是函数内还是函数外),如a=100则可以通过console.log(a)打印这个变量,因为没有声明而直接赋值的变量a,可以看作是window.a 且可以看作全局变量
(2)如果在全局作用域中通过var声明了变量a,那么也可以看作window.a,
2.函数预编译,发生在函数执行的前一刻。
(1)创建AO对象。AO即 Activation Object 活跃对象,其实就是「执行期上下文」。
(2)找形参和变量声明,将形参名和变量作为 AO 的属性名,值为undefined。
(3)将实参值和形参统一,实参的值赋给形参。
(4)查找函数声明,函数名作为 AO 对象的属性名,值为整个函数体。
Ps ,在函数预编译过程中,提升了变量声明和函数声明,在之后顺序执行时已经不再读,也就是放入AO对象中的属性的相关语句之后已不再读
juejin.cn/post/684490… Js2-36页
this 指向(执行上下文就是执行时的环境,在不同的环境中,执行结果可能不同
)
1. 以函数的形式调用时(包括普通函数,定时器函数.立即执行函数),this指向全局对象window,因为这些函数可以看作window对象的方法,即window.setTimeout();
2.以方法的形式调用时,this指向调用该方法的对象
3.以构造函数的形式调用时,this指向实例对象
4.以事件绑定函数的形式调用时,this指向绑定事件的对象
5.使用 call 和 apply 调用时,this 指向指定的那个对象
Call( ) Apply( ) bind( )
1.call( ) 可以调用函数,可以改变函数内部的this指向,可以实现继承
(1)利用call调用函数
function f1(){ 此时f1.call(this),相当于调用了函数f1,且没有改变f1的this指向,等价于f1();
alert(‘lulu‘);}
(2)利用call改变函数的this指向:
Var obj = {
name:’lulu’, 此时fn.call(obj);即使函数fn内部的this从指向window 变为了
age:25,} 指向obj,此时,console.log(this)的结果即obj,console.log(this.name);
function fn( a,b) { 的结果就是‘lulu‘
console.log(this); 总结:当利用f1.call(f2)改变了f1中的this指向后,
console.log(this.name);
console.log(a+b)}
fn.call(obj,2,3)
===============================================
function Class1() {
console.log(this) 此时this已经指向了class2所以这里的结果为class2函数
this.showNam = function() {
alert(this.name);} }
function Class2() {
this.name = "class2";
alert('lulu')}
Class1.call(Class2) 这一步即改变了this的指向,又调用了class1 所以class1中的
Console.log(this)此时已经执行了 结果为class2整个函数
Class2.showNam() ; 由于this已经指向了class2,所以只有通过class2才可以使用class1
方法但并不意味class2已经被调用了,此时弹出‘class2‘,因为this已经指向class2.
var c2 = new Class2() 此时才是调用了class2,class2不被class1影响 .但只有
在这里调用了class2才可以弹出‘lulu‘
总结:Class1.call(Class2)改变了Class1中的this指向,使其从原来的指向class1变为了指向class2时,同时调用了Class1,等价于var class1=new class1(),此时class1中的所有属性和方法都相当于成为了class2的, 必须通过class2.属性 和class2.方法才能使用原本class1中的属性和方法。但并不影响原本class2中的内容。且可以在class2未被调用时就使用这些方法和属性。
(3)利用call实现继承
function Father(myName, myAge)
this.name = myName;****
this.age = myAge;}****
function Son(myName, myAge) {****
Father.call(this, myName, myAge);}****
var son = new Son('lulu', 25); .
此时this指向的是son中的this,指向的是son的实例对象,通过call将father中的this指向修改为了指向son的实例对象,此时son继承了father的所有属性和方法,son的实例对象可以通过传递参数,使用father中的属性和方法
3中继承与2中的改变this指向不一样,2中是将class1中的this指向改为了指向class2,所以在class1中console.log(this)的结果为class2整个函数。而继承是将father中的this指向改为了指向 son的实例对象,所以在father中console.log(this)结果为son对象(son的实例化对象)。
总结:通过call改变函数内部的this指向,实质上是为了是c2可以使用c1中的属性和方法,
如果在Class1.call(Class2)之后再通过var c1=new Class2();则Class1中的console.log(this)
结果仍是class1的实例对象
2. apply()可以调用函数,改变函数内部this指向,但使用apply()时,参数必须以数组或伪数组的形式存在,即便只有一个参数也必须写成数组的形式 如fn1.apply(obj1, ['hello'])
3. bind() 方法不会调用函数而是返回一个新函数但是可以改变函数内部的 this 指向。
Call apply bind 三种方法的异同
三者都可以改变函数内部的this指向,但call和apply都可以直接调用函数,而bind返回的是原函数改变this之后产生的新函数,需要单独调用后再使用,并且,call和bind参数传递时,实参都是用逗号隔开并直接传递的,而apply中的实参必须是以数组的形式传递。
改变this指向的目的:
当一个对象A具有一个方法fn,另一个对象B没有方法,但是需要用到同样功能的fn方法时,可以通过改变A对象中函数fn的执行上下文(this)来实现调用,达到节约代码空间,不产生冗余函数的目的。
不用想的太复杂,实际就是通过f1.call(f2)使f2可以使用f1中的属性和方法,看到这句话就当成现在f2可以使用f1方法了,不用考虑调用不调用的。如继承中的Father.call(this)就是使son的实例对象可以使用father的实例对象的方法和属性,既不影响原函数f1的使用也不会影响f2中其他方法的使用,可以通过f1.方法.call(f2)来使f2可以使用f1中的方法,也可以分开写成f1.call(f2);f2.方法();而bind只是多了一步调用,也就是可以不马上使f2使用f1中方法,先var lulu = f1.bind(f2);再在需要的时候lulu();f2.方法();
闭包
闭包就是指有权访问另一个函数作用域中的变量的函数。
如果一个函数位于另一个函数的内部,那么这个内部函数可以访问外部函数作用域中的局部变量、参数和其他内部函数,此时在外部函数中调用这个内部函数,就形成了闭包。
function closure(){
var str = "I'm a part variable.";
return function(){ alert(str); } } 这里也可以写成funcition fn(){ alert(str); } 再使用fn()调用
var fObj = closure();
fObj();
如果course函数执行完毕,函数内的局部变量str会被销毁,但由于在内部函数fn中使用了str,
且在course中调用了(返回了)内部函数fn,且内部函数又使用了str这个变量,所以str所占用的资源不会被回收。这样closure就形成了一个闭包。
function fn1() {
let a = 20;
function fn2() {
console.log(a);
}
return fn2;}
const foo = fn1();
foo();
===========================================
function fn1() {
let a = 10;
function fn2() {
console.log(a);}
fn2();}
fn1();
基本数据类型和复杂数据类型
基本数据类型:数字型 字符串型 布尔型 null undefined
基本数据类型的值保存在栈内存中,值与值之间单独存在互不影响,改变一个变量,不会影响其他变量。(值保存在变量中,变量保存在栈中)
引用数据类型(复杂数据类型):对象
引用数据类型的值保存在堆内存中,栈内存中保存的是对象的内存地址(对象的引用)。
所以如果两个变量保存的是同一个对象引用,它们指向同一堆内存空间,当通过一个变量修改属性时,另一个变量也会受到影响。
var obj1 = new Object();
obj1.name = "孙悟空";
var obj2 = obj1 ; // 这句话相当于将 obj1 的地址赋值给 obj2。也就是此时obj1 和 obj2 指向了同一个堆内存空间,也就是指向了同一个对象。
obj2.name = "猪八戒"; //当我们修改obj2的name属性,实际是修改了对象的属性,所以obj1的name属性也会随之修改,此时在console.log(obj1.name)得到的结果不再是‘孙悟空‘而是‘猪八戒’,
让obj1和obj2相互不受影响的话,可以通过 Object.assign() 来复制对象,但只能实现浅拷贝
JSON.stringify(obj2) 用于JavaScript 值转换为 JSON 字符串。(通常用于数组和对象)
浅拷贝和深拷贝
浅拷贝:只能拷贝最外层数据;更深层次的对象,只能拷贝引用。
1. 通过for in实现浅拷贝:
Const obj1={
name:’lulu,
age:25,
info:{
desc:’这是1‘}
const obj2={ };
for(let k in obj1)
{obj2[k]=obj1[k];}
Obj2.name = 'lulu';
Obj2.info.desc=’这是2‘
console.log('obj1' + JSON.stringify(obj1);
console.log('obj2:' + JSON.stringify(obj2));
结果为:
obj1:{"name":"qianguyihao","age":28,"info":{"desc":"这是2"}}
obj2:{"name":"lulu","age":28,"info":{"desc":"这是2"}}
即for in实现了浅拷贝,拷贝了name age info这些外层数据,但对于更深一层的desc只是拷贝了它的引用,当我们修改obj2中的name age info时,不会影响obj1,但如果修改了obj2中的desc,obj1也会随之更改。
2. 通过Object.assgin()实现浅拷贝
const myObj = {
name: 'qianguyihao',
age: 28,
};
const obj1 = {};
Object.assign(obj1, myObj); //其中obj1为目标对象。Myobj为源对象
总结: 如果只用const obj2=obj1 则复制的是obj1的地址,obj2更改任何一个属性都会影响obj1.使用for in 和object.assign()复制的是最外层的数据,对于外层的name age info 改变obj2不会影响obj1,而对于内层的desc是复制了地址, 改变了obj2会影响obj1。
深拷贝:拷贝多层数据;每一层级别的数据都会拷贝。
深拷贝其实就是将浅拷贝进行递归。
function deepCopy(newObj, oldObj) {
for (let key in oldObj) {
let item = oldObj[key]; // 获取属性值 oldObj[key]
if (item instanceof Array) { // 判断这个值是否是数组
newObj[key] = [];
deepCopy(newObj[key], item);
} else if (item instanceof Object) { // 判断这个值是否是对象
newObj[key] = {};
deepCopy(newObj[key], item);
} else {
// 简单数据类型,直接赋值
newObj[key] = item; }}}
let obj1 = {name: 'qianguyihao',age: 28,info: {desc: 'hello',},color: ['red', 'blue', 'green'],};let obj2 ={};deepCopy(obj2, obj1);obj2.info.desc = 'github';
此时再更改obj2的任意属性值都不会影响obj1了,因为每层复制的都是数据而不是地址
获取元素属性值:
div.属性及div.style.属性及div.getAttribute(‘属性‘)都只能获取行内样式的属性值。
设置元素属性值:
div.属性=””,div.style.属性=“”,div.setAttribute(‘属性‘,’属性值‘)都是修改了行内的属性,如果行内原本不存在这个属性,则是在行内新加了这个属性,(使其优先级最高)并不是修改中和css文件中的属性,只是优先级更高覆盖了和css文件中的属性
如果想获取css文件或中的样式可以通过下面方法:
window.getComputed(div).getPropectyValue(‘color‘)
如果想整体修改css样式可以直接使用div.className = ‘lulu‘或setAttribute(’class‘,’lulu‘) 通过修改类名来修改样式。
Js 动画主要内容包括
1. 三大家族和一个事件对象。三大家族包括offset 、scroll和client
2. 动画
3. 冒泡 兼容 和封装
Offset
Offset即偏移,可以通过offset相关属性动态获取元素的位置大小等。
1. offsetparent:获取当前元素的定位父元素。
如果父元素有absolute、fixed、releative定位,则获取的是最近一级父元素,如果没有定位,则获取的是body。
2. offsetwidth:获取当前元素的(宽度 + padding-left + padding-right + border-left + border-right)注意没有单位且不包括margin值。
3. offsetheight:获取当前元素的(高度 + padding-top + padding-bottom + border-top + border-botton)注意没有单位且不包括margin值。
4. offsetLeft:获取当前元素相对于其定位父元素的水平偏移量。
Offsetleft = 定位父元素的padding-left + 子元素的margin-left;
- offsetTop:获取当前元素的(宽度 + padding-left + padding-right + border-left + border-right)注意没有单位且不包括margin值。 1. offsetheight:获取当前元素的(高度 + padding-top + padding-bottom + border-top + border-botton)注意没有单位且不包括margin值。
2. offsetLeft:获取当前元素相对于其定位父元素的水平偏移量。
ofsetLeft = 定位父元素的padding-left + 子元素的margin-left;(不包括父元素边框)
注意:如果当前元素在行内设置了定位,则offsetLeft=子元素的margin-left+子元素left
(这是因为子绝父相会使父盒子的padding失效)
- offsetTop:获取当前元素相对于其定位父元素的垂直偏移量。
offsetTop = 定位父元素的padding-top + 子元素的margin-left;(不包括父元素边框)
同理,当给子元素设置了定位,则offsetTop=子元素的margin-top+子元素的top
在没有margin的情况下offsetTop=div.style.top,offsetLeft=div.style.left (top和left必须是行内样式)
offset 和style的区别
1. offset可以返回任意样式表中的样式,而style只能返回行内样式表中的样式。
2. offsetWidth包括padding border width,且得到的是数字型数据没有单位而style.width只包括width,得到的是字符串型数据,有单位。
3. offsetWidth是只读属性只能获取不能赋值,而style.width是可读写属性,既可以获取也能赋值
4. offset用于获取元素的大小位置,style用于设置大小位置
client
client即可视区,可以通过client相关属性动态获取元素的大小(内容的大小+padding 不包含边框)、边框大小等。
元素调用时:
div.clientWidth:获取元素的可见宽度(width + padding)。不包括边框和margin
div.clientHeight:获取元素的可见高度(height + padding)。不包括边框和margin
html/body调用时:
clientWidth:获取网页可视区宽度 随网页的放大缩小发生变化
clientHeight:获取网页可视区高度 随网页的放大缩小发生变化
ps:clientWidth和clientHeight属性是只读的,只能获取不能赋值修改,得到的是数字型数据没有单位。
事件对象调用时:
e.clientX:返回的是鼠标相对于浏览器可视窗口的X轴位置。
e.clientY:返回的是鼠标相对于浏览器可视窗口的Y轴位置。
.clientTop:返回的是元素上边框的大小
clientLeft:返回的是元素左边框的大小
ps 如果元素在css中设置了box-sizing:border-box;且宽度为500px,则offsetWidth返回的是500px,因为这500px包括了实际宽度300px、padding50px、border50px。而clientWidth返回的是400px,因为client不包括边框,所以减去了两侧边框的大小。
Scroll
Scroll即滚动,可以获取元素实际内容的大小,和滚动距离
Scrollwidth:获取元素内容的实际宽度 包括padding 不包括border
scrollHeight:获取元素内容的实际高度,包括padding 不包括border
注意:如果元素的内容超出了盒子的大小则Scrollwidth和scrollHeight为元素内容的实际宽高,如果元素的内容没有超出盒子,则Scrollwidth和scrollHeight为盒子的宽度(注意这里包括width和padding)
scrollLeft:获取水平滚动条滚动的距离也就是滚动条拖动时被卷去的左侧距离;
scrollTop:获取垂直滚动条滚动的距离也就是滚动条拖动时被卷去的上册距离;
Tips :
当scrollHeight(内容的实际高度)- scrollTop(被卷去的上侧距离) == clientHeight`(元素的可见宽度)时,说明垂直滚动条滚动到底了
当scrollWidth(内容的实际宽度) - scrollLeft(被卷去的左侧距离) == clientWidth(元素的可见高度)时,说明水平滚动条滚动到底了。
补充: overflow:auto;内容超出盒子时生成滚动条 onscroll()滚动条滚动事件
总结:
对于宽高来说:
offsetWidth:元素在页面中占有的实际宽度width + padding +border;
offsetHeight:元素在页面中占有的实际高度height+border+padding
clientWidth :元素的可见宽度 width+padding;
clientHeight:元素的可见高度height+padding;
scrollWitdh:元素的实际内容宽度,内容超过盒子则返回内容的宽度,不超过盒子则为盒子的width+padding
scrollHeight:元素的实际内容高度,内容超过盒子则返回内容的高度,不超过盒子则返回盒子的height+padding
对于left和top来说:
offsetLeft:返回当前元素相对于定位父元素的水平偏移;
offsetTop:返回当前元素相对于定位父元素的垂直偏移;
clientLeft:元素左侧边框的大小;
clientTop:元素上册边框的大小;
scrollTop:垂直滚动条滚动的距离
scrollLeft:水平滚动条滚动的距离
立即执行函数:
立即执行函数即可以马上执行,不需要调用的函数
主要作用是创建了一个独立的作用域,其中所有的变量都是局部变量,避免了命名冲突。
写法:
1. ( function(形参){ } )(实参)或(function getTime(形参){ })(实参)
先立即执行 后调用
2. ( function (形参){}(实参) ) 或( function getTime(形参){ }(实参) )
先调用后立即执行
Tips:当们封装了一个关于定时器的函数,用于实现某种功能,js中的每个对象都可以使用这个函数时,我们在函数内部平时使用var timer =setInterval()来存放定时器,但这样每次都要声明一个timer变量,由于js将所有标签都看作对象,所以我们可以使用div.timer=setInterval来存放定时器。也就是将timer看作是div对象的属性****
动画
1. 匀速动画: 利用offsetleft获取元素的当前位置,结合定时器,使元素当前的位置每隔一段时间变化一次 div.style.left=div.offsetLeft+100+’px’,并通过清除定时器使动画停止,注意清除定时器必须写在定时器内部。******
2. 缓动动画: 缓动即让动画运动的速度逐渐变慢,核心思想是:不再让盒子每次移动固定的距离,而是使用(目标距离-当前位置)/10使得盒子越靠近终点每一次移动的距离越短从而达到视觉上变慢的效果**
高阶函数:对其他函数进行操作的函数
函数B作为函数A的参数,或函数C(指的是C整个函数,而不是c的结果)是函数a的返回结果
function fn1(a, b, callback) {
console.log(a + b);
// 执行完上面的 console.log() 语句之后,再执行下面这个 callback 函数。也就是说,这个 callback 函数是最后执行的。
callback && callback(); 相当于if(callback){callback();}
}
fn1(10, 20, function () {
console.log('我是最后执行的函数');
}); //当函数fn1执行完毕后再返回来执行这个匿名函数,所以这个匿名函数是个回调函数
产生了闭包的函数也是一个高阶函数,因为将函数作为了返回结果
function fn1() {
let a = 20;
return function () {};}
const foo = fn1(); // 执行 fn1() 之后,会得到一个返回值。这个返回值是函数
foo();
轮播图的主要思路:
1. 将图片都放在ul的li中,并通过浮动使所有图片位于一行,如果ul的父盒子过小,导致图片无法在一行显示,则可以增大ul的宽度,并不影响实际效果。
2. 根据图片的个数动态生成小圆点,为了实现点击第几个小圆点就显示第几张图片,要将点击时,ul(图片的切换时靠移动ul实现的)的移动距离设为小圆圈的索引号*图片的大小。
3. 图片无缝衔接实现的方法是将第一张图片复制一份放在ul最后,当点击右侧按钮显示了该张复制图片时,ul快速的移动回初始位置。(复制图片不能在html中而是在js中实现且必须放在生成小圆点之后)
Ps:mouseover时当鼠标经过盒子时触发事件,但当鼠标经过该盒子的子盒子时会再次触发事件,而mouseenter只会在经过盒子时触发一次事件。 ES5 的一些扩展
Object 的扩展:
1. Object.create (原型对象,【属性】)以指定对象为原型,创建新的对象。第二个参数可以为新的对象添加属性,也可以省略
var obj1 = {
name:’lulu’,
age:25,’}
var obj2={address:‘克旗’}
obj2=Object.create(obj1) 相当于以obj1为原型,创建了一个新的对象obj2,所以原本obj2种属性address不存在了,
var obj2=Object.create(obj1,{ adress:属性名,不要忘记 :
{value:‘chifeng’, 属性值 ,必须通过value来设置
writable:false, 标识该属性是否可以被更改,可以省略,省略时默认为false,即不可修改
configurable:false,表示该属性是否可以被删除,可以省略,省略时默认为false,即不可删除
enumerable:false} })标识该属性是否能用for in(遍历对象),省略为false,即不可以使用for in
tips:可以使用delete 对象.属性来删除对象中的属性,也可以使用**Reflect.deleteProperty(对象,属性名) **来删除对象的属性。
2.get 方法和set方法
get 属性名(){ } 用来获取属性 需要return返回
set 属性名(){ }用来设置属性 不需要返回
使用get方法和set方法提高了灵活性,如果对象中的_name属性为‘lulu’,但我们想通过 对象.属性 来获得 ‘我的名字是lulu’,就可以get name(){return ‘我的名字是’+this.name} 中
var a={
_name:"小明",
_age:18,
get name(){
return ‘我的名字是’+this._name
},
set age(val){
this._age=val
}, }
console.log(a.name + a._age) //我的名字是小明18
console.log(a.name + a.age) // 此时我们只通过get获取了name,而对于age只用set设置了属性值,所以a.age 的返回值是undefined,必须用a._age得到_age属性的值。
a._name = "小红"; //此时我们只通过get获取了name,而没有使用set设置属性值,所以a.name是不可以通过赋值来更改属性值的。但可以通过a._name=’小红’来更改属性值,a._name=’小红’改变了对象中的_name属性,get方法动态的获取了新的_name值,最后的结果是我的名字是小红
a.age = 10; // 我们通过set设置了age,10就相当于实参传递给了set,所以成功更改了年龄
console.log(a.name + a._age) //我的名字是小红10
总结:get只能获取属性,不能设置属性,必须通过return返回属性值,使用get name()后,我们通过obj.name就可以获取到属性,不需要通过obj._name,但不可以通过给obj.name赋值来更改属性值。
set 只能设置属性,不能获取属性,不需要通过return返回结果,但是要有一个形参用来更改属性值,使用set age(val)后我们通过给obj.age赋值就可以更改_age的值,不需要通过给obj._age赋值来更改,如果只有set没有get,最后输出结果时还是要使用obj._age
ES6-let 命令
1. let用于声明变量,类似于var,如let a=10;但用let声明的变量只在变量所在的代码块内有效,在代码块外使用该变量会报错。(但用let声明的变量可以在当前代码块的内层代码块中使用,也就是在父作用域中用let声明的变量也可以在子作用域中使用)
for(var i=1;i<10;i++){
a[ i ]=function(){
console.log(i)
} }
a6; //结果为10
因为var声明的变量是全局变量,for循环中的所有i都指向同一个全局变量i,每次循环都相当于给全局变量i重新赋值,循环结束时i的最终结果为10.所以在for循环之外再调用函数时返回的是10。
for(let i=1;i<10;i++){
a[ i ]=function(){
console.log(i)
} }
a6; 此时结果为6,
因为let声明的变量只在变量所在的块作用域内有效,所以i只在当前循环内有效,每次循环都相当于新声明了一个变量i,a6调用的是第六次循环时的i ,此时i=6,所以在for循环之外再调用函数时返回的是6。
Ps:for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域(for(let i=1;i<10;i++)),而循环体内部{ }是一个单独的子作用域。所以如果在循环体内部{ let i = ‘lulu’
Console.log(i)}输出的结果为‘lulu’;
2. 使用var声明的变量在预解析时会发生变量提升,所以赋值可以写在声明之前,而let声明的变量不会变量提升,所以声明必须写在赋值前,否则会报错。也就是说在let声明变量之前,变量都是不可用的,称为暂时性死区。
3. Let 声明的变量也可以不赋值,返回undefined。
4. let命令不允许在同一个作用域内重复声明同一个变量:
function func() { function func() { function func(arg) {
let a = 10; let a = 10; let arg; } //这是因为形参相当于函数内
var a = 1; let a=1; func() 部的一个局部变量
} }
以上三种情况都会报错。即便重复的变量一个用let声明一个用var声明也会报错,也就是同一个作用域内,如果let声明了一个变量就不可以再重复声明这个变量。
块级作用域 用{ }括起
1.位于if和for中var声明的变量属于全局变量还是局部变量取决于 if和for位于全局作用域中还是局部作用域中。
var tmp = new Date();
function f() {
console.log(tmp);
if (false) {
var tmp = 'hello world';} }
f(); // 结果为undefined,因为if位于函数内部,if中的tmp变量是使用var声明的所以相当于是函数的局部变量,(相当于不把if看作函数),即便if语句没有执行,var tmp会被变量提升至函数内部的顶端,所以打印的结果为undefined。此时如果在if中使用let声明变量,结果就可以输出当前时间,因为let声明的变量只能在当前代码块也就是if中使用。
2. if语句和for语句的{ } 相当于块作用域,
3. 块作用域的嵌套也和函数的嵌套一样,内层的作用域可以使用外层作用域中声明的变量,包括let声明的变量。
{
let a = 10; {
console.log(a);
}
} //结果为10
4.在1中也已说明,如果块作用域位于函数内部,则在块作用域内使用var声明的变量相当于函数的局部变量。如果块作用域位于全局作用域中,则在块作用域中使用var声明的变量相当于全局变量,也会变量提升,提升至局部作用域或全局作用域的顶端。
- 函数作用域中的默认变量(没有用var let const声明,而直接赋值了的变量)可以看作全局变量,但是必须在函数调用后才能在全局作用域中使用。块作用域中的默认变量与函数作用域中的默认变量相同,也可以看作全局变量,但是必须在a=10;即赋值语句执行之后才可以在全局中使用。 1. 在全局中使用。
Console.log(b); { { {
{ console.log(b); b=10; b=10;
b =10; b=10; console.log(b) }
} //报错 } //报错 } //输出10 console.log(b)//输出10
2. 在块级作用域内声明函数(对于es6浏览器):
块内的函数声明(整个函数体 不包括函数调用)会提升到块内的顶部,同时也会在代码块所在的作用域顶部用var声明一个同名的变量,初始值为undefined:
function f() { console.log('I am outside!'); }
(function () {
var f = undefined;
if (false) {
function f() { console.log('I am inside!'); }
}
f();
}()); //结果会报错即f is not defined
为了避免这种情况,在快作用内声明函数优先使用函数表达式且用let存储变量:
{ let a = 'secret';
let f = function () {
return a;
};
}
const 命令
1. const声明的是一个只读常量,一旦声明,常量的值就不能更改,否则会报错,这也意味着,如果用const声明变量,必须在声明的同时就赋值,不能像var声明变量一样,先声明后赋值 var a;a = 10; 只能 const b = 10;
2. const声明的变量同let一样,只在该变量所在的代码块内起作用
3. const声明的常量也不存在提升,这就意味着与let一样存在暂时性死区,即常量的调用必须在常量声明完成之后进行,常量声明完成之前的区域就是暂时性死区。
4. const与let一样,不允许在同一作用域内声明重复的变量。
5. const的实质不是保证变量的值不可以改变,而是保证变量指向的内存地址中的数据不可以改变,对于简单数据类型,变量指向的内存地址中存放的就是数据,等同于常量,而对于复杂数据类型,变量指向的内存地址中保存的是对象的数据在堆中的地址(也就是指向实际数据的指针),所以如果用const保存一个对象,只能保证指针总是指向固定的地址,但是不能保证固定地址中的数据是否改变。也就是可以改变对象的属性但不可以指向另一个对象
const foo = {};
// 为 foo 添加一个属性,可以成功
foo.prop = 123;
// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only const保存的是一个只读常量。
如果想使对象中的数据也不改变可以通过const obj = Object.freeze( { } )创建对象,
此时在obj.name=‘lulu’就无效了。
6. 用const声明的常量存储数组,数组本身是可以增删改查数组元素的,但不能改变常量的赋值,使该常量存放另一个数组。
const a = [];
a.push('Hello'); // 可执行
a.length = 0; // 可执行
a = ['Dave']; //报错
顶层对象属性
顶层对象也就是window对象,在es5中,在全局作用域中声明的变量和函数可以看作使window对象的属性和方法,也就是window.a=1;等价于a =1;而在es6中,用var 和 函数声明声明的变量和函数仍是顶层对象的属性,但let命令,const命令,class命令在全局作用域中声明的对象不属于顶层对象的属性。
Var a =1;
Console.log(window.a)//输出1;
let a=1;
Console.log(window.a)//输出undefined;
由于顶层对象在各种实现里面是不统一的,es2020提供了global.This来获取顶层对象。
变量的解构赋值 –数组的解构赋值
1.ES6允许按照一定模式,从数组和对象中提取值来给变量赋值,被称为解构。
即可以通过let [a,b,c] = [1,2,3] 来给变量a,b,c赋值,此时
console.log(a) //1 console.log(b) //1 console.log(c) //3
这种写法称为 “模式匹配” ,只要等号左右两侧的模式相同,左边的变量就可以被赋予右侧的值。
嵌套数组:
let [a, [b],[c]] = [1,[2],3]; 左右两侧模式相同,a=1;b=2,c=3;
普通数组:
let [ , , c] = [1,2,3]; 左右两侧模式相同,但个数不同,但左侧数组相当于有两个空元素,但数组长度是3,此时c = 3;
左侧变量少于右侧值:(不完全解构)
let [c]=[1,2,3];左右两侧模式相同个数不同,左侧数组只有一个元素,此时会优先取第一个值c=1
右侧值少于左侧变量:(解构不成功)只要变量的值返回了undefined就是解构不成功
let x = [ ]; //此时返回undefined ,结构不成功
let [x,y] = [1]//此时x = 1 y返回undefined ,结构不成功
let [x, y, ...z] = ['a']; //此时x=‘a’;y = undefined;数组中的…z意味z是一个数组,z=[ ]
let [ …a] = [1,2,3] a=[1,2,3]
如果等号右侧不是数组,或者说不是可遍历的结构,则会报错。
2. 解构赋值允许给变量设置默认值,但只有变量===undefined(严格等于),默认值才会生效。
let [x = 1] = [undefined]; 此时x =1;
let [x=1]=[null];此时x = null;
let [x,y=1]=[1], 右侧值少于左侧变量,所以x=1;而y = undefined,此时默认值生效最终y返回默认值 1.
默认值也可以设为函数, 但是只有变量===undefined时,函数才会调用。
默认值也可以引用解构函数的其他变量,但必须在变量声明之后才能引用:
let [x,y=x]=[1]; 此时x返回1 y也返回1;
let [ x=y,y=1]=[ ];当x的默认值设为y时,y还未声明,所以报错
let [ x=y,y=1]=[2,3] 此时x,y被赋值了2和3,默认值不会生效,所以不会报错。
变量的解构赋值 –对象的解构赋值
1.let { foo, bar } = { foo: 'aaa', bar: 'bbb' }; 此时foo返回‘aaa’,bar返回‘bbb’,
对象的解构赋值与数组不同在于, 数组的解构赋值, 变量的顺序与值的顺序一致,即第一个值赋给第一个变量,而对象的解构赋值,对象的属性没有次序,只要对象中含有这个属性就可以将属性值赋给变量。
即let { bar,foo } = { foo: 'aaa', bar: 'bbb' }foo还是返回‘aaa’,bar还是返回‘bbb’,
1. 对象的解构赋值可以将现有的对象的方法赋值给变量:
let {log,sin,cos} = Math;相当于将Math对象的Math.log(),Math.sin(),Math.cos()三个方法赋值给了变量 log,sin,cos,此时可以通过log(5)求5的对数,sin(5)求5的正弦值。
Let{log}=console;相当于将console对象的console.log()方法赋值给了变量log,此时可以通过log(5),在控制台输出5. 注意:变量名必须和方法名一致。
2. 如果对象解构赋值时,变量名和属性名不一致:
Let {bar} = { foo:1,}则bar的返回结果为undefined。
写成:let {foo:bar}={foo:1}则bar的返回结果为1 ,而输出foo会报错
这说明,对象的解构赋值let { bar,foo } = { foo: 'aaa', bar: 'bbb' }相当于是
Let {bar:bar,foo:foo}={foo:‘aaa’,bar:’bbb’}的简写,也就是说,对象的解构赋值实质是先找到同名的属性foo,再将值赋给变量foo,也就是说左侧的的foo实际是属性名也就是模式,划线的foo是属性值也就是变量,而对象的解构赋值是将右侧的属性值赋给左侧的属性值
let obj = { first: 'hello', last: 'world' };
let { first: f, last: l } = obj; 此时f返回‘hello‘ l 返回’world‘ 而输出first 和last 会报错,因为first和last是模式,f和l才是变量。
3. 对象的解构赋值也可以嵌套:
let obj = {
p: [
'Hello',
{ y: 'World' }
]
};
let { p: [x, { y }] } = obj;相当于let { p: [x, { y:y }] } = obj;
此时左侧p是属性名,也就是模式,而[x,{y}]才是属性值,也就是变量,右侧p是属性名,与左侧p匹配,变量x的赋值相当于数组的解构赋值即将数组的第一个元素hello赋值给x,变量y的解构赋值也就是对象的解构赋值,将右侧y属性的属性值world赋值给y。
let { p, p: [x, { y }] } = obj;此时左侧相当于let{p:p,p:[x { y } ] },所以变量p的返回结果为右侧p属性的属性值也就是 ["Hello", {y: "World"}];
const node = {
loc: {
start: {
line: 1,
column: 5 } } }
let {
loc: loc , loc: { start: start },loc: { start: { line: line, } } } = node;
console.log(loc); / / {start: {…}}
console.log(start); // {line: 1, column: 5}
console.log(line); //1
4. 对象的解构也可以设置默认值,当变量===undefined时,默认值会生效
var {x, y = 5} = {x: 1}; 此时x返回1,y===undefined,所以默认值5生效,最终y返回5
var {x: y = 3} = {x: 5}; 此时左侧x是模式,y是变量,右侧x是属性,5是属性值,对象解构赋值是将属性值赋给变量,所以y的返回结果为5.
var {x = 3} = {x: undefined};相当于 var {x:x=3}={x:undefined},变量x的赋值为undefined,默认值生效最终x返回3
5. 由于数组本质是特殊对象,所以可以对数组进行对象属性的解构
let arr = [1, 2, 3];
let {0 : first, [arr.length - 1] : last} = arr;
first // 0相当于属性名,指的是数组的第0号元素,first相当于属性值,指的是数组的第0号元素的值。所以first返回1.
last // arr.length-1即2,指的是数组的第二号元素,last相当于属性值,指的是数组的第2号元素的值,所以last返回3.
变量的解构赋值 –字符串的解构赋值
字符串也可以解构赋值,因为将字符串转换为了一个类似于数组的对象。
Const [x,y,z,h] = ‘lulu’; 此时x返回l,y返回u,z返回l,h返回u。
变量解构赋值的用途
1. 交换变量的值:
let x = 1;
let y = 2;
[x, y] = [y, x]; 不再需要在声明一个temp变量用于交换。
2. 从函数返回多个值:
函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,从返回的数组或对象中取这些值就非常方便了。 (还有其他几种,但现在并不理解,暂且搁置)