JavaScript回炉重生(一)

176 阅读14分钟

对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)