js整理(6)

83 阅读11分钟

执行环境

执行环境定义了变量或函数能够访问的数据,为此,每个执行环境都关联着一个“隐藏的”变量对象,环境中定义的所有变量和函数都保存在这个变量对象中,虽然我们编写的代码无法访问这个对象,但解析器在处理数据时会在后台使用它。
全局执行环境是最外围的一个执行环境。在Web浏览器中,全局执行环境被认为是window对象,因此所有全局变量和函数都作为window对象的属性和方法。
执行环境中的所有代码执行完毕后,该环境被销毁,保存在其中的所有变量和函数也随之销毁。

作用域与作用域链

作用域可以理解为「标识符」以及「标识符所能访问到的范围」
标识符解析是沿着作用域链一级一级地搜索标识符的过程。搜索过程始终从作用域链的前端开始,然后逐级地向后回溯(最外层是window对象),直至找到标识符为止(如果找不到标识符,通常会导致错误发生)。
作用域链的作用,是确保对执行环境中变量和函数的有序访问。
作用域类型
1.全局作用域 Web浏览器下可简单理解为window对象,所有最外层的变量和函数都作为window对象的属性和方法,且它们可以在任何代码位置被访问到。全局作用域处于作用域链的最末端(最外层)。
2.局部作用域(函数作用域)
局部作用域是在函数执行时被临时创建的作用域。在局部作用域中创建的变量会在函数执行完毕后自动释放。
3.ES6块级作用域 变量的声明应该距离使用的地方越近越好,并且应最大程度地避免向外层作用域泄露 ES6的let和const实际上为JavaScript新增了块级作用域,let所声明的变量,只在let命令所在的代码块内可以访问。

箭头函数

箭头函数的写法省略了function关键字,参数列表依然写在小括号()内,函数体语句也依然写在一对花括号{}内,参数列表和花括号间使用箭头=>来衔接。
如果函数参数只有一个,()可以省略;如果函数体只有一行代码,{}也可以省略(如果省略{},return必须省略)
了解了箭头函数的写法,关于箭头函数的使用,有以下特别注意点
(1)函数体内的this对象,就是函数定义时所在的对象,而不是调用时所在的对象;
(2)不可以当作「构造函数」,也就是说,不可以使用new命令,否则会抛出一个错误;
(3)没有arguments对象; arguments对象 函数有两个内置对象:this和arguments,arguments是一个类数组对象,它包含着传入函数的所有参数。 arguments是一个类数组对象,可以使用数组的属性,不能使用数组的方法。 rest参数 ES6引入rest参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了

立即执行函数表达式

函数被包含在一对( )内,从而成为了一个表达式,通过在末尾加上另一个( )可以立即执行这个函数。小括号前的分号;是为了防止与之前的语句发生解析错误。
社区为这种常见的模式起了个术语:立即执行函数表达式(Immediately Invoked Function Expression,IIFE),它的写法是;(function() { .. })(),第一个( )将函数变成了表达式,第二个( )调用了这个函数。
IIFE的作用:通过形成函数局部作用域,避免外界直接访问变量,实现简单的模块化;

闭包

一般表现形式为父函数中嵌套一个子函数,子函数作为父函数的返回结果,使用了父函数的变量,而子函数在外部使用了,从而实现了变量私有,实现了闭包

防抖

function debounce(fn,wait){
  var timeout = null  
  return function debounce() {
      if(timeout !== null) clearTimeout(timeout)
      timeout = setTimeout(fn, wait)
    }
}

节流

function throttle(fn, delay) { 
  var prev = 0  
  console.log(1);       
  return function() {              
    var now = Date.now()               
    if (now - prev > delay) {                   
      fn()                
      prev = Date.now()             
    }         
  }       
} 

面向对象

资料:什么是面向对象?(通俗易懂) - BWH_Steven - 博客园 (cnblogs.com)
面向对象特点:封装、继承、多态性
封装是指隐藏对象的属性和实现细节,仅对外提供公共访问方式,提高了代码的复用性.
继承就是在一个已有类的基础上派生出新类(例如动物类可以派生出狗类和猫类),子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为.提高了代码的复用性,提高了代码的维护性(通过少量的修改,满足不断变化的具体要求),让类与类产生了一个关系,是多态的前提。但是缺点也很显著:让类的耦合性增强,这样某个类的改变就会影响其他和该类相关的类。
多态是同一个行为具有多个不同表现形式或形态的能力

