继承和函数进阶

130 阅读13分钟

笔记来源:拉勾教育 - 大前端就业集训营

文章内容:学习过程中的笔记、感悟、和经验

继承和函数进阶

对象继承

对象之间的继承

对象拷贝(for-in)

//创建父子,老李和小李,小李现在要继承老李的财产
var laoli = {
  name: '老李',
  money: 1000000000,
  house: ['商铺', '住宅'],
  tech: function () {
    console.log('厨艺');
  }
},
xiaoli = {
  name: '小李'
}
//常见一个用于继承的函数
function jicheng(father, childer) {
//使用for-in遍历对象
for (const k in father) {
  //子对象本身就有的属性不需要继承
  if (childer[k]) {
    continue;
  }
  childer[k] = father[k];
}
}
//调用继承函数
jicheng(laoli, xiaoli);
//打印下小李,发现继承成功
console.log(xiaoli);

原型继承

在类型和类型(构造函数)之间继承,利用原型对象进行继承

//常见一个人类的构造函数
    function Person(name, age, sex) {
      this.name = name;
      this.age = age;
      this.sex = sex;
    }
    //现在想创建学生的类,因为学生也属于人类,所以要继承人类的三个属性
    function Student(score) {
      this.score = score;
    }
    //利用原型对象继承人类的属性,创建一个人类实例,然后赋值给学生的原型
    Student.prototype = new Person('张三', 19, '男');
    //我们需要手动修改constructor指向,指向学生的构造函数,否则会指向人类
    Student.prototype.constructor = Student;

    //创建两个实例
    var s1 = new Student(20);
    var s2 = new Student(98);

    // 打印两个实例看一下
    console.dir(s1);
    console.dir(s2);

缺点

  • 在原型上传参只能传一次,但是原型上的属性是不能更改的,所以无论新建多少个实例原型上的属性都是相同的
  • 要手动更改自己的constructor指向

函数的call方法

函数本身的方法,函数也是一种对象,也有属性和方法

call方法本身也是一种执行函数的方法

作用

  • 更改this指向
  • 调用函数内部代码执行
//定义函数
function go(a, b) {
  console.log(this);
  console.log(a + b);
}
//调用函数查看输出的this,发现输出的是window(函数内部this默认指向window)
go(3, 4);

//创建一个变量对象
var me = {
  name: 'zhangsan'
}
//使用call调用函数
//参数1:更改this指向的对象
//其他参数;函数实参
go.call(me, 3, 4)
// 发现this输出的是me,可见call更改了函数的this指向

借用构造函数继承属性

直接调用父类型的构造函数,而不创建实例,这时候父类只能当做一个普通函数使用

注意:因为如果构造函数作为普通函数调用,那么他的this会指向window,所以要手动修改window指向

//一个人类的构造函数
function Person(name, age, sex) {
  this.name = name;
  this.age = age;
  this.sex = sex;
}

//学生构造函数
function Student(name, age, sex, num) {
  //直接调用人类构造函数把构造函数当作普通函数来调用
  //因为函数作为普通函数调用会导致this指向window,所以使用call更改this指向
  //因为学生构造函数将来是用来构造实例的,这里直接写this就可以指向这个实例
  Person.call(this, name, age, sex);
  this.num = num;
}
//老师构造函数,原理同上
function Teacher(name, age, sex, salary) {
  Person.call(this, name, age, sex);
  this.salary = salary;
}

//创建学生和老师实例
var s1 = new Student('zhangsan', 19, 'nan', 78);
var t1 = new Teacher('lisi', 39, 'nv', 14000);
//打印看一下
console.log(s1, t1);

构造函数方法的继承

拷贝继承

直接拷贝父类原型上的方法,并手动修改constructor指向

原型赋值

直接把父类实例赋值给子类原型,并手动修改constructor指向

