对JavaScript的一些细节以及ES6自我总结,重新深入理解其语法规则
作用域
首先要重新总结的是作用域,作用域伴随着我们学习的始终,如果作用域理解不好,会影响我们this指代、闭包、函数等问题处理不当
JS是一种静态作用域,根据书写的位置,一层一层向上查找
js的执行是依靠作用域链的
函数作用域可以分为以下几类
全局作用域
故名思意,全局作用域可以在任意位置访问到
var a=5; //全局变量定义方式
a=5; //全局属性的定义
虽然上述两种定义都可以取到它的值,但是他们的本质是不一样的,全局属性是可以通过delete删除的,并且这种方式也并不规范
函数作用域
也称为局部作用域,它的变量有效区域仅在函数内部有效,所以还称它为函数作用域
function test () {
var a =5;
console.log(a);
} //a=5
console.log(a); //a=undefined
我们可以将函数的{看作是一堵墙,墙内可以访问墙外的变量,但是墙外访问不到墙内的变量,
所以墙内的变量相对于墙外的变量就是私有的
但是为了能够在墙外访问墙内的变量,我们可以通过return返回值的方式和闭包的方式做到这点
function test () {
var a =5;
function show () {
console.log(a)
}
return show
}
var key = test()
这就是一个闭包函数,我们可以在函数外访问到变量a。但是为什么这样就能实现闭包呢? 其本质原因还是在于作用域,由于函数test返回了一个函数,并复制给了key,通过调用key我们可以运行这个函数。函数执行过程依然记得自己的作用域实在函数test的内部,所以查找变量会回到这个函数作用域中去查找,这就是闭包函数的原理
注意
function test () {
a=5; //这种情况,不区分变量在哪定义,一律可以访问到
}
console.log(a) //a=5
这是一个特殊情况,我们刚才也介绍过,这种定于方式是在window全局对象下,添加了一个a的属性,即使我们看到它出现在函数内部,但是我们仍然可以全局访问到它。因为它是window 的一个属性,我们访问它可以不加window直接访问
块作用域
函数仅仅区分全局和局部作用域是不够细致的,因为我们可能会要求在局部作用域中进一步区分全局和局部
function test () {
var a=5;
if (a==5) {
var b=6;
}
console.log(b) //b=6
}
var工作原理
为什么var定义的变量不能事项块作用域的效果呢?原因在于变量提升,要知道即使是在if中定义变量,但是由于变量提升的缘故,浏览器会实现下面这个过程,所以即便是if内定义,仍然会被外部访问到
function test () {
var a=5;
var b;
if (a==5) {
b=6;
}
console.log(b) //b=6
}
这里我们看到在函数作用域中,包含了if区块,而这个if区块我们就可以称它为块作用域。以上面定义的方式来看,我们在函数作用域中可以访问到块作用域的变量b,并没有起到块作用域的作用,这将导致一个变量不能为不同的块作用域所用。实际上,就是在函数作用域中将变量局部化
function test () {
var a=5;
if (a==5) {
let b=6;
}
console.log(b) //b=undefined
}
为了实现块作用域,我们引入了let,const。这样我们就可以在块作用域中定义变量,为不会污染外界变量了
动态作用域
动态作用域就是它所指定的区域是动态变化的
a=5;
function test() {
console.log(this.a) //a=5
}
test.bind({a:10})() //a=10
this.a并没有任何变化,但是输出的值确是不一样的,引文通过bind的方式改变了this所指的用域,这就是动态作用域
关于let和const的补充
let
let创建的变量是不能通过window访问到的
let一旦定义是不允许重复创建的
let不会进行变量提升
const
const创建的是常量,但是可以添加操作
不允许先声明再赋值
遍历方法
1、for
for是最基础的遍历方法,再for循环中,支持continue和break这种操作
for (let i ; i < arr ; i++){
console.log(i)
}
2、forEach
只是对for循环的方法增强,遍历不改变数组的值
forEach大大简化了for循环的篇幅,不需要判断循环的条件,所以循环必须从头到尾,中间没有中断,故break和continue不能使用
const arr = [1,2,3,4,5,6]
arr.forEach((item)=>{
console.log(item)
}}
3、every\some
由于forEach这种循环不能在循环过程中中断,所以我们为能够中断,我们提出了every和some方法。遍历同样不会改变数组,其主旨是遍历
every
every循环方式和forEach类似,但是每次循环必须返回true才能继续循环,一旦返回值为false,循环终止
const arr = [1,2,3,4,5,6]
arr.every(item=>{
if(item===2){
}
console.log(item)
return true
})
some
和every十分类似,但是每次循环返回必须是false,一旦返回值为true,循环终止
const arr = [1,2,3,4,5,6]
arr.some(item=>{
if(item===2){
return false
}
}) //输出结果为 1,3,4,5,6
4、for... in...
for---in是为遍历对象而提出的遍历方式
5、for... of...
for---of是为遍历除了数组和对象的遍历方式
6、object.keys/values/entries
通过Object.keys/value的方法,我们可以遍历对象得到对应的键数组和值数组。
注意返回值为数组,此时我们可以对返回值应用对数组的所有方法
const person = {
name: 'zhao',
age: 23,
sex: 'male'
}
console.log(Object.keys(person))
console.log(Object.values(person))
对于不可遍历对象,我们可以通过下面方法实现对象可遍历,并用for ... of 循环
for (let [k,v] of Object.entries(person)){
console.log(k,v)
}
7.ES10极客操作对象
日常操作中,数组和对象的相互转换都是常规操作,为了更方便的进行相互的转换,ES10为我们提供了强劲的API
数组转成对象
const var = [['name','zhao'],['age': 12]]
const res = Object.fromEntries(var)
对象转数组
const var = {
name: 'zhao',
age: 12
}
const res = Object.entries(var)
根据以上这两个API我们还可以进行组合,以达到对对象有对数组一样的操作。
8.map
map的功能和forEach的功能类似,但是唯一不同的是map会返回一个新的数组
let list = [1,2,3,4]
let test = list.map((item)=>{
return item*2
})
处理伪数组from
什么是伪数组?伪数组要满足2个基本点。首先,要按照索引方式排序;其次,要有长度
const arr = Array.from(document.querySelectorAll('img')
//将伪数组转换为数组,arr可以用响应的数组的操作
const arr = Array.from({length:5}, ()=>{return 1})
//伪数组遍历初始化数组,并填充默认值
其中from后参数分别是伪数组条件,对于每个元素的操作,this的指向
Array.prototype.of/fill
const arr = Array.of(1,2,4,5,3)
console.log(arr)
const arr = Array(5).fill(8)
console.log(arr)
其中()中的参数可以是填充数字,start位置,end位置
数组的查找方法
1、filter
filter查找是将数组中所有满足条件的值都筛选出来,返回值是一个数组
const val = [1,2,4,5,5]
const arr = val.filter(item=>{
if(item===5){
return item
}
})
console.log(arr)
2、find
由于filter要遍历数组中所有的值,如果想知道数组中是否存在某个值,效率低。所以我们用find方法可以返回第一个满足条件的值,注意!!!它的返回值只有一个值
const val = [1,2,4,5,5]
const arr =val.find(item=>{
if(item===5){
return item
}
})
console.log(arr)
3、findIndex
返回第一个满足条件值的位置,
const val = [1,2,4,5,5]
const arr =val.findIndex(item=>{
if(item===5){
return item
}
})
console.log(arr)
4、includes
同样是一种查找的方法,但是这是ES7中的新方法。可以将这个函数理解为包含,可以直接作用在数组上,判断这个元素是否包含在这个数组中
let ar = [1,2,34,5]
console.log(ar.includes(5))
直接输出对元素是否在数组中的判断
类
javascript在本质上是不存在类的,因为类原则上来说是复制,但是javascript中不存在复制关系,而是通过原型链建立起来的关系
类的本质
function test (val) {
this.type = val
this.eating = ()=>{
console.log('this is class')
}
}
const one = new test('ok')
const two = new test('ok')
console.log(one)
通过关键字new实现所谓的‘类’创建,其中变量one,two都会在new的一瞬间,创建一个自己的空间,空间存放着type和eating这两个属性
为了达到同样的目的,但是简化了步骤,我们在ES6中提出了class这个语法糖
原型链
原型链和链表的作用类似都是起到一个类似连接的作用
- 构造函数
- 构造函数原型
- 实例
原型链主要围绕这三个方面展开的
首先,每个构造函数都有一个自己的prototype,同时每个构造函数的prototype都有一个constuctor指向构造函数
function Person(name){
this.name = name
}
Person.prototype.say = function () {
console.log(this.name)
}
每个构造函数在创建的时候就已经有了一个prototype原型,这是他们两者之间的关系
其次,我们要分析的就是实例和原型的关系。
每个实例__proto__属性就是连接原型的属性
console.log(one.__proto__) //此时显示的就是Person的原型
class test {
constructor (val) {
this.type = val
}
eating () {
console.log('this is class')
}
}
const one = new test('ok')
console.log(one)
getter\setter
所谓的getter、setter就是对类的取值和赋值加以限定,而不是任何类创建的实例都可以自行自由的修改
class test {
constructor (val) {
this.type = val
}
get age(){
return 4
}
set age (val) {
this.realname=val
}
eating () {
console.log('this is class')
}
}
const one = new test()
console.log(one.age)
one.age=8
console.log(one.realname)
其中age实际上就是属性,只不过加以限定
类的静态方法 & 静态属性
静态和实例是有点相近的两个词,因为一个对象可以既有实例方法也可以有静态方法
function test (){
//实例方法
this.say = {
test.jump()
console.log('this is saying')
}
}
//静态方法
test.jump = function () {
console.log('this is jump')
}
实例方法是可以访问到实例上的属性的。我们需要通过实例.方法,调用实例方法
静态方法是绑定在类上的,不能访问到实例的属性。我们需要类.方法,调用静态方法
但是类.prototype.方法原型方法是实例和构造函数都可以调用,是共享的方法。
class test {
constructor (val) {
this.type = val
}
get age(){
return 4
}
set age(val) {
this.realname=val
}
eating () {
test.walking()
console.log('this is class')
}
static walking () {
console.log('i am walking')
}
}
const one = new test()
one.eating()
继承
在子类的constructor中添加super函数,其次是子类的构造函数
class test {
constructor (val) {
this.type = val
}
}
class best extends test{
constructor(val, age){
super(val)
this.age=age
}
go () {
console.log('you are the best')
}
}
const one = new best('ok',12)
console.log(one)
one.go()
类的属性描述符
通过类的属性描述符,我们可以整体查看具体属性
const person = {
one: 12,
two: 234,
three: 32
}
Object.defineProperty(person, 'three', {
writable: false
})
console.log(Object.getOwnPropertyDescriptor(person, 'three'))
person.three = 11
console.log(person)
函数参数的默认值
函数传参是一个很常见的行为,但是有时候我们要为这些参数设置默认值,在es5中还要做判断,在es6中给我们提供了更为简单的方法
function count (a, b=7,c){
const sum = a+b+c
console.log(sum)
}
count(1,undefined,2)
//注意:对于传空的情况,我们要将空值设置为undefined
对于参数我们可以进行运算赋值
function count (a,b=7,c=a*b)
我们还可以通过count.length获取缺省参数的个数
...代替arguments的操作
function count (a, ...num){
let sum = 0
console.log(num)
num.forEach(item=>{
sum += item
})
return a*2+sum
}
console.log(count(1,2,3))
...num可以接收剩余变量传入的值,同时num为一个数组
...的反操作
let arr = [2,3,5]
function sum (x=1,y=2,z=3){
return x+y+z
}
console.log(sum())
console.log(sum(...arr))
当我们要传值的参数是一定义好的数组时,我们通过...可以将这个数组准确的分配给函数中的形参,这个操作类似于
sum.prototype.apply(this, arr)
this指向问题
关于this指向的问题可以说是让很多人晕头转向,搞不懂的话真的全靠猜。其实this的指向是非常有规律的
1、有调用对象
是我们最常见的this指代问题,当有明显的调用对象时,这个this就直接指向这个对象
var obj = {
a:'the first',
foo: function () {
console.log(this.a)
}
}
obj.foo() //'the first'
当没有明确的调用的时候
var a='global'
function test () {
console.log(this.a)
}
test() //'global'
2、箭头函数
箭头函数的this和我们之前理解的不太一样,在箭头函数中this的指代和创建箭头函数时的this指代是相同的。也就是说,箭头函数的this继承了创建时的this
var test = {
a:'part',
one:{
a: 'one',
foo: function two(){
setTimeout(() => {
console.log(this.a)
}, 2000)}
}
}
test.one.foo() //'one'
3、new创建对象
new关键字我们常常用来创建类,但是之所以能实现类的功能,全是因为new创建实例时,发生的一系列操作。
1、构造一个全新的对象
2、将实例对象的prototype绑定到构造函数的prototype上
3、将this绑定在实例对象上
4、将构建好的对象返回
4、指代丢失
当我们将带有this的函数赋值给其他变量并且调用时,会发生this指代丢失的现象
var obj = {
a:'this is one',
foo: ()=>{
console.log(this.a)
},
fun: function() {
console.log(this.a)
}
}
var a = 'global'
obj.fun() //'global'
obj.foo() //'this is one'
var b=obj.fun
b() //'global'
目前来看,我区分箭头函数this指向的方法就是看它的词法作用域,是全局作用域还是函数作用域
apply/call/bind
var obj = {
a: 'zhao'
}
function fun (val1,val2) {
console.log(a,val1,val2)
}
fun.call(obj,1,2)
call和apply都会更改this的指向,但是不同的是后面跟的参数。call后面可以跟多个参数,apply后面则只跟一个数组。
但是bind和apply/call最大的区别就是绑定this的时候是否执行,apply/call绑定this的同时,绑定的函数是会执行一遍的
ES6中关于对象操作的更新
简便定义
当键值和键同名时可以只写键名,同时还允许[变量]的形式作为键名
a=1,b=2,c=3,z=4
let obj = {a,
b,
c,
[z]:5
}
set数据结构
set中所存的数据都是没有重复的,集合中的数都是无序且唯一存在的
我们可以将创建好的有重复元素的数组作为参数,传入到set中,返回的就是去重后的集合,通过...展开为数组
增:set.add(1,2)
删:set.delete(content)
查:set.has(content)
set转换成数组的方法:
[...setlizi]
Array.from(setlizi)
map数据结构
键值对的数据结构称为字典,它重要的特点是根据索引值进行增删改查的操作
增:map.set(1,2) 第一个值为键名(键名可以是任意类型),第二个值为键值
删:map.delete(index)
查:map.get(index)
字符串模板
在es5中,字符串的拼接都是要用过'',+实现字符串和自变量的拼接,如果稍有不慎,我们就会因为缺少符号而拼接报错,或者格式不理想。es6为了实现更加高效的字符串拼接,提出了模板字符串
const one = 1
const two = 2
const str = 'this is the sum'
console.log(`haha, ${str} ${one+two}`) //'haha, this is the sum 3'
在字符串模板中我们不仅能够实现拼接变量,我们还可以处理复杂的逻辑判断和控制换行
function test(string, val) {
const one = string[0]
const two = string[1]
let last
if (val ==='things') {
last = 'win'
} else {
last = 'what'
}
return `${one}${last}${two}`
}
const str = test`what is ${'things'} emm hemm`
console.log(str)
由上面我们可以看出我们不仅可以实现拼接功能,还是实现拼接前复杂的判断过程
const str= `oneoneone
twotwotwo`
console.log(str)
最后通过字符串模板,我们可以实现字符串换行输出的效果
ES6补白
我们为了整齐的输出一串数字,我们用字符补齐缺位,如下面这个函数用0进行补位
for ( let i=0 ; i < 35 ; i++) {
if (i<10) {
console.log(`0${i}`)
} else{
console.log(i)
}
}
但是这种补位方法不够灵活,并且复杂。在ES6中我们有API更好的处理这种情况
for ( let i=0 ; i < 35 ; i++) {
console.log(i.toString().padStart(2,'0'))
}
padStart表示在头部进行补白,第一个参数是值字符串有几位,第二个参数是补位所需要的字符
解构赋值
解构赋值为我们从数组或者对象中提取值和赋值提供了极大的方便,其实什么样的数据类型我们可以进行解构赋值,可遍历的数据对象都可以解构赋值
对于数组的解构赋值
数组的解构
let arr = [1,2,3,4,5,6,7,8]
let [one,two,,four,...last] = arr
console.log(one,two,four.last) //1 2 4 (4) [5, 6, 7, 8]
如果解构的数组为空,那么解构得到的变量为undefined
同时解构赋值也可以应用于循环运算中
对于对象的解构赋值
对于对象的解构赋值,我们要知道它赋值的本质,就是根据键值进行查找和重新赋值。
let obj = {
cluster: {
name: 'zhangsan',
sex: 'male',
study:{
math: 98
}
},
lesson: ['english', 'history', 'pe'],
afterschool: 'playing'
}
let {cluster:{name:look},lesson:[first],afterschool} = obj
console.log(look, first, afterschool)