类指的是对拥有相同特征的一类事物的抽象,而实例对象指的是拥有各自特征、能执行某些动作的个体。通过类可以创建任意多个具有相同属性和方法的对象。
创建对象
工厂模式
工厂模式是一种广为人知的设计模式,这种模式抽象了创建实例对象的过程,可以把实例对象的共性封装到一个「工厂函数」中,传入不同的参数并调用它,从而得到不同的实例对象

    // 工厂函数:批量生产相似实例对象
    function createChessPlayer(name, sex) {
    const obj = {} // 定义一个空对象
    // 为对象添加属性和方法
    obj.name = name
    obj.sex = sex
    obj.sayName = function() { // 喊出自己的名字
    console.log(this.name)
    }
    return obj // 返回对象
    }
    // 调用工厂函数,得到实例对象
    const yuan = createChessPlayer('小源', 1)
    const hong = createChessPlayer('小红', 0)
    const pang = createChessPlayer('小胖', 1)
    pang.sayName() // 输出 '小胖'

工厂函数解决了批量创建相似对象的问题。但该模式没解决对象识别的问题。 构造函数模式

// 构造函数
    function ChessPlayer(name, sex) {
    // 把属性和方法挂载到 this 上
    this.name = name
    this.sex = sex
    this.sayName = function() {
    console.log(this.name)
    }
    }
    // 通过 new 关键字,创建实例对象
    const yuan = new ChessPlayer('小源', 1)
    const hong = new ChessPlayer('小红', 0)
    const pang = new ChessPlayer('小胖', 1)
    pang.sayName() // 输出 '小胖'

构造函数与一般函数在写法上的区别:
1.首字母大写,即“大驼峰命名
2.将属性和方法挂载到this,this就指向最终创建的实例对象;
3.不需要return语句,默认返回新创建的实例对象

使用new关键字创建对象,中间发生了什么? (1)创建一个空对象;
(2)让this指向这个空对象(即将构造函数的作用域赋给新对象);
(3)执行构造函数中代码,为这个对象添加属性和方法;
(4)将对象返回;
使用构造函数的主要问题,就是方法要在每个实例上重新创建一遍,也叫「方法过载」。

原型对象prototype

(1)函数被创建时都有一个prototype属性,它是原型对象,里面包含着实例所共享的属性和方法;
(2)原型对象中有一个constructor属性,它指向构造函数;
(3)创建出来的实例对象,有一个内置的__proto__属性,指向该实例所对应的原型对象。所以通过该属性建立了实例与原型之间的关系;

原型链

JS中无论什么类型的数据,只要沿着原型往上找,都一定能找到Object,从Object再往上找,就是null了。
一个构造函数有对应的原型对象,而原型对象又可以是另一个构造函数所创建的实例(它也有它所对应的原型对象),这种关系层层递进,所构成的链式结构就是「原型链」。在一个对象中查找标识符时,会先在对象自身上找,没找到则会沿着原型链逐层向上找,直到找到为止。

this指向

全局中
全局的this取决于JS宿主环境,浏览器下全局中的this指向全局对象window
一般函数中
一般函数中的this指向「调用者」,谁调用函数,就指向谁。
对象方法中
对象方法中this的指向,依然是指向「调用者」,谁调用的该对象方法,就指向谁(注意不要错误记成固定指向该对象).
事件处理函数中
事件处理函数中的this,指向「事件源」
构造函数中
构造函数中的this,指向新创建出来的实例对象。
定时器中
时器函数setTimeout()和setInterval()中,this指向window,因为这2个定时器方法是window对象上的
bind、call与apply
每个函数都存在的call()和apply()方法,使用它们可以显式地指定this并调用。apply()与call()的唯一区别是传参数数组,而不是参数列表。
JS还提供了函数的bind()方法,使用bind()方法可以得到一个改变了this的新函数。
箭头函数中
箭头函数中的this,指向定义时所处的作用域

