函数对象

let fun2 = new Function([函数的参数用逗号分开],函数体);

定义函数就会new function();

1
2
3
4
5
6
7
let fun2 = new Function("console.log('yingyingying')"); 

fun2();

let fun3 = new Function("a", "b", "console.log(a+b)");

fun3(3, 5);

函数定义的三种方式:

函数声明

1
2
<1>function fun(){
}

函数表达式(匿名函数)

1
2
<2>let fun = function() {
}

对象的方式

<3>let fun2 = new Function([函数的参数用逗号分开],函数体);

1
2
let fun2 = new Function("console.log('yingyingying')");
fun2();

this

this:函数的内置对象,只能在函数体内出现

1,事件体:代表触发该事件的元素

1
2
3
document.onclick = function() {
console.log(this);
}

2,构造(方法)函数:new出来的对象,指向构造函数的实例

1
2
3
4
5
6
7
8
function Student(name, age) {
    this.name = name;
    this.age = age;
    this.study = function() {
    //3.与普通方法连用:调用该方法的对象
    console.log(this.name + ":study");
}
}

3,箭头函数的this:(箭头函数没有this),代表该方法的父元素的前缀

1
2
3
4
5
6
7
创建类
class 类名{
constructor() {
}
方法1
方法2
}

4,和普通方法(函数)连用时,代表调用该方法的对象(window)

arguments(伪数组)

arguments对象保存函数的所有参数,虽然可以像数组一样访问每个参数,但是,并不是标准的数组(无法使用数组的API),arguments对象不但保存着所有的参数,而且还有一个名叫callee的属性。callee属性是个指针,指向了arguments对象所在的函数。

不定参函数

1
2
3
4
5
6
7
8
9
10
function fun() {
   for (let i = 0; i < arguments.length; i++) {
    if (typeof arguments[i] == "number") {
        console.log("数字操作");
     } else if (typeof arguments[i] == "string") {
          console.log("字符串操作");
       }
   }
}
fun("heihei", "hello", 1);

arguments.callee: 代表该函数对象

1
2
3
4
function fun() {
    console.log(arguments.callee);
}
fun();

递归:一个函数直接或间接的调用自己本身

作用:简化代码

示例:

1
2
3
4
5
6
7
8
9
10
function age(n) {
   let c;
   if (n == 1) {
      c = 10;
   } else {
        c = arguments.callee(n - 1) + 2;
    }
        return c;
    }
console.log(age(5));

递归:自己调用自己

递归如果没有终止条件,会一直压栈,造成内存泄漏。

在分析递归之前,需要了解javascript中“压栈”概念

栈是什么?可以理解成内存中某一块区域,这个区域比喻成一个箱子,往箱子里放东西,这个动作就是压栈。所以最先放下去的东西在箱子最底下,最后放下去的在箱子最上面。把东西从箱子中拿出来可以理解为出栈。(先进后出的规则)

在 JavaScript 中,调用函数时,都会发生压栈行为,遇到含

return 关键字的句子或执行结束后,才会发生出栈(pop)。

image-20220801204949855

函数的属性和方法

prototype属性:

对于引用类型来说,prototype保存着所有实例方法的真正所在,即所有的实例方法都是在prototype中保存着。

我们创建的每一个函数都有一个prototype(原型)属性,这个属性是个指针,指向一个对象,该对象的用途是包含所有实例共享的属性和方法,所有通过同一个构造函数创建的实例对象,都会共享同一个prototype

示例:

1
2
3
4
function Student(id, name) {
        this.id = id;
        this.name = name;
}

prototype:原型对象,是函数(构造函数)对象的一个属性

它保存着所有实例化对象共享的函数或者属性

1
2
3
4
5
6
7
Student.prototype.study = function() {
  console.log(this.name + ":study");
}
Student.prototype.teacher = "大杨";
Student.prototype.teacher = "小杨杨";
let s1 = new Student(1, "菜徐坤");
let s2 = new Student(2, "肖战");

区分原型的属性和实例的属性:

实例:用new调用构造函数创建出来的对象叫做实例化对象

原型属性:写在prototype后面的叫做属性

实例属性:创建出来的对象,重新给原型属性赋值后,就成为了实例属性

原型图:

函数(类)的原型对象属性:prototype

实例化对象的属性:proto

截图1