//一个人类的构造函数
    function Person(name, age, sex) {
      this.name = name;
      this.age = age;
      this.sex = sex;
    }
    Person.prototype.sayHello = function () {
      console.log('hello');
    }
    //学生构造函数
    function Student(name, age, sex, num) {
      //直接调用人类构造函数把构造函数当作普通函数来调用
      //因为函数作为普通函数调用会导致this指向window,所以使用call更改this指向
      //因为学生构造函数将来是用来构造实例的,这里直接写this就可以指向这个实例
      Person.call(this, name, age, sex);
      this.num = num;
    }
    //老师构造函数,原理同上
    function Teacher(name, age, sex, salary) {
      Person.call(this, name, age, sex);
      this.salary = salary;
    }


    //原型方法继承
    // 方法1:原型拷贝 for-in,直接遍历父类原型对象,除了constructor外把所有方法都拷贝过来
    // for (const k in Person.prototype) {
    //   if (k === 'constructor') {
    //     continue;
    //   }
    //   Student.prototype[k] = Person.prototype[k];
    // }
    // 方法2:直接把子类原型指向父类实例
    // 不需要传参,因为我们只需要原型 ,构造函数内部的属性不需要,记得修改constructor指向
    Student.prototype = new Person();
    Student.prototype.constructor = Student;




    //创建学生和老师实例
    var s1 = new Student('zhangsan', 19, 'nan', 78);
    var t1 = new Teacher('lisi', 39, 'nv', 14000);
    //打印看一下
    console.log(s1, t1);

组合继承

使用构造函数继承属性,使用原型继承方法

//一个人类的构造函数
function Person(name, age) {
  this.name = name;
  this.age = age;
}
// 给人类添加方法
Person.prototype.sayHello = function () {
  console.log('hello');
}

//学生构造函数
function Student(name, age, sex) {
  //调用父类构造函数继承属性
  Person.call(this, name, age);
  this.sex = sex;
}
//使用原型继承方法
Student.prototype = new Person();
//重置constructor指向
Student.prototype.conconstructor = Student;

var s1 = new Student('zhangsan', 19, 'nv');
console.dir(s1);

实际上,这种原型继承方法继承的是父类的实例,只是把父类实例赋值给子类的原型,所以子类的原型上并没有这个方法,所以会通过原型链向上查找,找到父类的原型上

函数进阶

函数声明和函数表达式

  • 函数声明必须有名字,会自动提升,声明前后都可以调用到
  • 函数表达式一般使用匿名函数,属于变量赋值,在定义之前调用会导致错误
  • 建议:为了避免因为变量提升导致的问题,尽量使用函数表达式方式声明函数
// 关于if语句中的函数声明问题


// 测试函数声明提升
// fn();
// 结果显示:
// 在低版本浏览器中,打印出fn-flase,因为函数声明提升了两次,flase的提升覆盖true的提升
// 在高版本浏览器中,无法调用函数,因为在现代浏览器中,不允许这样的函数声明提升

// 把函数声明写在if语句内部
// if (true) {
//   function fn() {
//     console.log('fn-true');
//   }
// } else {
//   function fn() {
//     console.log('fn-flase');
//   }
// }


// 解决方案:使用函数表达式声明函数
if (true) {
  fn = function () {
    console.log('fn-true');
  }
} else {
  fn = function () {
    console.log('fn-flase');
  }
}


// 测试函数结果
// 结果显示(使用函数表达式):
// 在低版本浏览器中,打印出fn-flase,因为函数声明提升了两次,flase的提升覆盖true的提升
// 高版本浏览器中,打印出fn-true,因为执行了if语句之后创建了函数,可以正常调用,且走了true分支
// 结果显示(函数字面量):
// 无论高版本还是低版本,都可以走正常的true分之,不会发生错误
fn();



// 结论:在if语句中进行函数声明,建议使用函数字面量的方式进行函数声明,这样可以避免浏览器兼容性问题

函数也是对象

函数本身也是对象,可以使用new关键字创建new Function()

书写方式:var fn = new Function('参数1','参数2',...........'结构体')

注意:函数声明的方式更简单,更简洁,比new方法解析更快速,一般不使用new方法