基本类型与引用类型

基本类型值指的是简单的数据段,引用类型值指那些可能由多个值构成的数据。
基本类型与引用类型的内存分配
(1)基本类型变量放在栈内存中
(2)引用类型的具体值放在堆内存中,对应一块地址,并将该地址存放在栈内存;
栈内存中保存着基本类型变量值,以及引用类型变量的地址(指向引用类型值的指针),它们在内存中占用着固定大小的空间;而引用类型变量真正的内容是保存在堆中的,它们的大小是不固定的(比如数组在创建后可以继续添加元素)。
变量的拷贝 (1)基本类型变量的拷贝,是进行值的拷贝
(2)引用类型变量的拷贝,是进行地址的拷贝
对象的深/浅拷贝
(1)对象浅拷贝:只拷贝对象的一层属性,如果遇到引用类型的属性,拷贝的是引用类型数据的地址。因此引用类型属性的数据会被新旧对象共享;
(2)对象深拷贝:无线层级拷贝。拷贝后,新旧对象不共享引用类型属性的值,新旧对象的变更互不影响;
3种进行对象拷贝的方式:
(1)for in 方式; 浅拷贝

        let obj={
            name:"张三",
            age:20,
            sex:0
        }
        let obj1={}
        for(let key in obj){ //也会遍历原型链上的属性
           if(obj.hasOwnProperty(key)){ // 判断属性是否是对象自身的,而不是原型链上的
             obj1[key]=obj[key]
           }
        }

(2)Object.assign()方式 浅拷贝
Object.assign()仅遍历对象自身的属性,因此省略了hasOwnProperty判断。

        let obj={
            name:"张三",
            age:20,
            sex:0,
            like:["耍","读书"]
        }
       let obj1=Object.assign({},obj)
       obj1.like.push("吃")
       console.log(obj.like);
       输出:['耍', '读书', '吃']

(3)JSON.parse()与JSON.stringify()方式 深拷贝
缺陷:无法拷贝函数类型的属性

        let obj={
            name:"张三",
            age:20,
            sex:0,
            like:["耍","读书"]
        }
        let obj1=JSON.stringify(obj)
        let obj2=JSON.parse(obj1)
        obj2.like.push("吃")
       console.log(obj.like);
       输出: ['耍', '读书']

变量类型检测补充
Array.isArray()
Array.isArray()是构造函数Array上的一个属性,它可以判断一个值的类型是否为数组:返回值为布尔值

继承

原型链继承
原型链继承:让子类的原型对象等于要继承的父类的实例
原型链继承最大的问题是:原型中引用类型数据会被各实例所共享,一旦数据改变,会影响各个实例 寄生组合式继承(ES5最理想方式)
Object.create()方法传入一个对象并创建一个新对象,传入的对象作为新创建对象的原型,即新对象的__proto__指向传入的对象

        function Father(name, age) { // 父类
            this.name = name
            this.age = age
            this.skills = ['耍', '睡觉', '吃饭']
        }
        Father.prototype.getFatherSkill = function() { // 父类方法-获取父类技能
             return this.skills
        }

        function Son(name, age) { // 子类
        // 1.调用父类构造函数,从而继承父类的属性
        Father.call(this, name, age)
        // 可以继续定义子类的属性
        this.love = '读书'
        }
        // 2.继承父类原型上的方法
        Son.prototype = Object.create(Father.prototype)
        // 3.完善子类的 constructor 指向
        Son.prototype.constructor = Son

        // 调用
        const son = new Son('小花', 5)
        son.skills.push('装死')
        console.log(son.skills) // ['耍', '睡觉', '吃饭','装死']

        const twoson = new Son('小于', 2)
        console.log(twoson.skills) // ['耍', '睡觉', '吃饭']

继承的实现主要分3步: (1)在子类中通过call()方法调用父类构造函数,从而继承父类的属性。之后可以再继续定义子类的属性; (2)使用Object.create()方法得到父类原型的副本,将该副本赋值给子类的原型,从而继承父类的方法; (3)将子类原型的constructor指向子类构造函数;