总结:prototype原型对象,是函数(构造函数)对象的一个属性,保存 所有实例化对象共享的属性或者方法,我们创建的每一个函数都有 一个prototype属性,该属性指向原型对象,所有通过同一个构造函数创建的实例化对象,都会共享同一个prototype

_proto_实例化对象的属性:指向原型对象,实例化对象可以使用 原型对象的属性或者方法但是不能修改

new关键字执行过程:

1.创建新的实例化对象(开辟空间)

2.新的实例化对象的_proto_指向构造函数的prototype

3.将构造函数的this指向新的实例化对象

4.返回新对象

apply()和call()方法:

apply和call:改变函数的this指向

函数对象.apply(call) (被改变的this指向,函数对象的参数1,参数2..);

不同点在于call()方法直接在方法里传参,而apply()是将所有参数以数组的形式进行传递

将函数和对象之间进行解耦

示例;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Monkey(name) {
    this.name = name;
}
function Snake(name) {
    this.name = name;
}
function eat(food1, food2) {
    console.log(this.name + ":" + food1 + " " + food2);
}

let m = new Monkey("熏悟空");
eat.call(m, "桃子", "唐僧");
eat.apply(m, ["桃子", "唐僧"]);

let s = new Snake("白素贞");
eat.call(s, "许仙", "法海");
eat.apply(s, ["许仙", "法海"]);

总结:

apply(对象,”参数1”,”参数2”);

call(对象,[“参数1”,”参数2”]);

bind、apply和call的异同?

1.都是用来改变this指向的

2.bind用来修饰匿名函数

3.apply和call修饰有名函数

4.apply的参数必须放在数组中

5.apply和call是对源原函数的调用

6.bind是生成一个新的函数对象,需要重新调用

继承

原型继承:

就是子对象自动拥有父对象的属性和方法,继承可以提高代码的复用性

js里的继承主要依靠的是原型链,让原型对象(每一个构造函数都有一个原型对象)的值等于另一个类型的实例,即实现了继承;另外一个类型的原型再指向第三个类型的实例,以此类推,也就形成了一个原型链

原型对象实现继承

将父类的实例化对象赋值给子类的原型对象

子类的原型对象指向其父类的实例化对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//创建Animal类
function Animal(name) {
    this.name = name;
}
    //定义Animal方法
    Animal.prototype.eat = function() {
        console.log("Animal eat");
    }
 //创建Human类
function Human(id) {
    this.id = id;
}
//创建Animal实例化对象指向Human类
Human.prototype = new Animal("老王");
//定义Human方法
Human.prototype.makeTools = function() {
     console.log("Human makeTools");
}
//创建Human的实例化对象
let h = new Human(1);
console.log(h.name, h.id);
h.eat();
h.makeTools();

原型链:

子类对象以向上找的方式来访问所有的属性和方法,如果找不到就到它的原型去找,如果还是找不到就到原型的原型去找,如果直到最顶层的object.prototype还是找不到就返回null

原型继承的缺陷:

1.必须先实现继承关系,再给子类原型对象添加属性或方法

2.一旦实现原型继承,则子类的原型对象的值不能再修改(可以添加原型对象的属性和方法)

3.由父类派生给子类的对象的属性是不可以初始化的

call和apply的继承:

在子类构造函数中使用call或apply方法调用父类构造函数并改变其this指向为子类构造函数的this

作用:为了实现将父类派生给子类的属性,在子类对象构造时进行初始化

如何让实现继承:借用父类的构造方法

缺陷:不能继承原型上的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Human(name, id) {
    this.name = name;
    this.id = id;
    // this.eat = function() {
    //console.log("Human eat");
     // }
    }
Human.prototype.eat = function() {
    console.log("Human eat");
}
function Student(name, id, score) {
    Human.call(this, name, id);
    this.score = score;
}
let s = new Student("蔡徐坤", 9527, 100);
console.log(s);
s.eat();

混合继承

1,属性用apply和call

2,方法用原型继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Human(name, id) {
    this.name = name;
    this.id = id;
}
Human.prototype.eat = function() {
    console.log("Human eat");
}
function Student(name, id, score) {
    //属性继承
    Human.apply(this, [name, id]);
    this.score = score;
}
//实现原型继承
Student.prototype = new Human();
Student.prototype.study = function() {
     console.log("Student study");
}
let s = new Student("刘昊然", 1, 100);
console.log(s);
s.eat();
s.study();

ES6继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Human {
   constructor(name, id) {
        this.name = name;
        this.id = id;
    }
   eat() {
        console.log("Human eat");
      }
    }
