AI智能
改变未来

js js高级笔记

js高级

  • js变量提升和函数提升(预解析)

    如果通过var声明变量:

  • 如果一个声明的变量在函数体内,那么它的作用域就是函数内部。如果是在全局环境下声明的,那么它的作用域就是全局
  • ES6之前没有块级作用域,只有函数作用域和全局作用域,ES6中提供了块级作用域
  • 函数内部的变量会被提升到函数的头部,函数在解析执行的时候,先进行变量声明处理,然后在运行函数内部的代码
  • 函数内部如果没有变量声明,那么该变量就是全局变量(如果函数内部没有var,该变量就是全局变量)
  • 变量和赋值语句一起书写,在js引擎解析时,会将其拆成声明和赋值两部分,声明置顶,赋值保留在原来位置
  • 变量重复声明不会出错,后面的会覆盖前面的
  • 局部变量会随函数调用后释放
  • 函数提升所指的形式:function fn(){…} 必须是函数声明的形式,不能是函数表达式的形式
  • 函数提升的方式:将function fn(){…}整个函数声明代码块提升到当前作用域的顶部,原先位置以不存在该代码
  • 作用域和作用域链

      作用域:

      用途: 一个变量的可用范围

    • 本质: 一个保存变量的对象(window)
    • 为什么: 避免不同范围的变量互相干扰
    • 分类:全局作用域(window)保存全局变量,可重复使用,随处可用
    • 缺点: 全局污染
  • 函数作用域
      保存局部变量,不会污染全局
    • 缺点: 不可重用,仅函数内使用
    • 包含:
      1.在函数内var的
      2.参数变量也是局部变量
  • 作用域链

      定义: 由多级作用域对象,逐级引用形成的链式结构
    • 作用:存储所有变量(局部变量和全局变量)
    • 控制变量的使用顺序,先局部,后全局
  • 闭包

      定义: 既重用一个变量,又保护对象不被污染的一种机制
    • 何时使用: 只要重用一个变量,又保护对象不被污染时
    • 如何使用:
      1.用外层函数包裹,要保护的变量和使用变量的内层函数
      2.外层函数返回内层函数
      3.调用者,调用外层函数来获得内层函数对象
    • 缺点:闭包比普通函数占用更多内存
    • 解决: 闭包函数 = null
  • ps:闭包必须通过函数返回实现
  • 产生闭包的条件:
      函数嵌套
    • 内部函数引用了外部函数的数据(变量/函数)
  • 作用:
      使用函数内部的变量在函数执行完后,仍然存活在内存中(延长了局部变量的周期)
    • 让函数外部可以操作(读写)到函数内部的数据(变量/函数)
  • 面向对象介绍

      什么是对象?
      对象到底是什么,我们可以从两个层次来理解。
      (1)对象是单个事物的抽象
      (2)对象是一个容器,封装了属性(property)和方法(method)
      属性是对象的状态,方法是对象的行为(完成某种任务)

      属性: 其实就是保存在对象中的普通方法对象的属性如果是复杂命名(xxx-xxx),或者不确定的时候,用对象[属性名]表示

  • 方法: 其实就是保存在对象中的普通函数
      对象中的方法可以简写为 方法名(){},不用写成方法名:function (){}
      PS:每个对象都是基于一个引用类型创建的,这些类型可以是系统内置的原生类型,也可以是开发人员自定义的类型
  • 什么是面向对象?
    面向对象编程 —— Object Oriented Programming,简称 OOP,是一种编程开发思想。
    我们在使用对象时,我们只关注对象的功能,不关注对象的内部细节
    在面向对象开发思想中,每一个对象都是功能中心,具有明确分工,可以完成接受信息、处理数据、发出信息等任务。
    因此,面向对象编程具有灵活、代码可复用、高度模块化等特点,容易维护和开发

  • 面向对象与面向过程:

      面向过程就是亲力亲为,事无巨细,面面俱到
    • 面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,
      而是为了描叙某个事物在整个解决问题的步骤中的行为
    • 面向对象将执行者转变成指挥者
    • 面向对象不是面向过程的替代,而是面向过程的封装
  • 面向对象的特性:

      封装性
      对于一些功能相同或者相似的代码,我们可以放到一个函数中去,
      多次用到此功能时,我们只需要调用即可,无需多次重写
    • 继承性:
      类与类之间的关系,子类可以使用父类的所有功能,并且对这些功能进行扩展
    • 多态性:
      相同行为,不同的实现
    • 抽象性:
      对象本身的状态与行为,以及对象之间的关系,都是抽象的结果;
      把同类的对象共有的属性或方法抽出封装成单独的对象,在用到的时候给相应的对象使用
  • 创建对象的方式:

      通过Object构造函数
    • 对象字面量
    • 工厂函数
    • 构造函数
    • 如果对象自己的方法访问自己的属性,用this来访问,this就代表当前对象
  • 构造函数

      创建多个结构相同的对象
    • 定义同一类型的所有对象的统一结构
    • 为对象构造想要的结构
    • 构造函数也是函数,函数名首字母必须大写
  • 通过new的方式创建实例发生了什么:

      创建一个新对象;
    • 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象)
    • 执行构造函数中的代码(为这个新对象添加属性)
    • 返回新对象
  • 工厂函数与构造函数的区别

      没有显示的创建对象
    • 直接将属性和方法赋给了this对象
    • 没有 return 语句
    • 函数名首字母使用的是大写的
    • 创建构造函数的实例,则必须使用new操作符
  • 判断一个对象的类型

      typeof: 返回所有基本类型 object function
    • instanceof: 判断一个变量是否是一个构造函数的实例
    • console.log(s1.constructor === Student) // => true
      console.log(s2.constructor === Student) // => true
      console.log(s1.constructor === s2.constructor) // => true
      console.log(s1 instanceof Student) // => true
      console.log(s2 instanceof Student) // => true
  • 构造函数和实例对象的关系

      构造函数是根据具体的事物抽象出来的抽象模板
    • 实例对象是根据抽象的构造函数模板得到的具体实例对象
    • 每一个实例对象都具有一个__proto__属性,指向创建该实例的构造函数的prototype
    • instanceof可以判断实例和构造函数的关系
  • 构造函数、原型对象、实例的关系

      每个构造函数都有一个原型对象
    • 原型对象都包含一个指向构造函数的指针
    • 实例都包含一个指向原型对象的内部指针
  • 思考:假设我所有的实例都有一个type属性和sayHi方法, 我应该怎么做最好?

  • 原型

      prototype和__proto__每一个构造函数都有一个prototype属性,指向另一个对象。
    • 这个对象的所有属性和方法,都会被构造函数的所拥有
    • 可以通过prototype来扩展构造函数的公共属性和方法
    • 任何函数都具有一个prototype属性,该属性是一个对象
    • 构造函数的prototype对象默认都有一个constructor属性,指向prototype对象所在函数
      function F () {}
      console.log(F.prototype.constructor === F) // true
    • 通过构造函数得到的实例对象内部会包含一个指向构造函数的 prototype 对象的指针 proto
      var instance = new F()
      console.log(instance.proto === F.prototype) // => true
    • 实例对象可以直接访问原型对象成员
  • 总结
      任何函数都具有一个 prototype 属性,该属性是一个对象
    • 构造函数的 prototype 对象默认都有一个 constructor 属性,指向 prototype 对象所在函数
    • 通过构造函数得到的实例对象内部会包含一个指向构造函数的 prototype 对象的指针 proto
    • 所有实例都直接或间接继承了原型对象的成员
  • 原型链

      每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性

      搜索首先从对象实例本身开始

    • 如果在实例中找到了具有给定名字的属性,则返回该属性的值
    • 如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性
    • 如果在原型对象中找到了这个属性,则返回该属性的值
  • 也就是说,在我们调用 student1.sayHi() 的时候,会先后执行两次搜索

      首先,解析器会问:“实例person1有sayName 属性吗?” 答:“没有”
    • 然后,它继续搜索,再问:“person1的原型有sayName属性吗?” 答:“有”
    • 于是,它就读取那个保存在原型对象中的函数
    • 当我们调用 student2.sayHi()时,将会重现相同的搜索过程,得到相同的结果
    • 这正是多个对象实例共享原型所保存的属性和方法的基本原理
  • 原型模式的执行流程

      先查找构造函数实例里的属性或方法,如果有,立刻返回
    • 如果构造函数实例里没有,则去它的原型对象里找,如果有,就返回
  • 总结

      先在自己身上找,找到即返回
    • 自己身上找不到,则沿着原型链向上查找,找到即返回
    • 如果一直到原型链的末端还没有找到,则返回undefined
    • 一直会找到原型链的顶端 null
  • 更简单的原型语法
    function Person (name, age) {
    this.name = name
    this.age = age
    }
    Person.prototype = {
    type: ‘human’,
    sayHello: function () {
    console.log(‘我叫’ + this.name + ‘,我今年’ + this.age + ‘岁了’)
    }
    }

      在该示例中,我们将 Person.prototype 重置到了一个新的对象。
      这样做的好处就是为 Person.prototype 添加成员简单了
      但是也会带来一个问题,那就是原型对象丢失了 constructor 成员

    • 所以,我们为了保持 constructor 的指向正确,建议的写法是
      function Person (name, age) {
      this.name = name
      this.age = age
      }
      Person.prototype = {
      constructor: Person, // => 手动将 constructor 指向正确的构造函数
      type: ‘human’,
      sayHello: function () {
      console.log(‘我叫’ + this.name + ‘,我今年’ + this.age + ‘岁了’)
      }
      }

    • 原型模式的缺点

      原型中所有属性是被很多实例共享的,共享对于函数非常合适,
      对于包含基本值的属性也还可以。但如果属性包含引用类型,就存在一定的问题
      function Box() {};
      Box.prototype = {
      constructor : Box,//指向构造函数
      name : ‘Lee’,
      age : 100,
      family : [‘父亲’, ‘母亲’], //添加了一个数组属性
      run : function () {
      return this.name + this.age + this.family;
      }
      }
      var box1 = new Box()
      box1.family.push(‘弟弟’) //在实例中添加’哥哥‘
      alert(box1.run())
      var box2 = new Box()
      alert(box2.run()) //共享带来的麻烦,也有’弟弟’了

    • 不能传参
  • 组合模式

      为了解决构造传参和共享问题,可以组合构造函数+ 原型模式
      function Box(name, age) { //不共享的使用构造函数
      this.name = name
      this.age = age
      this.family = [‘父亲’, ‘母亲’, ‘妹妹’]
      }
      Box.prototype = { //共享的使用原型模式
      constructor : Box,
      run : function () {
      return this.name + this.age + this.family
      }
      }

      var b1 = new Box(\'lp\', 20)var b2 = new Box(\'wq\', 22)b1.family.push(\'哥哥\')console.log(b1.run())console.log(b2.run())console.log(b1.family === b2.family)
  • 原生对象的原型

      所有函数都有 prototype 属性对象Object.prototype
    • Function.prototype
    • Array.prototype
    • String.prototype
    • Number.prototype
    • Date.prototype
  • 原型对象使用建议

      私有成员(一般就是非函数成员)放到构造函数中
    • 共享成员(一般就是函数)放到原型对象中
    • 如果重置了 prototype 记得修正 constructor 的指向
  • this指向

      this关键字表示对某个对象的引用,可以把他理解为一个引用类型的变量,但它的值是系统确定的,也就是this无法赋值
    • 全局中调用:
      在全局执行环境中使用this,表示global对象,在浏览器中就是window对象
      var box = 2;
      alert(this.box); //全局,代表 window
    • 函数调用:
      在函数执行环境中使用this,情况比较复杂,
      如果函数没有明显的作为非window对象的属性,而只是定义了函数,不管这个函数是否定义在另一个函数中,
      这个函数中的this仍然表示window对象。如果函数显式地作为一个非window对象的属性,那么函数中的this就表示这个对象
      function greeting(){
      this.name=“jack”;
      alert(\”hello \”+this.name);
      }
    • 当通过new运算符来调用函数时,函数被当作一个构造函数,this执行构造函数创建出的对象对象的方法调用
      obj.fn();//obj对象的fn()方法中的this指向obj
    • 调用构造函数
      var dog=new Dog();//构造函数内的this指向新创建的实例对象
  • 使用面向对象编程

      创建一个元素,并添加样式

      面向过程的编程方式
      var div = document.createElement(“div”);
      div.style.width = “500px”;
      div.style.height = “300px”;
      div.style.backgroundColor = \”#f37f38”;
      document.body.appendChild(div);

    • 面向对象的编程方式
      function CreateElement(tag) {
      this.createDOM = document.createElement(tag)
      }
      CreateElement.prototype = {
      constructor: CreateElement,
      appendTo: function (node) {
      node.appendChild(this.createDOM);
      return this
      },
      css: function (data) {
      for (var key in data) {
      this.createDOM.style[key] = data[key]
      }
      return this
      }
      }
      var pElement = new CreateElement(“div”);
      pElement.css({
      “width”: “300px”,
      “height”: “300px”,
      “backgroundColor”: “#30d05f”
      }).appendTo(document.body)
  • 继承

      原型继承(将父类的实例作为子类的原型)
      JS主要时通过原型链来实现继承,现在让一个对象的原型等于另一个对象的实例,
      那么这个对象就继承了另一个对象的所有属性和方法,而这个对象的所有实例都共享这个对象的原型里面的所有属性及方法
      function Person(name,age) {
      this.name =name || ‘Tom’
      this.age = age || 20
      }

      Person.prototype.tst1=function(){
      alert(‘哈哈哈’)
      }

      function Student(sex) {
      this.sex = sex || ‘男’
      }
      Student.prototype=new Person(‘xiao’,18)
      var stu1=new Student(‘男’)
      console.log(stu1.name)
      console.log(stu1.age)
      console.log(stu1.sex)
      stu1.tst1()

    • 特点:
      非常纯粹的继承关系,实例是子类的实例,也是父类的实例
      父类新增原型方法/原型属性,子类都能访问到
      简单,易于实现

    • 缺点:
      要想为子类新增属性和方法,必须要在new Animal()这样的语句之后执行,不能放到构造器中
      无法实现多继承
      来自原型对象的所有属性被所有实例共享(来自原型对象的引用属性是所有实例共享的)
      创建子类实例时,无法向父类构造函数传参

    • 构造继承(使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型))

      call apply继承

      bind
      改变this的指向 并且返回一个新的函数(不调用函数)

    • call
      改变this的指向 直接调用函数 第一个参数(要改变this指向的对象) 第二个参数(参数列表)

    • apply
      改变this的指向 直接调用函数 第一个参数(要改变this指向的对象) 第二个参数(参数数组)

    • call方法继承
      function Person(name,age) {
      this.name =name || ‘Tom’
      this.age = age || 20
      }
      function Student(name,age,sex){
      Person.call(this,name,age)
      this.sex = sex || ‘男’
      }
      var stu=new Student(‘lucy’,18,‘女’)
      alert(stu.name)

    • apply 方法继承
      function Person(name,age) {
      this.name =name || ‘Tom’
      this.age = age || 20
      }
      function Student(name,age,sex) {
      Person.apply(this,[name,age])
      this.sex=sex
      }
      特点:
      子类实例共享父类引用属性的问题
      创建子类实例时,可以向父类传递参数
      可以实现多继承(call多个父类对象)
      缺点:
      实例并不是父类的实例,只是子类的实例
      只能继承父类的实例属性和方法,不能继承原型属性/方法
      无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

  • 实例继承(为父类实例添加新特性,作为子类实例返回)
    function Cat(name){
    var instance = new Animal()
    instance.name = name || ‘Tom’
    return instance
    }

    var cat = new Cat()
    console.log(cat.name)
    console.log(cat.sleep())
    console.log(cat instanceof Animal) // true
    console.log(cat instanceof Cat) // false

      特点:
      不限制调用方式,不管是new 子类()还是子类(),返回的对象具有相同的效果
    • 缺点:
      实例是父类的实例,不是子类的实例
      不支持多继承
  • 拷贝继承
    function Cat(name){
    var animal = new Animal()
    for(var p in animal){
    Cat.prototype[p] = animal[p]
    }
    Cat.prototype.name = name || ‘Tom’
    }

    var cat = new Cat()
    console.log(cat.name)
    console.log(cat.sleep())
    console.log(cat instanceof Animal) // false
    console.log(cat instanceof Cat) // true

      特点:
      支持多继承
    • 缺点:
      效率较低,内存占用高(因为要拷贝父类的属性)
      无法获取父类不可枚举的方法(不可枚举方法,不能使用for in 访问到)
  • 组合继承(通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用)
    function Cat(name){
    Animal.call(this)
    this.name = name || ‘Tom’
    }
    Cat.prototype = new Animal()
    组合继承也是需要修复构造函数指向的
    Cat.prototype.constructor = Cat

    var cat = new Cat()
    console.log(cat.name)
    console.log(cat.sleep())
    console.log(cat instanceof Animal) // true
    console.log(cat instanceof Cat) // true

      特点:
      可以继承实例属性/方法,也可以继承原型属性/方法
      既是子类的实例,也是父类的实例
      不存在引用属性共享问题
      可传参
      函数可复用
    • 缺点:
      调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了
  • 寄生组合继承(通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点)
    function Cat(name){
    Animal.call(this)
    this.name = name || ‘Tom’
    }
    (function(){
    // 创建一个没有实例方法的类
    var Super = function(){}
    Super.prototype = Animal.prototype
    //将实例作为子类的原型
    Cat.prototype = new Super()
    Cat.prototype.constructor = Cat
    })()

    var cat = new Cat()
    console.log(cat.name)
    console.log(cat.sleep())
    console.log(cat instanceof Animal) // true
    console.log(cat instanceof Cat) //true

    PS:最好的实现继承的方式

  • call、apply其他使用场景

      数组最大最小值
      var arr = [1, 33, 43, 765, 23, 99]
      var min = Math.min.apply(null, arr)
      var max = Math.max.apply(null, arr)

    call apply第一个参数传null或者undefined 代表window

    • 数组追加
      var array1 = [12 , “foo” , {name:“Joe”} , -2458]
      var array2 = [“Doe” , 555 , 100]
      Array.prototype.push.apply(array1, array2)
      function的apply方法的第二个参数是一个数组集合,
      在调用的时候,他需要的不是一个数组,但是为什么他给我一个数组我仍然可以将数组解析为一个一个的参数
      这个就是apply的一个巧妙的用处,可以将一个数组默认的转换为一个参数列表([param1,param2,param3] 转换为 param1,param2,param3)
      所以利用变个特性,我们就实现了上面的功能。

    • 验证是否是数组
      functionisArray(obj){
      return Object.prototype.toString.call(obj) === ‘[object Array]’
      }

    • 把类数组转换为数组
      Array.prototype.slice.call(类数组)

  • arguments对象
    当一个函数要被执行时,系统会在执行函数体代码前做一些初始化的工作,
    其中之一就是为函数对象创建一个arguments对象属性。Arguments 对象只能使用在函数体中,
    并用来管理函数的实际参数。arguments 对象有一个Length属性。表示函数被调用时实际传递的参数个数
    在函数代码中,使用特殊对象 arguments,开发者无需明确指出参数名,就能访问它们
    – 检测参数个数
    function howManyArgs() {
    alert(arguments.length)
    }
    howManyArgs(“string”, 45)
    howManyArgs()
    howManyArgs(12)

    - 模拟函数重载- 用 arguments 对象判断传递给函数的参数个数,即可模拟函数重载function doAdd() {if(arguments.length == 1) {alert(arguments[0] + 5)} else if(arguments.length == 2) {alert(arguments[0] + arguments[1])}}doAdd(10)	//输出 \"15\"doAdd(40, 20)	//输出 \"60\"当只有一个参数时,doAdd() 函数给参数加 5。如果有两个参数,则会把两个参数相加,返回它们的和所以,doAdd(10) 输出的是 \"15\",而 doAdd(40, 20) 输出的是 \"60\"
  • 赞(0) 打赏
    未经允许不得转载:爱站程序员基地 » js js高级笔记