JavaScript网页编程之继承和函数进阶

287 阅读14分钟

继承

回顾:面向对象的特点

  • 继承
  • 封装
  • 多态(抽象)

对象之间的继承

  • 继承指的是:类型和类型之间

对象拷贝

  • for...in : 父对象的属性拷贝给子对象
var laoli = {
    name:"laoli",
    age:55,
    house:"别墅"
};
var xiaoli = {
    name:"xiaoli",
    age:23
}
// 将父对象的属性拷贝给子对象
// 封装一个对象之间继承的函数
function extend(parent,child){
    for(var k in parent){
        // 子级有的属性不需要继承,例如:子对象自己的名称
        if(child[k]){
            continue;
        }
        child[k] = parent[k];
    }
}
// 调用封装的继承函数
extend(laoli,xiaoli);
console.log(xiaoli); // 继承成功

构造函数的属性继承

  • 借用构造函数(使用call方法)
function Person(name,age,sex){
    this.name = name;
    this.age = age;
    this.sex = sex;

function Student(name,age,sex,score){
    // 调用父级的构造函数
    Person.call(this,name,age,sex);
    this.score = score;
}
// 创建一个实例对象
var stu = new Student("ww",12,"女",99);
console.dir(stu);
console.log(stu.name);

构造函数的原型方法继承

  • 拷贝继承(for——in)
function Person(name,age,sex){
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
    // 父级的原型方法
    Person.prototype.sayHi = function (){
        console.log("Hello");
    }
    function Student(name,age,sex,score){
        // 调用父级的构造函数
        Person.call(this,name,age,sex);
        this.score = score;
    }
    // 使用拷贝方法遍历的时候,要注意constructor
    // 子类原型对象 循环 拷贝 父类原型对象
    for(var k in Person.prototype){
        if(k === "constructor"){
            continue;
        }
        Student.prototype[k] = Person.prototype[k];
        }
        // 创建一个学生实例对象 
        // 解决了之前原型继承带来的问题:new Person()传入的参数是一次性的
        var stu = new Student("ff",23,"女",67);
        stu.sayHi();
  • 原型继承
function Person(name,age,sex){
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
    // 父级的原型方法
    Person.prototype.sayHi = function (){
        console.log("Hello");
    }
    function Student(name,age,sex,score){
        // 调用父级的构造函数
        Person.call(this,name,age,sex);
        this.score = score;
    }
    // 使用拷贝方法遍历的时候,要注意constructor
    // 子类原型对象 循环 拷贝 父类原型对象
    // 创建一个学生实例对象 
    // 解决了之前原型继承带来的问题:new Person(传入的参数是一次性的
    
    // 方法2:原型继承
    Student.prototype = new Person();
    Student.prototype.constructor = Student;
    var stu = new Student("ff",23,"女",67);
    stu.sayHi();

组合继承

  • 属性:在构造函数内部继承
  • 方法:通过原型继承
function Person(name,age,sex){
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
    // 父级的原型方法
    Person.prototype.sayHi = function (){
        console.log("Hello");
    }
    function Student(name,age,sex,score){
        // 属性继承:调用父级的构造函数
        Person.call(this,name,age,sex);
        this.score = score;
    }
    // 使用拷贝方法遍历的时候,要注意constructor
    // 子类原型对象 循环 拷贝 父类原型对象
    // 创建一个学生实例对象 
    // 解决了之前原型继承带来的问题:new Person(传入的参数是一次性的
    
    // 方法继承:原型继承
    Student.prototype = new Person();
    Student.prototype.constructor = Student;
    var stu = new Student("ff",23,"女",67);
    stu.sayHi();

函数定义的方式

  • 推荐:函数声明的方式来定义函数

函数声明

// 函数声明
function fn (){
    console.log("fn");
}

函数表达式

// 函数表达式
var fun = function (){
    console.log("fun");
}

new Function

  • 不推荐:它的性能比较低,js在执行的时候,先把整个字符串解析,然后分析语句结束的位置,在执行。
// new function: 参数为字符串形式,语句书写方式与平时的相同
var func = new Function('var a = "1";console.log(a)');

注意(存在兼容问题):

  • 如果将fn()写在最后面,高版本会输出true,因为执行到里就会发现,是个函数声明而不是变量,修改后,根据if条判断输出。
  • 低版本,不论if是true还是false,都只会输出false,因其只认最后一次定义的函数。
  • 代码展示
/ 在高版本浏览器中,报错
// 即:高版本浏览器中的是变量声明提升
// IE8低版本浏览器中,进行的是函数声明提升
fn();
// if语句中的函数提升
if(true){
    function fn (){
        console.log("true");
    }
}else {
    function fn (){
        console.log("false");
    }
}

函数声明与函数表达式的区别

  • 函数有无名字
    • 函数声明:必须有名字
    • 函数表达式:可以没有名字,例如匿名函数
  • 预解析阶段
    • 函数声明:会函数提升,在预解析阶段就已创建,声明前后都可以调用。
    • 函数表达式:类似于变量赋值。没有函数提升,在执行阶段创建,必须在表达式执行之后才可以调用。

函数也是对象

  • 函数本身也是一种对象,可以通过构造函数方法定义,并调用属性和方法。
// new function: 参数为字符串形式,语句书写方式与平时的相同
var func = new Function('var a = "1";console.log(a)');

函数的调用和this

函数的调用方法

普通函数

// 普通函数
function fn (){
    console.log("Hi");
}
// 普通调用方法
fn();

构造函数

// 构造函数
    function Person(name){
        this.name = name;
    }
    // 创建一个实例对象来调用
    var p = new Person("zs");s

对象方法

// 对象方法
    var o = {
        sayName : function (){
            console.log("wm");
        }
    }
    // 调用方法
    o.sayName();

事件函数

// 事件函数:当触发该事件时就会调用
    document.onclick = function (){
        alert("你好");
    }

定时器、延时器的函数

// 定时器、延时器的方法
    setInterval(function (){
        console.log("wa");
    },1000);
    setTimeout(function (){
        console.log("timeout");
    },1000);

函数内this指向的不同场景

  • 普通函数:内部的this默认指向window
  • 构造函数:内部的this指向的是将来创建的实例对象
    • 原型方法中this也是实例对象
  • 对象方法:内部的this默认指向的是调用的对象自己
  • 事件函数:指向触发事件函数的事件源
  • 定时器、延时器:默认内部的this指向的是window
  • 注意:this的指向不是一开始就定死的,而是需要去查看上下文
    • 如下代码:
      // 普通函数
            function fn (){
                console.log(this);
            }
      
            // 对象方法
            var o = {
                sayName : function (){
                    console.log("wm");
                },
                fun : fn
            }
            // 此时的this指向的是o对象
            // 普通函数中的this不再是window
        o.fun();
      

call、apply、bind方法

  • 函数内部在调用时,this有自己默认的指向。但是可以通过下面三个方法来改变this的指向。

call方法

  • 功能:
    • 指定函数内部的this
    • 可以执行函数并传参
  • call() 方法调用一个函数, 其具有一个指定的 this值和分别地提供的参数(参数的列表)。
  • 注意:该方法的作用和 apply() 方法类似,只有一个区别,就是 call() 方法接受的是若干个参数的列表,而 apply() 方法接受的是一个包含多个参数的数组。
  • 语法:
    fun.call(this,arg1,arg2);
    
    • this:在 fun 函数运行时指定的 this 值
    • 如果指定了 null 或者 undefined 则内部 this 指向 window。
    • 展示代码
      function fun (a,b){
              console.log(this);
              console.log(a + b);
          }
          var o = {
              name : "zs"
          }
          // call方法:改变普通函数内部的this指向
          // 功能:1. 改变函数内部的this指向
          //       2. 执行函数并传参
          // 返回值:函数自己的返回值
          fun.call(o,1,2); // 3
          console.log(o);
      

apply方法

  • apply() 方法调用一个函数, 第一个参数是一个指定的 this 值,第二个参数是以一个数组(或类似数组的对象)形式提供的参数。
  • 注意:该方法的作用和 call() 方法类似,只有一个区别,就是 call() 方法接受的是若干个参数的列表,而 apply() 方法接受的是一个包含多个参数的数组。
  • 语法:
fun.apply(thisArg, [argsArray])
  • 演示代码
function fun (a,b){
    console.log(this);
    console.log(a + b);
}
var o = {
    name : "zs"
}

// apply方法
// 功能:1.指定函数的内部this
//       2. 可以执行函数并传参
fun.apply(o,[4,8]); // 12

bind方法

  • bind() 函数会创建一个新函数(称为绑定函数),新函数与被调函数(绑定函数的目标函数)具有相同的函数体(在 ECMAScript 5 规范中内置的 call 属性)。
  • 参数
    • thisArg:当绑定函数被调用时,该参数会作为原函数运行时的 this 指向。当使用 new 操作符调用绑定函数时,该参数无效。
    • arg1, arg2, ...:当绑定函数被调用时,这些参数将置于实参之前传递给被绑定的方法。
  • 返回值:返回由指定的 this 值和初始化参数改造的原函数拷贝。
  • 演示代码
function fun (a,b){
            console.log(this);
            console.log(a + b);
        }
        var o = {
            name : "zs"
        }
        // bind函数
        // 功能:1.指定函数的this
        //       2.不能执行函数,但是可以传参
        var fn = fun.bind(o,1,34); // 绑定函数
        // bind调用之后,没有执行
        /* 返回值 -> 输出结果:
           返回一个新的制定了this的函数,也可以叫绑定函数
            ƒ fun (a,b){
            console.log(this);
            console.log(a + b);
            }
        */
        console.log(fn);
        fn(); // 函数执行(不用传参) ,输出35
        // 如果函数执行的时候,传参。
        fn(6,8);  // 输出结果仍是35 
  • 分开传递参数,代码演示
function fun (a,b,c,d){
    console.log(this);
    console.log(a + b + c + d);
}
var o = {
    name : "zs"
}
// bind函数
// 功能:1.指定函数的this
//       2.不能执行函数,但是可以传参
var fn = fun.bind(o,1,34);
console.log(fn);
fn(5,3); // 输出 :43

call、apply、bind方法的应用

call方法的应用

  • call的应用,借用对象没有的那些方法
  • 演示代码
// call方法的应用
    var arr = [1,5,6,7];
    // arr.push(8);
    // console.log(arr);
    // 但是像类数组对象就没有push方法
    var o = {
      0:1,
      1:5,
      2:7,
      length:3  
    };
    // 如何才能实现类数组对象的push
    // 只要找到push函数,将其内部的this改为我们的数组对象即可
    Array.prototype.push.call(o,7);
    console.log(o); // length自动变为4

apply方法的应用

  • 数组中没有的一些方法,可以通过apply方法把自己展开,再进行下一步的操作。
  • 演示代码
// apply方法应用
    // 定义一个数组,利用apply方法,可以将它拆开进行操作
    var arr = [1,8,4,7];
    // 想借用一些现在内置在js中的方法
    // console.log(Math.max(1,4,6,7,9,3));
    console.log(Math.max(arr)); // NaN
    // 因此可以借用apply方法,其会将参数进行拆分
    var maxX = Math.max.apply(Math,arr);
    console.log(maxX); // 8
  • 注意:Math.max.apply(Math,arr)中apply方法的第一个参数,填null或者Math都可以,null表示不改变原来的指针指向。
  • 演示代码2
var arr = [1,8,4,7];
console.log(1,5,7,34); // 1,5,7,34
// 此时输出的是一个数组
console.log(arr); // [1,8,4,7]
// 可以通过调用apply方法,将数组展开
console.log.apply(null,arr); // 1 8 4 7

bind方法的应用

  • 适用于需要绑定函数,但是不立即执行。
  • 演示代码1
// bind方法应用
var o = {
    name : "zs",
    age : 13,
    s : function (){
        // 定时器内部默认的this是window,而window内部根本没有ag性,就返回undefined
        setInterval(function (){
            console.log(this.age);
        }.bind(this),1000);
    }
};
// 当我们利用bind方法绑定o对象,就可以输出o.age了
  o.s(); // 13 ,此时的this = o
  • 演示代码2
document.onclick = function (){
    console.log(this);
} // 输出:this = #document
// // 现在我想改变下事件函数内部的this,但是还是不要其立即执行
document.onclick = function (){
    console.log(this);
}.bind(o); 

总结

call和apply特性是一样的

  • 相同点:
    • 1.都是用来调用函数,而且是立即调用
    • 2.可以在调用函数的同时,通过第一个参数指定函数内部 this 的指向.
  • 不同点:
    • call 调用的时候,参数必须以参数列表的形式进行传递,也就是以逗号分隔的方式依次传递.
    • apply 调用的时候,参数必须是一个数组,然后在执行的时候,会将数组内部的元素一个一个拿出来,与形参一一对应进行传递.
  • 注意: 如果第一个参数指定了 null 或者 undefined 则内部 this 指向 window
var arr = [5,9,3,7];
// 这四种语句输出的结果都一样
// 输出结果: 5 9 3 7
console.log.apply(null,arr);
console.log.apply(window,arr);
console.log.apply(console,arr);
console.log.apply(window.console,arr);

bind方法

  • 可以用来指定内部 this 的指向,然后生成一个改变了 this 指向的新的函数
  • 不同点: 和 call、apply 最大的区别是:bind 不会调用
  • bind 支持传递参数,它的传参方式比较特殊,一共有两个位置可以传递
    • 1.在 bind 的同时,以参数列表的形式进行传递
    • 2.在调用的时候,以参数列表的形式进行传递
  • 那么 bind 的时候传递的参数为准呢还是以调用的时候传递的参数为准?
    • 两者合并:bind 的时候传递的参数和调用的时候传递的参数会合并到一起,传递到函数内部.

函数的其他成员

arguments 实参集合

  • 实际应用中,会在函数内部直接使用一个arguments的关键字
  • 存储的是函数在调用时,传入的所有实参组成的一个类数组对象.
  • 灵活使用arguments类数组对象
    • 因为传的参数是不固定的,没有办法通过abcd来确定,就可以使用这种方法来制作.
    • 演示代码 js function max(){ var nowMax = arguments[0]; for(var i = 0;i < arguments.length; i++){ if(arguments[i] > nowMax){ nowMax = arguments[i]; } } return nowMax; }

arguments.callee 函数本身,arguments的一个属性

arguments.length 实参的个数

fn.caller 函数的调用者,如果再全局调用,返回的调用者为null

  • 函数在哪个作用域调用,caller就是谁

fn.length 形参的个数

fn.name 函数的名称

  • 演示代码
function fn(a,b){
    // 实参集合
    console.log(fn.arguments); // Arguments(2)[1,2,3,4]
    // 函数的调用者,如果再全局调用,返回的调用者为null
    console.log(fn.caller); // null
    // 形参的个数
    console.log(fn.length); // 2
    // 函数名
    console.log(fn.name); // fn
    console.log(fn.arguments.callee); // 函数本身
}
fn(1,2,3,4);

高阶函数

函数可以作为参数

// 高阶函数
// 定时器\延时器也是高阶函数
// 1.函数作为另一个函数的参数
// 举例:吃完饭之后看电影
function eat(movie){
    console.log("吃完饭");
    movie();
}
function movie (){
    console.log("看电影");
}
eat(movie);

函数可以作为返回值

// 2.函数作为另一个函数的返回值
function outer(n){
    return function inner(m){
        console.log(m + n);
    }
}
var fun = outer(100);
fun(8); // 108
fun(18); // 118

函数闭包

回顾作用域、作用域链、预解析

  • 温习
    • 作用域链: 一个变量在使用时,会优先从自己所在层作用域查找变量,如果当前层没有变量定义会按照顺序从本层往外依次查找,直到找到第一个变量定义(如果一直没有找到,那么就会在控制台报错 is no defined.)
    • 遮蔽效应:最近的会遮盖较远的
  • 全局作用域
  • 函数作用域
  • 没有块级作用域
  • 内层作用域可以访问外层作用域,反之不行

什么是闭包

  • 闭包让你可以在一个内层函数中访问其外层函数的作用域。在js中,每创建一个函数,闭包就会在函数创建的同时被创建出来.

闭包

  • 函数定义时天生就能记住自己生成的作用域环境和函数自己,将它们形成一个密闭的环境,这就是闭包。不论函数以任何方式在任何地方进行调用,都会回到自己定义时的密闭环境进行执行。
  • 如下图

观察闭包

  • 从广义上来说,定义在全局的函数也是一个闭包,只是我们没办法将这样的函数拿到更外面的作用域进行调用,从而观察闭包的特点。
  • 闭包是天生存在的,不需要额外的结构,但是我们为了方便观察闭包的特点,需要利用一些特殊结构将一个父函数内部的子函数拿到父函数外部进行调用,从而观察闭包的存在。
  • 演示代码
// 体会闭包
// 将一个内部函数拿到父函数的外面,观察是否还能调用父内部的变量
function outer (){
    var a = 10;
    // 将inner作为返回值 -> 高阶函数
    return function inner (){
        console.log(a);
    };
}
// 在outer函数的外面,是不能直接访问a变量
console.log(a);
var out = outer();
// 调用函数,输出结果10
// 在全局调用out,按道理应该查找全局的a变量
// 但是输出的真正结果是10,来自outer函数内部的变量
out();

闭包的用途

  • 可以在函数外部读取函数内部成员
  • 让函数内成员始终存活在内存中
function outer(n){
    return function inner(m){
        console.log(n + m);
    }
}
var out = outer(100);
// 可以重复多次调用
out(4); // 104
out(7); // 107
out(45); // 145

闭包的问题

  • 解决方法:用自调用函数封闭作用域,减小作用域范围
// 闭包的问题
var arr = [];
for(var i = 0;i <= 10; i++){
    // 解决方法:
    // 用自调用函数封闭作用域,减小作用域范围
    (function (i){
        arr[i] = function (){
        console.log(i);
    }
    })(i);          
}
// 目的:调用数组对应的项,输出它的对应下标
// 但是却都输出了11
// 也就是说:现在的i是全局变量,i = 11
// 添加自调用函数后,输出变为: 0 1
arr[0]();
arr[1]();

补充

  • 控制台打断点方式(并且可以通过scope查看作用域)