// 使用普通方法定义函数
function fn1(a, b) {
  console.log(a + b);
}

// 使用new关键字定义函数
var fn2 = new Function('a', 'b', 'console.log(a + b)');


fn1(1, 3); //4
fn2(1, 3); //4
// 两个函数运行结果相同,说明两个函数都能运行

函数的调用和this指向

函数的调用

  • 普通函数:加()执行
  • 构造函数:使用new关键字调用构造函数()执行
  • 对象的方法:打点调用加()执行
  • 事件函数:不需要特殊符号,事件触发自动执行
  • 延/定时器:不需要特殊符号,时间到自动执行

函数内部的默认this指向(默认!默认!默认!)

  • 普通函数:默认执行window
  • 构造函数:指向创建的实例对象
  • 对象的方法:指向调用的对象本身
  • 事件函数:指向触发事件的事件源本身
  • 延/定时器:默认指向window
调用方式非严格模式备注
普通函数调用window严格模式下是undefined
构造函数调用实例对象原型方法中this也是实例对象
对象方法调用该方法所属对象紧挨着的对象
事件绑定方法绑定事件对象
定时器函数window

this指向只能在调用的时候才能确定指向,不同调用方法会导致this指向发生变化

// this指向问题

//创建普通函数
function fn1() {
  console.log(this);
}
//创建构造函数
function Fn2() {
  console.log(this);
}

//普通方式执行函数
fn1(); //指向window
var fn2 = new Fn2(); //指向了创建的实例对象


//更改函数调用方式
//创建一个对象,对象内部调用fn1
var n = {
  sayThis: fn1
}

//其他方法调用相同的函数
n.sayThis(); //this指向了n
Fn2(); //指向了window


//总结:不同的函数调用方式决定了不同的this指向,this的指向只有在调用的那一刻才能确定下来

call、apply、bind方法

函数调用的时候会有一个this指向,可以使用这三个方法修改this指向

call方法

调用执行一个函数,并将函数this指向某个对象,可以传递参数带入函数执行

语法:函数.call(指向,"实参","实参"........)

  • 参数1:需要更改的this指向,如果为null或者indefined则指向window
  • 参数2、3、4......:要代入函数的实参,使用逗号分隔

apply方法

原理同上,直接调用函数,区别是实参部分使用数组

语法:函数.apply(指向,['参数1','参数2','参数3'.....])

  • 参数1:需要更改的this指向,如果为null或者indefined则指向window
  • 参数2:一个实参数组。

bind方法

不会执行函数,而是返回一个新函数,完全复制被调用的函数的函数体,可接受参数

语法:函数.bind(指向,'参数1','参数2'.....)

  • 参数1:需要更改的this指向,如果为null或者indefined则指向window
  • 参数2、3、4......:预传参数,会占用实参个数
// 创建一个函数作为基础函数
function fn(a, b) {
  console.log(a + b, this);
}
// 创建一个对象作为指向
var o = {
  name: 'o'
};

fn(3, 4); //直接调用函数,this指向window


// call方法:直接调用执行函数
fn.call(o, 3, 4); //this指向o,并且执行了函数

// apply方法:直接调用函数
fn.apply(o, [3, 4]); //this指向哦,并且执行了函数

// bind方法:创建一个新函数,只是借用了fn的结构体
var x = fn.bind(o, 3); //第二个参数会占用新建函数x的一个形参
x(4); //我们只需要再传一个参数即可,因为第一个参数固定为3,同时发现this指向了o

三种方法应用

call应用

让类数组也可以使用数组的方法,借用方法

// getElementsByTagName方法可以生成类数组
//模拟一个类数组
var o = {
  0: 10,
  1: 20,
  2: 30,
  length: 3
}

// 需求:我们想给o内部添加一个3:40

//常规方法
// o['3'] = 40; //添加成员
// o.length = 4; //修改长度

// 使用call方法,借用Array的push方法,把this指向o即可
Array.prototype.push.call(o, 40);
console.log(o);