class Student extends Human { //子类继承父类extends
        constructor(name, id, score) {
            //super父类的构造方法
            //super必须写在第一行
            super(name, id);
            this.score = score;
        }
        study() {
            console.log("Student study");
        }
    }
let s = new Student("老王", 1, 100);
console.log(s.name, s.id, s.score);
s.eat();
s.study();

深浅拷贝

拷贝:用已有的对象初始化一个新的对象

let a = 123;

let b = a;

深拷贝与浅拷贝 -> 内置类型和引用类型在内存中存储的异同

内置类型:只有一块栈空间,存储的就是数据本身

引用类型:有两块空间,栈存储的是堆空间的地址,堆存储的是数据

只有引用类型才有深浅拷贝的概念

浅拷贝:只拷贝地址,不开辟空间

1
2
let arr = [6, 5, 7, 4, 8];
let arr1 = arr;

深拷贝:开辟空间且赋值

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
数组深拷贝
let arr = [6, 54, 343, 45, 65];
let arr1 = [];
for (let i = 0; i < arr.length; i++) {
        arr1.push(arr[i]);
    }
arr[0] = 99999;
console.log(arr, arr1);
console.log(arr == arr1);

函数:
function Student(id, name) {
     this.id = id;
     this.name = name;
}
Student.prototype.clone = function() {
    let item = new Student(this.id, this.name);
      return item;
}
let s1 = new Student(1, "刘德华");
//必须满足开辟空间且赋值
let s2 = s1.clone();
s2.name = "陈奕迅";
console.log(s1, s2);

函数自运行

能够自己运行的一个函数。即,不用别人调用就能运行的函数,(闭包的基础)

自运行的语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<1>
(function(){
console.log("xixi");
}) ();

<2>官方推荐:
(function(){
console.log("xixi");
} ());

<3>
! function() {
console.log("xixi");
}();

<4>
void function() {
console.log("xixi");
}();

闭包

闭包的使用场景:

js的作用域分为两种,全局和局部,在作用域环境中访问变量是由内向外的,内部作用域可以获得当前作用域下的变量并且可以获得当前包含当前作用域的外层作用域下的变量,反之则不能,也就是说在外层作用域下无法获取内层作用域下的变量,同样在不同的函数作用域中也是不能相互访问彼此变量的,想要在一个函数内部访问另一个函数内部的变量就使用闭包,闭包的本质就是在一个函数内部创建另一个函数

闭包是指有权访问另外一个函数作用域中的变量的函数,可以理解为(能够读取其他函数内部变量的函数)

闭包的概念:

函数嵌套函数,被嵌套的函数是闭包函数

闭包的作用:

可以在主函数外部使用内部的局部变量

闭包的实现原理:

在主函数中定义子函数,将主函数的局部变量在子函数中操作,将子函数作为主函数的返回值。然后在外部定义全局变量与主函数的返回值绑定,从而延长了局部变量的生命周期。

1
2
3
4
5
6
7
8
9
10
11
unction fun() { //闭包的概念:函数嵌套函数,被嵌套的函数称为闭包函数
    var count = 0; //闭包的作用:在于fun外部使用fun的内部局部变量count
    //闭包的实现:在子函数fun1中操作count,将fun1作为fun的返回值
    var fun1 = function() { //通过全局变量f绑定fun()的返回值fun1,
        return ++count;//从而延长了fun1及count的生命周期
    }//实现了可以在fun外部操作fun内部的局部变量
   //相当与产生了一个块级作用域
    return fun1;
}
let f = fun();
console.log(f());

闭包的缺陷:

打破了垃圾回收机制,可能造成内存泄漏(已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果)

垃圾回收机制:

当对象创建时会在堆内存中开辟空间,如果某个对象完全不被使用时,系统将会自动销毁其空间

闭包的好处:

<1>保护函数内的变量安全,实现封装,防止变量流入其他环境发生命名冲突

<2>在内存中维持一个变量,可以做缓存(使用多了会消耗内存)

<3>匿名自执行函数可以减少内存消耗

闭包的坏处:

<1>引用的私有变量不会被销毁,增大了内存的消耗,造成内存泄漏,解决方法就是可以在使用完变量后手动为它赋值null

<2>其次由于闭包设计跨域访问,所以会导致性能损失,可以通过把跨作用域变量存储在局部变量中,然后直接访问局部变量,来减轻对执行速度的影响