JavaScript 高级
什么是对象
- Everything is object (万物皆对象)
- 对象到底是什么,我们可以从两次层次来理解。
- 对象是单个事物的抽象。
- 一本书、一辆汽车、一个人都可以是对象,一个数据库、一张网页、一个与远程服务器的连接也可以是对象。当实物被抽象成对象,实物之间的关系就变成了对象之间的关系,从而就可以模拟现实情况,针对对象进行编程。
- 对象是一个容器,封装了属性(
property
)和方法(method
)- 属性是对象的状态,方法是对象的行为(完成某种任务)。比如,我们可以把动物抽象为animal对象,使用“属性”记录具体是那一种动物,使用“方法”表示动物的某种行为(奔跑、捕猎、休息等等)。在实际开发中,对象是一个抽象的概念,可以将其简单理解为:
数据集或功能集
。
- 属性是对象的状态,方法是对象的行为(完成某种任务)。比如,我们可以把动物抽象为animal对象,使用“属性”记录具体是那一种动物,使用“方法”表示动物的某种行为(奔跑、捕猎、休息等等)。在实际开发中,对象是一个抽象的概念,可以将其简单理解为:
- 提示:每个对象都是基于一个引用类型创建的,这些类型可以是系统内置的原生类型,也可以是开发人员自定义的类型。
什么是面向对象
- 面向对象不是新的东西,它只是过程式代码的一种高度封装,目的在于提高代码的开发效率和可维护性。
- 面向对象编程 ——
Object Oriented Programming
,简称OOP
,是一种编程开发思想。 - 它将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。
- 在面向对象程序开发思想中,每一个对象都是功能中心,具有明确分工,可以完成接受信息、处理数据、发出信息等任务。
- 因此,面向对象编程具有灵活、代码可复用、高度模块化等特点,容易维护和开发,比起由一系列函数或指令组成的传统的过程式编程(
procedural programming
),更适合多人合作的大型软件项目。
面向对象与面向过程
-
面向过程:
- 面向过程就是亲力亲为,事无巨细,面面俱到,步步紧跟,有条不紊
- 面向对象就是找一个对象,指挥得结果
- 比如:自己想建造一辆汽车。
- 面向过程:从头开始建造每一个都重新开始
- 面向对象:直接从别的地方购买拿过来直接使用(购买轮胎、方向盘等组装)
- 比如:自己想建造一辆汽车。
- 面向对象将执行者转变成指挥者
- 面向对象不是面向过程的替代,而是面向过程的封装
-
面向对象的特性:
- 封装性
- 继承性
- [多态性]
面向过程的体现
-
在 JavaScript 中,所有数据类型都可以视为对象,当然也可以自定义对象。
-
自定义的对象数据类型就是面向对象中的类(
Class
)的概念。 -
我们以一个例子来说明面向过程和面向对象在程序流程上的不同之处。
-
面向过程:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body></body> <script> var stu1 = { name: "张三", score: 80, }; var stu2 = { name: "李四", score: 90, }; function printScore(student) { console.log(`姓名:${student.name},成绩:${student.score}`); } printScore(stu1); printScore(stu2); </script> </html>
-
面向对象的基本体现(一)
-
如果采用面向对象的程序设计思想,我们首选思考的不是程序的执行流程,
-
而是
Student
这种数据类型应该被视为一个对象,这个对象拥有name
和score
这两个属性(Property
)。 -
如果要打印一个学生的成绩,首先必须创建出这个学生对应的对象,然后,给调用
printScore
,将成绩打印。 -
抽象数据行为模板(Class):
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body></body> <script> // 创建学生类 name score 形参 function Student(name, score) { // this 表示 student 对象 // this.name 表示的是 student 的属性 this.name = name; this.score = score; // 方法 this.printScore = function () { console.log(`姓名:${this.name},成绩:${this.score}`); }; } // 创建学生对象 (实例) var stu1 = new Student("张三", 80); var stu2 = new Student("李四", 90); console.log(stu1); console.log(stu2); // 对象调用方法 stu1.printScore(); stu2.printScore(); </script> </html>
面向对象的基本体现(二)
- 面向对象的设计思想是从自然界中来的,因为在自然界中,类(
Class
)和实例(Instance
)的概念是很自然的。 Class
是一种抽象概念,比如我们定义的Class
——Student
,是指学生这个概念- 而实例(
Instance
)则是一个个具体的 Student ,比如,张三
和李四
是两个具体的Student
。 - 所以,面向对象的设计思想是:
- 抽象出
Class
- 根据
Class`` 创建
Instance` - 指挥
Instance
得结果
- 抽象出
- 面向对象的抽象程度又比函数要高,因为一个 Class 既包含数据,又包含操作数据的方法。
创建对象(一)
-
简单方式
-
通过
new Object()
创建<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body></body> <script> // 创建对象的方式一 new Object var dog = new Object(); dog.name = "泰迪"; dog.weight = "15"; dog.talk = function () { console.log(`${this.name}:汪汪汪`); }; console.log(dog); dog.talk(); </script> </html>
-
每次创建通过
new Object()
比较麻烦,所以可以通过它的简写形式对象字面量来创建:<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body></body> <script> // 创建对象的方式二 字面量 方式创建对象 var dog2 = { name: "哈士奇", weight: "30", talk: function () { console.log(`${this.name}:汪汪汪`); }, }; console.log(dog2); dog2.talk(); </script> </html>
-
创建对象(二)
-
对于上面的写法固然没有问题,但是假如我们要生成两个
dog
实例对象呢? -
通过上面的代码我们不难看出,这样写的代码太过冗余(如果要创建多条
dog
),重复性太高。 -
简单方式的改进:工厂函数
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body></body> <script> function createDog(name, weight) { return { name: name, weight: weight, talk: function () { console.log(`${this.name}:汪汪汪`); }, }; } var taidi = createDog("泰迪", 15); var hashiqi = createDog("哈士奇", 30); console.log(taidi); console.log(hashiqi); taidi.talk(); hashiqi.talk(); </script> </html>
工厂函数改造
-
这样封装确实爽多了,通过工厂模式我们解决了创建多个相似对象代码冗余的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。
-
一种更优雅的工厂函数就是下面这样,构造函数:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body></body> <script> // 构造函数 // 定义 Person function Person(name, age) { this.name = name; this.age = age; this.sayHi = function () { console.log(`${this.name}:'hi'`); }; } // 创建对象 new 构造函数() var zhangsan = new Person("张三", 18); var lisi = new Person("李四", 20); console.log(zhangsan); zhangsan.sayHi(); console.log(lisi); lisi.sayHi(); </script> </html>
解析构造函数代码的执行
- 在上面的示例中,
Person()
函数取代了createPerson()
函数,但是实现效果是一样的。这是为什么呢?我们注意到,Person()
中的代码与createPerson()
有以下几点不同之处:- 没有显示的创建对象
- 直接将属性和方法赋给了
this
对象 - 没有
return
语句 - 函数名使用的是大写的
Person
- 而要创建
Person
实例,则必须使用new
操作符。以这种方式调用构造函数会经历以下 4 个步骤:- 创建一个新对象
- 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象)
- 执行构造函数中的代码
- 返回新对象
构造函数和实例对象的关系
-
使用构造函数的好处不仅仅在于代码的简洁性,更重要的是我们可以识别对象的具体类型了。在每一个实例对象中的
__proto__
中同时有一个constructor
属性,该属性指向创建该实例的构造函数:console.log(zhangsan.constructor == Person); // true console.log(lisi.constructor == Person); // true console.log(zhangsan.constructor == lisi.constructor); // true
-
对象的
constructor
属性最初是用来标识对象类型的,但是,如果要检测对象的类型,还是使用instanceof
操作符更可靠一些:console.log("张三的原型Person? ", zhangsan instanceof Person); // 张三的原型Person? true console.log("李四的原型Person? ", lisi instanceof Person); // 李四的原型Person? true
-
总结:
-
构造函数是根据具体的事物抽象出来的抽象模板
-
实例对象是根据构造函数new出来的
-
每一个实例对象都具有一个
constructor
属性,指向创建该实例的构造函数- 注意:
constructor
是实例的属性的说法不严谨,具体后面的原型会讲到 - 可以通过实例的
constructor
属性判断实例和构造函数之间的关系 - 注意:这种方式不严谨,推荐使用
instanceof
操作符
- 注意:
构造函数的问题
-
使用构造函数带来的最大的好处就是创建对象更方便了,但是其本身也存在一个浪费内存的问题:
-
那就是对于每一个实例对象,
type
和sayHello
都是一模一样的内容,每一次生成一个实例,都必须为重复的内容,多占用一些内存,如果实例对象很多,会造成极大的内存浪费。<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body></body> <script> // 构造函数 // 定义 Person function Person(name, age) { this.name = name; this.age = age; this.type = "human"; this.sayHi = function () { console.log(`${this.name}:'hi'`); }; } // 创建对象 new 构造函数() var zhangsan = new Person("张三", 18); var lisi = new Person("李四", 20); console.log(zhangsan.constructor == lisi.constructor); // true console.log(zhangsan.sayHi === lisi.sayHi); // false </script> </html>
构造函数的问题解决
-
对于这种问题我们可以把需要共享的函数定义到构造函数外部:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body></body> <script> function sayHi() { console.log(`${this.name}:hi`); } function Person(name, age) { this.name = name; this.age = age; this.type = "human"; this.sayHi = sayHi; } var p1 = new Person("张三", 18); var p2 = new Person("李四", 20); console.log(p1); console.log(p2); p1.sayHi(); p2.sayHi(); console.log(p1.sayHi === p2.sayHi); // true </script> </html>
-
这样确实可以了,但是如果有多个需要共享的函数的话就会造成全局命名空间冲突的问题。
原型
-
更好的解决方案:
prototype
-
Javascript
规定,每一个构造函数都有一个prototype
属性,指向另一个对象。这个对象的所有属性和方法,都会被构造函数的实例继承。 -
这也就意味着,我们可以把所有对象实例需要共享的属性和方法直接定义在
prototype
对象上。<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body></body> <script> // Person 类 function Person(name, age) { this.name = name; this.age = age; } // 在原型上添加属性和方法 Person.prototype.type = "human"; Person.prototype.sayHi = function () { console.log(`${this.name}:hi`); }; var p1 = new Person("张三", 18); var p2 = new Person("李四", 20); console.log(p1, p2); p1.sayHi(); // 引用地址一致 避免浪费内存 console.log(p1.sayHi === p2.sayHi); // 指向同一个地址 </script> </html>
-
这时所有实例的
type
属性和sayHi()
方法,其实都是同一个内存地址,指向prototype
对象,因此就提高了运行效率。
构造函数、实例、原型三者之间的关系
-
任何函数都具有一个
prototype
属性,该属性是一个对象。function Foo() {} console.log(Foo.prototype); // Object Foo.prototype.sayHi = function () { console.log("hi"); };
-
构造函数的
prototype
对象默认都有一个constructor
属性,指向prototype
对象所在函数。console.log(Foo.prototype.constructor === Foo); // true
-
通过构造函数得到的实例对象内部会包含一个指向构造函数的
prototype
对象的__proto__
。// 创建 foo 的实例对象 var foo = new Foo(); console.log(foo.__proto__ === Foo.prototype); // true
-
__proto__
是非标准属性。 -
实例对象可以直接访问原型对象成员。
-
总结:
- 任何函数都具有一个
prototype
属性,该属性是一个对象 - 构造函数的
prototype
对象默认都有一个constructor
属性,指向prototype
对象所在函数 - 通过构造函数得到的实例对象内部会包含一个指向构造函数的
prototype
对象的指针__proto__
- 所有实例都直接或间接继承了原型对象的成员
- 任何函数都具有一个
更简单的原型语法
-
我们注意到,前面例子中每添加一个属性和方法就要敲一遍
Person.prototype
。 -
为减少不必要的输入,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body></body> <script> // Person 类 function Person(name, age) { this.name = name; this.age = age; } // prototype 是一个 Object Person.prototype = { type: "human", sayHi: function () { console.log(`${this.name}:hi`); }, }; var p1 = new Person("张三", 18); var p2 = new Person("李四", 20); console.log(p1, p2); p1.sayHi(); // 引用地址一致 避免浪费内存 console.log(p1.sayHi === p2.sayHi); // 指向同一个地址 </script> </html>
更简单的原型语法存在的问题
-
在该示例中,我们将
Person.prototype
重置到了一个新的对象。 -
这样做的好处就是为
Person.prototype
添加成员简单了,但是也会带来一个问题,那就是原型对象丢失了constructor
成员。 -
所以,我们为了保持
constructor
的指向正确,建议的写法是:<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body></body> <script> // Person 类 function Person(name, age) { this.name = name; this.age = age; } // prototype 是一个 Object Person.prototype = { // 如果将 prototype 写出一个对象 需要重写 constructor constructor: Person, type: "human", sayHi: function () { console.log(`${this.name}:hi`); }, }; var p1 = new Person("张三", 18); var p2 = new Person("李四", 20); console.log(p1, p2); p1.sayHi(); // 引用地址一致 避免浪费内存 console.log(p1.sayHi === p2.sayHi); // 指向同一个地址 </script> </html>
原生对象的原型
-
所有函数都有
prototype
属性对象。Object.prototype
Function.prototype
Array.prototype
String.prototype
Number.prototype
Date.prototype
-
作业:为数组对象和字符串对象扩展原型方
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body></body>
<script>
// 在原型上添加方法
Array.prototype.sayHi = function () {
console.log("你好,我是数组");
};
var a = new Array();
a.sayHi();
String.prototype.sayHi = function () {
console.log("你好,我是字符串");
};
var b = new String();
b.sayHi();
</script>
</html>
原型对象的问题
- 共享数组
- 共享对象
- 如果真的希望可以被实例对象之间共享和修改这些共享数据那就不是问题。但是如果不希望实例之间共享和修改这些共享数据则就是问题。
- 一个更好的建议是,最好不要让实例之间互相共享这些数组或者对象成员,一旦修改的话会导致数据的走向很不明确而且难以维护。
- 原型对象使用建议
- 私有成员(一般就是非函数成员)放到构造函数中
- 共享成员(一般就是函数)放到原型对象中
- 如果重置了
prototype
记得修正constructor
的指向
什么是继承
构造函数的属性继承:借用构造函数
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body></body>
<script>
// 定义 Person 类
function Person(name, age) {
this.name = name;
this.age = age;
}
// 定义 Student 类
function Student(name, age) {
// 继承
Person.call(this, name, age);
}
var sdt = new Student("张三", 18);
console.log(sdt);
</script>
</html>
构造函数的原型方法继承:拷贝继承(for-in)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body></body>
<script>
// 定义 Person 类
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHi = function () {
console.log(`${this.name}:'Hi'`);
};
// 定义 Student 类
function Student(name, age) {
// 继承属性
Person.call(this, name, age);
}
// copy 继承 继承原型的属性
for (var key in Person.prototype) {
console.log(key); // sayHi
Student.prototype[key] = Person.prototype[key];
}
var sdt = new Student("张三", 18);
console.log(sdt);
sdt.sayHi();
</script>
</html>
另一种继承方式:原型继承
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body></body>
<script>
// 定义 Person 类
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHi = function () {
console.log(`${this.name}:'Hi'`);
};
// 定义 Student 类
function Student(name, age) {
// 继承属性
Person.call(this, name, age);
}
// 将 Person 对象赋值给 Student 原型
Student.prototype = Person.prototype;
// Student.prototype = new Person();
var stu = new Student("张三", 20);
console.log(stu);
</script>
</html>
函数进阶-函数的定义方式
-
函数声明
-
函数表达式
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body></body> <script> // 函数的定义方式 // 声明式 foo(); // 不会报错 存在变量提升 function foo() {} // 表达式 // bar(); // 报错 表达式没有变量提升 var bar = function () {}; </script> </html>
-
函数声明与函数表达式的区别
- 函数声明必须有名字
- 函数声明会函数提升,在预解析阶段就已创建,声明前后都可以调用
- 函数表达式类似于变量赋值
- 函数表达式可以没有名字,例如匿名函数
- 函数表达式没有变量提升,在执行阶段创建,必须在表达式执行之后才可以调用
函数进阶-函数的调用方式
-
普通函数
-
构造函数
-
对象方法
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body></body> <script> // 普通函数 function foo() {} foo(); // 构造函数 function F() {} var a = new F(); // 对象中的函数 var c = { sayHi: function () {}, }; c.sayHi(); </script> </html>
函数进阶-函数内 this
指向的不同场景
-
函数的调用方式决定了
this
指向的不同:调用方式 非严格模式 备注 普通函数调用 window 严格模式下是 undefined 构造函数调用 实例对象 原型方法中 this 也是实例对象 对象方法调用 该方法所属对象 紧挨着的对象 事件绑定方法 绑定事件对象 定时器函数 window <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> <button id="btn">按钮</button> </body> <script> // 普通函数 function foo() { console.log(this); // Window } foo(); // 构造函数 function Person() { console.log(this); // Person {} } var p = new Person(); // 对象方法调用 var obj = { name: "张三", sayHi: function () { console.log(this); // 张三 等于所属对象 }, girlFriend: { name: "小红", talk: function () { console.log(this); // 小红 }, }, }; obj.sayHi(); obj.girlFriend.talk(); // 事件绑定方法 document.querySelector("#btn").onclick = function () { console.log(this); // <button id="btn">按钮</button> }; // 定时器函数 setTimeout(function () { console.log(this); // Window }, 2000); </script> </html>
函数进阶-call
、apply
、bind
-
那了解了函数
this
指向的不同场景之后 -
我们知道有些情况下我们为了使用某种特定环境的
this
引用,这时候时候我们就需要采用一些特殊手段来处理了,例如我们经常在定时器外部备份this
引用,然后在定时器函数内部使用外部this
的引用。<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body></body> <script> var obj = { name: "张三", sayHi: function () { // 保存 this var _this = this; console.log(this); // Object setTimeout(function () { console.log(_this.name); // 张三 }, 200); }, }; obj.sayHi(); </script> </html>
-
然而实际上对于这种做法我们的
JavaScript
为我们专门提供了一些函数方法用来帮我们更优雅的处理函数内部this
指向问题。这就是接下来我们要学习的call
、apply
、bind
三个函数方法。
call
-
call()
方法调用一个函数, 其具有一个指定的this
值和分别地提供的参数(参数的列表)。 -
注意:该方法的作用和
apply()
方法类似,只有一个区别,就是call()
方法接受的是若干个参数的列表,而apply()
方法接受的是一个包含多个参数的数组。 -
语法:
fun.call(thisArg,arg1, arg2)
-
参数:
thisArg
- 在
fun
函数运行时指定的this
值 - 如果指定了
null
或者undefined
则内部this
指向 `window``
- 在
arg1, arg2, ...
- 指定的参数列表
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body></body> <script> var obj = { name: "admin", }; function foo(a, b) { // console.log(this); // {name: 'admin'} console.log(this.name); // admin console.log(a + b); // 30 } foo.call(obj, 10, 20); </script> </html>
apply
-
apply()
方法调用一个函数, 其具有一个指定的this
值,以及作为一个数组(或类似数组的对象)提供的参数。 -
注意:该方法的作用和
call()
方法类似,只有一个区别,就是call()
方法接受的是若干个参数的列表,而apply()
方法接受的是一个包含多个参数的数组。 -
语法:
fun.apply(thisArg, [argsArray])
-
参数:
thisArg
argsArray
-
apply()
与call()
非常相似,不同之处在于提供参数的方式。 -
apply()
使用参数数组而不是一组参数列表。<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body></body> <script> var obj = { name: "admin", }; function foo(a, b) { // console.log(this); // {name: 'admin'} console.log(this.name); // admin console.log(a + b); } foo.apply(obj, [10, 20]); // call 和 apply 很相似 只是call传递参数是列表 而apply是数组 </script> </html>
bind
-
bind()
函数会创建一个新函数(称为绑定函数),新函数与被调函数(绑定函数的目标函数)具有相同的函数体。 -
当目标函数被调用时 this 值绑定到
bind()
的第一个参数,该参数不能被重写。绑定函数被调用时,bind()
也接受预设的参数提供给原函数。 -
一个绑定函数也能使用
new
操作符创建对象:这种行为就像把原函数当成构造器。提供的this
值被忽略,同时调用时的参数被提供给模拟函数。 -
语法:
fun.bind(thisArg, arg1, arg2 )
-
参数:
thisArg
- 当绑定函数被调用时,该参数会作为原函数运行时的
this
指向。当使用new
操作符调用绑定函数时,该参数无效。
- 当绑定函数被调用时,该参数会作为原函数运行时的
arg1, arg2, ...
- 当绑定函数被调用时,这些参数将置于实参之前传递给被绑定的方法。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body></body> <script> var obj = { name: "admin", }; function foo(a, b, c, d) { // console.log(this); // {name: 'admin'} console.log(this.name); // admin console.log(a + b); // 30 console.log(c + d); // 60 } // 最后一个括号表示调用 // 会将两个括号里面的参数进行合并 然后全部传递 foo.bind(obj, 10, 20)(20, 40); </script> </html>
小结
call
和apply
特性一样- 都是用来调用函数,而且是立即调用
- 但是可以在调用函数的同时,通过第一个参数指定函数内部
this
的指向 call
调用的时候,参数必须以参数列表的形式进行传递,也就是以逗号分隔的方式依次传递即可apply
调用的时候,参数必须是一个数组,然后在执行的时候,会将数组内部的元素一个一个拿出来,与形参一一对应进行传递- 如果第一个参数指定了
null
或者undefined
则内部this
指向window
bind
- 可以用来指定内部
this
的指向,然后生成一个改变了this
指向的新的函数 - 它和
call
、apply
最大的区别是:bind
不会调用 bind
支持传递参数,它的传参方式比较特殊,一共有两个位置可以传递- 在
bind
的同时,以参数列表的形式进行传递 - 在调用的时候,以参数列表的形式进行传递
- 在
- 那到底以谁
bind
的时候传递的参数为准呢还是以调用的时候传递的参数为准 - 两者合并
bind
的时候传递的参数和调用的时候传递的参数会合并到一起,传递到函数内部
- 可以用来指定内部
函数的其它成员
-
arguments
- 实参集合
-
length
- 形参的个数
-
name
- 函数的名称
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body></body> <script> function a() { // 打印出来的是一个伪数组 并不是真正的数组 console.log(arguments); // Arguments(5) [10, 20, 30, 40, 50, callee: ƒ, Symbol(Symbol.iterator): ƒ] console.log(arguments instanceof Array); // false } a(10, 20, 30, 40, 50); console.log(a.length); // 5 console.log(a.name); var foo = "Foo"; var obj = { [foo]: function () { console.log("123456"); }, }; obj[foo](); // 123456 console.log(obj[foo].name); // Foo </script> </html>
高阶函数
-
函数可以作为参数
-
函数可以作为返回值
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body></body> <script> // 高阶函数 // 第一种 参数是一个函数 map filter reduce forEach var a = [1, 2, 3]; var b = a.map(function (item, index, arr) { return item * 2; }); console.log(b); var c = a.filter(function (item, index, arr) { return item == 3; }); console.log(c); var sum = a.reduce(function (pre, item, index, arr) { return pre + item; }, 0); console.log(sum); // 6 // 第二种 返回值是一个函数 function foo() { return function () { console.log(10); }; } // var f = foo(); // f(); foo()(); </script> </html>
函数闭包
-
一个函数和对其周围状态的引用捆绑在一起(或者说函数被引用包围),这样的组合就是
闭包
(closure
) -
也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在
JavaScript
中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body></body> <script> function foo() { var counter = 10; return { printCounter: function () { console.log(counter); // 10 }, }; } // 调用函数 返回一个对象 // var obj = foo(); // 对象调方法 // obj.printCounter(); // 链式调用 foo().printCounter(); </script> </html>
作用域
-
全局作用域
-
函数作用域
-
没有块级作用域
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body></body> <script> // 代码块 { var a = 10; // 全局作用域 } console.log(a); // 10 // 函数作用域 // function foo() { // console.log(a); // 10 // } // foo(); // 立即调用 (function foo() { console.log(a); // 10 })(); </script> </html>
作用域链
-
内层作用域可以访问外层作用域,反之不行
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body></body> <script> var a = 10; function fn() { var b = 20; function fn1() { var c = 30; console.log(a + b + c); // 60 } function fn2() { var d = 40; console.log(c + d); // 报错 } fn1(); fn2(); } fn(); </script> </html>