apply应用

利用apply参数会被拆开的特点进行操作

var arr = [8, 3, 5, 8, 2, 9, 4, 7, 0];
// 需求:寻找出arr中的最大值

//使用Math对象的max方法,把数组当作apply的第二个参数
console.log(Math.max.apply('', arr));

//输出全部数组元素
console.log.apply('', arr);

bind应用

适用于不需要立即执行但需要修改this的场景

// 创建对象
var o = {
  name: 'zs',
  age: 18,
  //添加一个方法
  say: function () {
    setInterval(function () {
      console.log(this.age);
      // 手动把this指向o,这里写this是因为在调用的时候会默认把tgis指向o,这里的this===o
    }.bind(this), 1000)
  }
}
//执行方法,开启定时器
o.say();

//事件也可以更改指向
document.onclick = function () {
  console.log(this.age);
  //更改指向
}.bind(o);

函数其他成员

属性

  • arguments:函数再调用时传入的所有实参组成的类数组对象
  • arguments.callee:指向所属的函数本身,类似于构造函数
  • caller:函数的调用者,函数在哪个作用于调用,全局调用值为null
  • length:形参个数
  • name:函数名称

我们可以利用arguments来获取传入的所有实参,进行一些操作

// 利用arguments来操作实参
function max() {
  var maxNum = arguments[0];
  for (let i = 0; i < arguments.length; i++) {
    if (arguments[i] > maxNum) {
      maxNum = arguments[i];
    }
  }
  console.log(maxNum);
}
max(1, 4, 6, 4, 8, 9, 2, 7, 7);

高阶函数

当一个函数作为另一个函数的参数或者返回值那这个函数即为高阶函数

// 高阶函数

// 函数作为参数
function eat(fn) {
  console.log('吃饭');
  fn();
}

function look() {
  console.log('看电影');
}
//调用函数,执行作为参数的函数
eat(look);


//函数作为返回值
function he(x) {
  // 返回一个函数
  return function (y) {
    console.log(x + y);
  }
}
// 创建变量获取返回函数
var he100 = he(100);
var he1000 = he(1000);
var he10000 = he(10000);
//调用函数
he100(1000);
he1000(1000);
he10000(1000);

闭包

函数定义的时候能记住自己的作用域环境和自己,并将这些形成一个封闭的环境,叫做闭包,不论函数以任何方式任何位置调用,都会回到自己定义时的密闭环境中执行

  • 广义上来说,函数就是闭包,但是定义在全局的函数无法拿到更外层环境中观察
  • 闭包是天生存在的,只要定义了函数那么它就是一个闭包,为了方便观察,我们需要利用一些特殊结构把子函数拿到父函数外部执行,从而观察闭包的存在

闭包必须回到自己创建时候的作用域去执行

闭包的理解和应用

  • 可以在函数外部获取内部成员
  • 让函数内部的成员始终存活在内存中

闭包是一个指针,指向自己定义时候的作用域及上下文,类似于构造函数的原型,使用的时候要跑到自己的作用域中

闭包的问题

的值

// 闭包的问题
// var arr = [];
// for (var i = 0; i < 10; i++) {
//   // 这个函数的作用域是全局,闭包并不能记住这个i,只能调用的时候去读取这个i
//   arr[i] = function () {
//     console.log(i);
//   }
// }
// //当调用的时候i已经等于10了,所以无论怎么调用都只能输出10
// arr[0](); //10
// arr[4](); //10
// arr[6](); //10


//解决方案:封住作用域(使用自调用函数)
var arr = [];
for (var i = 0; i < 10; i++) {
  //使用自调用函数封住作用域
  (function (i) {
    // 自调用函数调用这个实参执行函数体
    // 自调用函数封住了作用域,并且把i的值保留了下来
    arr[i] = function () {
      console.log(i);
    }
  })(i); //把i的值存在自调用函数的实参上
}
arr[0](); //0
arr[4](); //4
arr[6](); //6