JS高级
1. 两大编程思想
1.1. 面向过程
面向过程:POP(Process-oriented programming)
- 面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候再一个一个的依次调用就可以了。
- 示例:把大象放到冰箱,面向过程的做法:
-
- 打开冰箱—> 2. 放入大象—> 3. 关上冰箱
-
1.2. 面向对象
面向对象:OOP (Object Oriented Programming)
-
面向对象是把事务分解成为一个个对象,然后由对象之间分工与合作。
-
面向对象三大特性
封装性: 封装就是把抽象出来的数据和对数据的操作封装成一个类. (把对象功能封装成一个函数方法,用时直接调用即可。)
继承性: 对象b可以继承对象a中的属性和方法。可以减少代码冗余 。
多态性: 多个形态,对象之间的各个功能互不影响。
1.3. 面向过程与面向对象对比
面向过程 | 面向对象 | |
---|---|---|
优点 | 性能比面向对象高,适合跟硬件联系很紧密的东西,例如单片机就采用的面向过程编程。适用于小项目。 | 易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护。适用于多人合作大项目。 |
缺点 | 不易维护、不易复用、不易扩展 | 性能比面向过程低 |
简单程序用面向过程,复杂程序用面向对象.
2. ES6中的类
类抽象了对象的公共部分,它泛指某一大类(class),抽取,把对象的属性和行为封装成一个类
对象特指某一个,通过类实例化一个具体的对象,类中的具体的某个实例(属性和方法的集合体)
面向对象的思维特点:
1.抽取(抽象)对象共用的属性和方法组织(封装)成一个类(模板)
2.对类进行实例化, 获取类的对象
2.1. ES6中的类
-
在ES6中新增加了类的概念,可以使用class关键字声明一个类,之后以这个类来实例化对象。
-
【构造函数实例化对象】
2.2. 创建类
// 语法:class 类名 {属性和方法}【构造函数语法糖】
class Person {}
// 注意类名首字母大写
// 类要抽取公共属性方法,定义一个类
// 1. 创建类class,创建一个明星类
class Star {
// 类的共有属性放到 constructor 里面
constructor(name, age) {
this.name = name;
this.age = age;
}
}
// 2. 利用类创建对象 new实例化对象
var ldh = new Star('刘德华', 18);
// 类就是构造函数的语法糖
2.3. 类的constructor构造函数
class Star {
constructor (uname,age){
this.uname = uname;
this.age = age;
}
}
// 属性:放到constructor,构造函数里面
// 注意:类里面的方法不带function,直接写既可
// 类里面要有属性方法,属性方法要是想放到类里面,用constructor构造器
// 构造函数作用:接收参数,返回实例对象,new的时候主动执行,主要放一些公共的属性
constructor() 方法是类的构造函数(默认方法),用于传递参数,返回实例对象,通过new命令生成对象实例时,自动调用该方法。
注意:每个类里面一定有构造函数,如果没有显示定义, 类内部会自动创建一个constructor() ,
注意:this代表当前实例化对象,谁new就代表谁
2.4. 类创建添加方法
class Star {
constructor () {}
sing () {}
tiao () {}
}
// 注意:方法和方法之间不能加逗号
class 类名 {
constructor(){}
方法名(){}
}
// 注意:类中定义属性,调用方法都得用this
// this.属性 this.方法()
- 注意:方法之间不能加逗号分隔,同时方法不需要添加function 关键字
总结:类有对象的公共属性和方法,用class创建,class里面包含constructor和方法,把公共属性放到constructor里面,把公共方法直接往后写既可,但是注意不要加逗号。
2.5. 类的继承extends
// 父类
class Father{
}
// 子类继承父类
class Son extends Father {
}
class Father {
constructor(surname) {
this.surname= surname;
}
say() {
console.log('你的姓是' + this.surname);
}
}
class Son extends Father{ // 子类继承了父类的属性和方法
}
var damao= new Son('刘');
damao.say(); //结果为 你的姓是刘
2.6. 子类使用super关键字访问父类的方法
开发过程中会遇到父类子类都有的属性,此时,没必要再写一次,可以直接调用父类的方法就可以了
- super关键字用于访问和调用对象父类上的函数。
- 可以调用父类的构造函数,也可以调用父类的普通函数。
// 当子类没有constructor的时候,可以随意用父类的。
// 但是如果子类也含有constructor的话,constructor会返回实例,this的指向不同,
// 不可以再直接使用父类的东西,可以使用(super)来调用父类的方法(普通方法,构造方法)。
调用父类构造函数
// 定义父类
class Father {
constructor(x, y) {
this.x = x;
this.y = y;
}
sum(){
console.log(this.x + this.y);
}
}
// 子元素继承父类
class Son extends Father{
constructor(x, y){
super(x, y); // 使用super调用了父类中的构造函数
}
}
var son = new Son(1, 2);
son.sum(); // 结果为3
调用父类普通函数
// 定义父类
class Father {
constructor (uname, age){
this.uname = uname;
this.age = age;
}
qian (){
console.log('一个亿');
}
}
// 子元素继承父类
class Son extends Father{
// 再写自己的构造函数
constructor (uname, age, score){
// 实例对象:子类的实例对象
// 如果子类有自己的构造函数,同时还想继承父类的构造函数,要用super
// 子类在构造函数中使用super, 必须放到this 前面
// (必须先调用父类的构造方法,再使用子类构造方法)
super(uname, age);
this.score = score;
}
qian () {
// super:调用父类的方法
super.qian();
console.log('2毛钱');
}
}
var obj = new Son('儿子', 16, 99);
console.log(obj);
obj.qian();
// 注意:如果子类也有相同的方法,优先指向子类,就近原则
- 总结:super调用父类的属性和方法,查找方法的原则是(就近原则)
属性和方法:
属性:如果子类既想有自己的属性,又想继承父类的属性,那么用super【super(参数,参数)】。
方法:如果子类和父类有相同的方法,假如子类依旧想用父类的方法,那么用super调用【super.方法()】。
如果子类不写东西,那么直接继承父类就可以用 。
如果子类有自己的构造函数和父类同名的方法,此时不可以直接用父类的东西,需要用super调用父类的方法和构造函数。
注意:
- 继承中,如果实例化子类输出一个方法,先看子类有没有这个方法,如果有就先执行子类的
- 继承中,如果子类里面没有,就去查找父类有没有这个方法,如果有,就执行父类的这个方法(就近原则)
- 如果子类想要继承父类的方法,同时在自己内部扩展自己的方法,利用super调用父类的构造函数,super 必须在子类this之前调用
// 父类有加法方法
class Father {
constructor(x, y) {
this.x = x;
this.y = y;
}
sum() {
console.log(this.x + this.y);
}
}
// 子类继承父类加法方法 同时 扩展减法方法
class Son extends Father {
constructor(x, y) {
// 利用super 调用父类的构造函数 super 必须在子类this之前调用,放到this之后会报错
super(x, y);
this.x = x;
this.y = y;
}
subtract() {
console.log(this.x - this.y);
}
}
var son = new Son(5, 3);
son.subtract(); // 2
son.sum(); // 8
2.7. 三个注意点
- 在ES6中,类没有变量提升,所以必须先定义类,才能通过类实例化对象。
- 类里面的共有属性和方法,一定要加this使用.【this,对象调用属性和方法】。
- 类里面的this指向问题:
- constructor 里面的this指向实例对象。
- 方法里面的this指向这个方法的调用者。
2.8. 类里面的this指向
- 构造函数的this指向实例对象。
- 普通函数的this是调用者,谁调用this是谁。
<body>
<input type="button" value="点击1" id="btn1">
<input type="button" value="点击2" id="btn2">
<script type="text/javascript">
class Btn {
constructor (id) {
this.id = document.querySelector(id);
// 点击按钮调用cli
// 添加:点击调用方法,注意不要给这个方法加括号,加括号会直接执行方法
// this.id ,this.cli;
this.id.onclick = this.cli;
// console.log(this);
}
cli () {
// console.log('这是一个方法');
console.log(this);
}
}
var obj = new Btn('#btn1');
obj.cli();
</script>
</body>
3. 对象
- 对象是由属性和方法组成的:是一个无序键值对的集合,指的是一个具体的事物。
- 对象:类中的具体的某个实例【属性和方法的集合体】
- 属性:事物的特征,在对象中用属性来表示(常用名词)。
- 方法:事物的行为,在对象中用方法来表示(常用动词)。
3.1. 创建对象
- 在ES5中创建对象可以通过以下三种方式:
1.对象字面量创建对象
var obj = {
// 属性和方法
// 键值对
// 对象:成员
uname : '阿飞',
age : 22,
sex : '男',
fei : function () {
console.log('1');
}
};
// 访问属性和方法
console.log( obj.uname );
console.log( obj['age'] );
obj.fei();
// 遍历对象
for ( var key in obj ) {
// 遍历对象的时候,千万不要用对象.属性
console.log( obj[key] );
}
2.new Object()【构造函数】
var obj = new Object();
// instanceof -----------------判断是不是属于什么数据类型
// arr instanceof Array
// console.log(obj instanceof Object);
// 添加属性
obj.uname = '张三丰';
obj.age = 22;
obj.sex = '男';
obj.taiji = function () {
console.log('太极');
}
3.自定义构造函数
function Person (uname, age) {
// 属性
this.uname = uname;
this.age = age;
// 方法
this.say = function () {
console.log('说话');
}
}
var n = new Person('阿飞', 22);
console.log(n.uname);
console.log(n.age);
n.say();
4. 构造函数和原型
-
在典型的面向对象(OOP)的语言中(如Java),都存在类的概念,类就是对象的模板,对象就是类的实例,但在ES6之前,JS 中并没用引入类的概念。
-
ES6,全称ECMAScript6.0 ,2015.06 发布。但是目前浏览器的JavaScript 是ES5 版本,大多数高版本的浏览器也支持ES6,不过只实现了ES6 的部分特性和功能。
-
在ES6之前,对象不是基于类创建的,而是用一种称为构造函数的特殊函数来定义对象和它们的特征。
4.1. 定义构造函数
- 构造函数是一种特殊的函数,主要用来初始化对象,即为对象成员变量赋初始值,它总与new一起使用。
- 可以把对象中一些公共的属性和方法抽取出来,然后封装到这个函数里面。
function Fn () {}
在JS 中,使用构造函数时要注意以下两点:
- 构造函数用于创建某一类对象,其首字母要大写。
- 构造函数要和new 一起使用才有意义。
function Fn (uname, age) {
this.uname = uname;
this.age = age;
}
var fn = new Fn('阿飞',22);
4.2. new的执行过程
new在执行时会做四件事情:
- 在内存中创建一个新的空对象。
- 让this指向这个新的对象。
- 执行构造函数里面的代码,给这个新对象添加属性和方法。
- 返回这个新对象(所以构造函数里面不需要return)。
4.3. 静态成员和实例成员
-
静态成员:在构造函数本身上添加的成员称为静态成员,只能由构造函数本身来访问。
-
实例成员:在构造函数内部this上创建的对象成员称为实例成员,只能由实例化的对象来访问。
function Person (uname, age) {
// 实例成员
this.uname = uname;
this.age = age;
}
// 静态成员
Person.sex = '男';
var ldh = new Person('刘德华', 23);
// console.log( ldh.sex ); ---->undefined
console.log( Person.sex ); // 静态成员只能由构造函数访问
console.log( ldh.uname ); // 实例成员只能由实例对象访问
4.4. 构造函数存在小问题
当实例化对象的时候,属性好理解,属性名属性值,那么方法是函数,函数是复杂数据类型
那么保存的时候是保存地址,又指向函数,而每创建一个对象,都会有一个函数,
每个函数都得开辟一个内存空间,此时浪费内存了。如果多次实例化,会浪费内存。
那么如何节省内存呢,我们需要用到原型方法放到构造函数里面。
function Star (uname, age) {
this.uname = uname;
this.age = age;
this.sing = function () {
console.log(this.name + '在唱歌');
}
}
var zxc = new Star('周星驰', 22);
var ldh = new Star('刘德华', 22);
![](https://jiyaning.github.io/staticImgs/images/内存.jpg)
4.5. 构造函数原型对象prototype
- 原型对象:就是一个属性,是构造函数的属性,这个属性是一个对象,也称呼prototype 为原型对象。
- 每一个构造函数都有一个属性,prototype
- 作用:是为了共享方法,从而达到节省内存
- 构造函数通过原型分配的函数是所有对象所共享的。
JavaScript 规定,每一个构造函数都有一个prototype 属性,指向另一个对象(通过调用构造函数而创建的实例的原型)。
这个prototype 就是一个对象,对象的所有属性和方法,都会被构造函数所拥有。
把公共的方法,直接定义在prototype 对象上,这样所有对象的实例就可以共享这些方法。
function Star (uname, age) {
this.uname = uname;
this.age = age;
// this.sing = function () {
// console.log(this.name + '在唱歌');
// }
}
Star.prototype.sing = function () {
console.log(this.uname + '在唱歌');
}
var zxc = new Star('周星驰', 22);
var ldh = new Star('刘德华', 22);
// console.log( Star.prototype );
ldh.sing();
zxc.sing();
总结:所有的公共属性写到构造函数里面,所有的公共方法写到原型对象prototype里面
疑问:为何创建一个实例对象,都可以自动的跑到原型对象上找方法?
因为每一个实例对象都有一个属性__proto__
,对象原型,指向原型对象。
4.6. 对象原型:__proto__
- 对象原型:是实例对象的一个属性,每一个对象一个属性,指向原型对象。
- 主要作用:是为了指向原型对象(prototype)。
每个实例化对象都会有一个属性
__proto__
指向构造函数的prototype 原型对象。之所以对象可以使用构造函数prototype 原型对象的属性和方法,就是因为对象有
__proto__
原型的存在。
__proto__
原型是一个非标准属性,不可以拿来赋值或者设置,(只读属性)。
1.
__proto__
对象原型和原型对象prototype 是等价的。
2.
__proto__
对象原型的意义在于为对象的查找机制提供一个方向,或者说一条路线,但是它是一个非标准属性,因此实际开发中,不可以使用这个属性,它只是内部指向原型对象prototype。
![](https://jiyaning.github.io/staticImgs/images/关系图.jpg)
总结:每一个对象都有一个原型,作用是指向原型对象prototype。
统一称呼:__proto__
为原型,prototype为原型对象。
4.7. constructor构造函数
- 记录是哪个构造函数创建出来的。
- constructor是一个属性:原型对象的一个属性。
- 作用:指回构造函数本身。
- 原型(
__proto__
)和构造函数(prototype)原型对象,里面都有一个属性constructor,称为构造函数,因为它指回构造函数本身。 - constructor 主要用于记录该对象引用于哪个构造函数,它可以让原型对象重新指向原来的构造函数。
- 一般情况下,对象的方法都在构造函数的原型对象中设置。如果有多个对象的方法,可以给原型对象采取对象形式赋值,但是这样就会覆盖构造函数原型对象原来的内容,这样修改后的原型对象constructor 就不再指向当前构造函数了。此时,可以在修改后的原型对象中,添加一个constructor 指向原来的构造函数。
// 给原型对象传入对象添加方法
Star.prototype = {
sing : function () {},
dance: function () {}
};
// 此时会覆盖原先prototype中的内容,传入一个新的对象,那么此时就不知道构造函数是哪个了
// 所以我们要指回构造函数:constructor:构造函数
function Star (uname, age) {
this.uname = uname;
this.age = age;
}
// 原型对象:方法
// Star.prototype.chang = function () {
// console.log('唱歌');
// }
// Star.prototype.tiao = function () {
// console.log('跳舞');
// }
Star.prototype = {
constructor : Star,
chang : function () {},
tiao : function () {},
rap : function () {}
}
// 实例对象
var obj = new Star('阿飞', 23);
console.log(Star.prototype.constructor);
console.log( Star.prototype );
</script>
总结:constructor 主要作用是可以指回原来的构造函数本身。
构造函数、对象实例、原型对象三者之间的关系
![](https://jiyaning.github.io/staticImgs/images/构造函数,原型对象,对象实例关系.jpg)
5. 原型链
- 原型链:由原型
__proto__
所串起来的一个链式。 - 作用:提供一个成员的查找机制,或者查找规则(查找方向)。
![](https://jiyaning.github.io/staticImgs/images/原型链.jpg)
5.1. JavaScript 的成员查找机制
- 当访问一个对象的属性(方法)时,首先查找这个对象自身有没有该属性(方法)。
- 如果没有就查找它的原型(也就是
__proto__
指向的prototype 原型对象)。 - 如果还没有就查找原型对象的原型(Object的原型对象)。
- 依此类推一直找到Object 为止(null)。
__proto__对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线。
console.log(Star.prototype.__proto__.__proto__);
console.log(Object.prototype);
5.2. 扩展内置对象
- 可以通过原型对象prototype ,对原来的内置对象进行扩展自定义的方法。
- 比如给数组增加自定义求和的功能:
console.log( Array.prototype );
// 添加求和方法
Array.prototype.sum = function () {
var sum = 0;
for (var i = 0; i < this.length; i++) {
sum += this[i];
}
return sum;
}
var arr = [1,2,3];
console.log( arr.sum() );
var newArr = [6,7,8,9];
console.log( newArr.sum() );
6. 继承-组合继承
- ES6之前并没有提供extends继承。可以通过
构造函数+原型对象
模拟实现继承,被称为组合继承。
call()
调用这个函数, 并且修改函数运行时的this 指向
fun.call(thisArg, arg1, arg2, ...);
// call把父类的this指向子类
// thisArg :当前调用函数this 的指向对象
// arg1,arg2:传递的其他参数
6.1. 属性继承
-
组合继承:利用构造函数和原型对象实现子类的继承。
-
实现方法:把父类的this指向子类的this既可,调用父类函数,用call改变this指向。
function Father (uname,age) {
// this指向父类的实例对象
this.uname = uname;
this.age = age;
// 只要把父类的this指向子类的this既可
}
function Son (uname, age,score) {
// this指向子类构造函数
// this.uname = uname;
// this.age = age;
// Father(uname,age);
Father.call(this,uname,age);
this.score = score;
}
Son.prototype.sing = function () {
console.log(this.uname + '唱歌')
}
var obj = new Son('刘德华',22,99);
console.log(obj.uname);
console.log(obj.score);
obj.sing();
6.2. 方法继承
- 实现方法:把父类的实例对象赋值给子类的原型对象,用contructor指回子类构造函数本身。
一般情况下,对象的方法都在构造函数的原型对象中设置,通过构造函数无法继承父类方法。
核心原理:将子类所共享的方法提取出来,让子类的prototype 原型对象= new 父类() 。
本质:子类原型对象等于是实例化父类,因为父类实例化之后另外开辟空间,就不会影响原来父类原型对象。
将子类的constructor 指回子类构造函数本身。
function Father () {
}
Father.prototype.chang = function () {
console.log('唱歌');
}
function Son () {
}
// Son.prototype = Father.prototype;通过构造函数无法继承父类方法。
Son.prototype = new Father();
var obj = new Son();
obj.chang();
Son.prototype.score = function () {
console.log('考试');
}
// obj.score();
// console.log(Son.prototype);
console.log(Father.prototype);
注意:一定要让Son指回构造函数
// 实现继承后,让Son指回原构造函数
Son.prototype = new Father();
Son.prototype.constructor = Son;
总结:用构造函数实现属性继承,用原型对象实现方法继承。
7. 类的本质
- ES6类class的本质还是function。
- 类的所有方法都定义在类的prototype属性上。
- 类创建的实例,里面也有
__proto__
原型,指向类的prototype原型对象。 - 所以ES6的类的绝大部分功能,ES5都可以做到。
- 新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
- 所以ES6的类其实就是语法糖。
语法糖:就是一种便捷写法,简单理解 。
有两种方法可以实现同样的功能, 但是一种写法更加清晰、方便,那么这个方法就是语法糖。
class Star {}
console.log( typeof Star );
var obj = new Star();
console.log(obj.__proto__);
console.log(Star.prototype);
8. ES5 中新增方法
- ES5 中新增了一些方法,可以很方便的操作数组或者字符串:
- 数组方法: 迭代(遍历)方法: forEach() filter() some() map() every()
- 字符串方法: trim()
8.1. forEach()-数组遍历
array.forEach(function(value, index, arr){
})
// value:数组当前项的值
// index:数组当前项的索引(下标)
// arr:数组对象本身
var arr = ['red','blue','yellow','orange'];
arr.forEach(function (elm,i,arrAbc) {
console.log(elm,i,arrAbc);
});
8.2. filter()-数组筛选(过滤)
- filter() 方法创建一个新的数组,新数组中的元素是通过筛选指定数组中符合条件的所有元素 。
- 主要用于筛选数组。
- 注意它直接返回一个新数组。
- filter() 不会对空数组进行筛选。
- filter() 不会改变原始数组。
- 如果没有符合条件的元素则返回空数组。
array.filter(function(value, index, arr){
})
// 数组中的每个元素都会执行这个函数
// value: 数组当前项的值
// index:数组当前项的索引
// arr:当前元素属于的数组对象
var arr = [100,66,99,123,333,33,44,66];
var reArr = arr.filter(function (elm, a, n) {
// console.log(elm,a, n);
return elm % 2 == 0;
});
console.log(reArr);
8.3. some()-数组查找
- some() 方法用于检测数组中的元素是否满足指定条件. 通俗点查找数组中是否有满足条件的元素。
- 注意它返回值是布尔值, 如果查找到这个元素, 就返回true , 如果查找不到就返回false。
- 如果找到第一个满足条件的元素,则终止循环. 不在继续查找。
array.some(function(value, index, arr){
})
// value: 数组当前项的值
// index:数组当前项的索引
// arr:数组对象本身
var arr = [100,200,300,400];
var re = arr.some(function (elm,i,arr) {
// console.log(elm,i,arr);
console.log(i);
return elm >= 200;
});
console.log(re);
8.4. map()-数组映射
- map() 方法返回一个新数组,数组中的元素为原始数组元素调用函数处理后的值。
- map() 方法按照原始数组元素顺序依次处理元素。基本用法跟forEach()方法类似
- 注意:
- map() 不会对空数组进行检测。
- map() 不会改变原始数组。
var newArray=array.map(function(value, index, arr){
})
// value:数组当前项的值
// index:数组当前项的索引(下标)
// arr:数组对象本身
// 数值项求平方
var data = [1, 2, 3, 4];
var arrayOfSquares = data.map(function (item) {
return item * item;
});
alert(arrayOfSquares); // 1, 4, 9, 16
8.5. every()
- every() 方法用于检测数组所有元素是否都符合指定条件(通过函数提供)。
- every() 方法使用指定函数检测数组中的所有元素:
- 如果数组中检测到有一个元素不满足,则整个表达式返回 false ,且剩余的元素不会再进行检测。
- 如果所有元素都满足条件,则返回 true。
- 注意:
- every() 不会对空数组进行检测。
- every() 不会改变原始数组。
var res = array.every(function(value,index,arr){
})
// value:数组当前项的值
// index:数组当前项的索引(下标)
// arr:数组对象本身
var arr = [ 1, 2, 3, 4, 5, 6 ];
console.log( arr.every( function( item, index, array ){
console.log( 'item=' + item + ',index='+index+',array='+array );
return item > 3;
}));
8.6. trim()-删除字符串两侧空白符
var str = ' hello '
console.log(str.trim()) // hello 删除字符串两侧的空白符
var str1 = ' he l l o '
console.log(str.trim()) // he l l o 删除字符串两侧的空白符
8.7. padstart()-字符串补位
var newStr = oldStr.padStart(len, str)
// 根据给定长度自动在字符串的前面补充想补充的字符串(只返回修改后的字符串,不修改原字符串)
// oldStr 要补充的字符串
// len 给定的长度,转换后
// str 想补充的字符串
var a = '7';
var a1 = a.padStart(2, '0');
console.log(a) // 7
console.log(a1) // 07
padEnd()
和padStart()参数用法一样,只是把想加的字符串加到后头。
insertAdjacentHTML 将指定的文本解析为HTML或XML,并将结果节点插入到DOM树中的指定位置
element.insertAdjacentHTML(position, text);
position是相对于元素的位置,并且必须是以下字符串之一:
beforebegin: 元素自身的前面。
afterbegin: 插入元素内部的第一个子节点之前。
beforeend: 插入元素内部的最后一个子节点之后。
afterend: 元素自身的后面。
text是要被解析为HTML或XML,并插入到DOM树中的字符串。
9. 函数的定义和调用
9.1. 函数的定义
1.函数声明方式 function 关键字 (命名函数)
function fn(){
console.log(this);
}
fn();
2.函数表达式(匿名函数、自调用函数)
// 匿名函数
var fun = function () {
console.log('哇哈哈');
}
fun();
// 自调用函数
(function (a, b) {
console.log('内容');
console.log(a,b);
})('aaa', 'bbb');
3.创建对象 new Function()
var f = new Function('a', 'b', 'console.log(a + b)');
f(1, 2);
var fn = new Function('参数1','参数2'..., '函数体')
// 注意:
// Function 里面参数都必须是字符串格式
// 第三种方式执行效率低,也不方便书写,因此较少使用
// 所有函数都是 Function 的实例(对象)
// 函数也属于对象
9.2. 函数的调用
// 普通函数
function fn () {}
fn();
// 对象的方法
var obj = {
taiji : function () {}
}
obj.taiji();
// 构造函数
function Person () {}
new Person();
// 绑定事件函数
btn.onclick = function () {}
// 定时器函数
window.setInterval(function () {},1000);
// 自调用函数(立即执行函数)
(function () {})();
9.3. 函数内部的this指向
这些 this 的指向,是当我们调用函数的时候确定的。调用方式的不同决定了this 的指向不同。
一般指向我们的调用者:
调用方式 | this指向 |
---|---|
普通函数调用 | window |
构造函数调用 | 实例对象,原型对象prototype里面的方法也指向实例对象 |
对象方法调用 | 该方法所属对象 |
事件绑定方法 | 绑定事件对象 |
定时器函数 | window |
立即执行函数 | window |
9.4. 改变函数内部 this 指向
JavaScript 专门提供了一些函数方法来更优雅的处理函数内部this的指向问题:
常用的有call()、apply()、bind() 三种方法。
9.4.1. call()方法
- call()方法调用一个对象。简单理解为调用函数的方式,但是它可以改变函数的 this 指向。
- 应用场景: 经常做继承.
fun.call(thisArg, arg1, arg2, ...)
// thisArg:在fun 函数运行时指定的this 值
// arg1,arg2:传递的其他参数
// 返回值就是函数的返回值,因为它就是调用函数
function Father () {
this
}
function Son () {
Father.call(this,1,2)
}
// 因此当想改变this 指向,同时想调用这个函数的时候,可以使用call,比如继承
示例:
function fn (a, b) {
console.log(this, a, b);
}
var obj = {name : '阿飞', age : 22};
fn();
fn.call(obj, 123, 456);
var obj = {
name : '阿飞',
age : 22,
fei : function () {
console.log(this);
}
}
var objN = {name : '带土',age : 22};
obj.fei();
console.log( obj.fei );
obj.fei.call(objN);
9.4.2. apply()方法
-
apply() 方法调用一个函数。简单理解为调用函数的方式,但是它可以改变函数的 this 指向。
-
应用场景: 经常跟数组有关系
fun.apply(thisArg, [argsArray]):调用函数
// thisArg:在fun函数运行时指定的this值
// argsArray:传递的值,必须包含在数组里面
// 返回值就是函数的返回值,因为它就是调用函数
// 因此apply 主要跟数组有关系,比如使用Math.max() 求数组的最大值
var arr = [1,2,3,66,44,7];
var n = Math.max.apply(arr, arr);
console.log(n);
示例:
var obj = {
name: 'andy'
}
function fn(a, b) {
console.log(this);
console.log(a+b)
};
fn() //此时的this指向的是window a+b运行结果为NaN
fn.apply(obj,[1,2]) //此时的this指向的是对象obj,参数使用数组传递,运行结果为3
9.4.3. bind()方法
-
bind() 方法不会调用函数,但是能改变函数内部this 指向,返回的是原函数改变this之后产生的新函数
-
如果只是想改变 this 指向,并且不想调用这个函数的时候,可以使用bind
-
应用场景:不调用函数,但是还想改变this指向
var obj = {
name: 'andy'
};
function fn(a, b) {
console.log(this);
console.log(a + b);
};
var f = fn.bind(obj, 1, 2); //此处的f是bind返回的新函数
f();//调用新函数 this指向的是对象obj 参数使用逗号隔开
9.4.4. call、apply、bind三者的异同
- 共同点 : 都可以改变this指向
- 不同点:
- call 和 apply 会调用函数, 并且改变函数内部this指向.
- call 和 apply传递的参数不一样,call传递参数使用逗号隔开,apply使用数组传递
- bind 不会调用函数, 可以改变函数内部this指向.
- 应用场景
- call 经常做继承.
- apply 经常跟数组有关系. 比如借助于数学对象实现数组最大值最小值
- bind 不调用函数,但是还想改变this指向. 比如改变定时器内部的this指向.
10. 严格模式-strict
10.1. 什么是严格模式
JavaScript 除了提供正常模式外,还提供了严格模式(strict mode)。
ES5 的严格模式是采用具有限制性 JavaScript变体的一种方式,即在严格的条件下运行 JS 代码。
严格模式在 IE10 以上版本的浏览器中才会被支持,旧版本浏览器中会被忽略。
- 严格模式对正常的 JavaScript 语义做了一些更改:
- 1.消除了 Javascript 语法的一些不合理、不严谨之处,减少了一些怪异行为。
- 2.消除代码运行的一些不安全之处,保证代码运行的安全。
- 3.提高编译器效率,增加运行速度。
- 4.禁用了在 ECMAScript 的未来版本中可能会定义的一些语法,为未来新版本的 Javascript 做好铺垫。比如一些保留字如:class,enum,export, extends, import, super 不能做变量名
10.2. 开启严格模式
严格模式可以应用到整个脚本或个别函数中。因此在使用时,可以将严格模式分为为脚本开启严格模式和为函数开启严格模式两种情况。
为脚本开启严格模式
-
有的 script 脚本是严格模式,有的 script 脚本是正常模式,这样不利于文件合并,所以可以将整个脚本文件放在一个立即执行的匿名函数之中。这样独立创建一个作用域而不影响其他script 脚本文件。
(function (){ //在当前的这个自调用函数中有开启严格模式,当前函数之外还是普通模式 "use strict"; var num = 10; function fn() {} })(); //或者 <script> "use strict"; //当前script标签开启了严格模式 </script> <script> //当前script标签未开启严格模式 </script>
为函数开启严格模式
-
要给某个函数开启严格模式,需要把“use strict”; (或 ‘use strict’; ) 声明放在函数体所有语句之前。
function fn(){ "use strict"; return "123"; } //当前fn函数开启了严格模式
10.3. 严格模式中的变化
严格模式对 Javascript 的语法和行为,都做了一些改变。
变量规定: 变量申明必须加var,而且不准删除变量
在正常模式中,如果一个变量没有声明就赋值,默认是全局变量。
严格模式禁止这种用法,变量都必须先用var命令声明,然后再使用。
'use strict'
num = 10
console.log(num)
//严格模式后使用未声明的变量--Uncaught ReferenceError: n is not defined
严禁删除已经声明变量。例如,delete x; 语法是错误的。
var num2 = 1;
delete num2; //严格模式不允许删除变量
严格模式下this 指向问题
- 正常模式中全局作用域函数中的this 指向window 对象。
- 严格模式下全局作用域中函数中的this是undefined。
function fn() {
console.log(this); //严格模式下全局作用域中函数中的this指向是 undefined
}
fn();
- 以前构造函数时不加new也可以调用,当普通函数,this 指向全局对象。
- 严格模式下,如果构造函数不加new调用, this 指向的是undefined ,如果给this赋值则会报错。
- new 实例化的构造函数指向创建的对象实例。
- 定时器this 还是指向window 。
- 事件、对象还是指向调用者。
function Star() {
this.sex = '男';
}
// Star();严格模式下,如果构造函数不加new调用,
// this 指向的是undefined 如果给this赋值则会报错.
var ldh = new Star()
console.log(ldh.sex);
setTimeout(function() {
console.log(this); // 定时器 this 还是指向 window
}, 2000);
函数变化:参数不能重名
function fn (a, a) {
// var a = 2;
console.log(a+a);
}
fn(1,2); // 正常模式中,结果是3
fn(1,2); // 严格模式 Uncaught SyntaxError:
// Duplicate parameter name not allowed in this context
函数必须声明在顶层
- 新版本的JavaScript 会引入“块级作用域”(ES6 中已引入)。为了与新版本接轨,不允许在非函数的代码块内声明函数。
更多严格模式要求参考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Strict_mode
11. 高阶函数
高阶函数是对其他函数进行操作的函数,它接收函数作为参数或将函数作为返回值输出。
把函数作为参数,或者把函数作为返回值的的函数,称为高阶函数。
// 把函数当做参数的函数,称为高阶函数,
function fn (n) { // 此时fn就是一个高阶函数
console.log(n);
n && n();
}
var num = function () {console.log('哇哈哈')};
fn(num);
//把函数当做返回值的函数,也叫高阶函数
function fun () {
var n = function () {console.log('内容')};
return n;
}
var re = fun();
console.log(re);
re();
函数也是一种数据类型,同样可以作为参数,传递给另外一个参数使用。最典型的就是作为回调函数。
同理,函数也可以作为返回值传递回来。
12. 闭包
12.1. 变量作用域
变量根据作用域的不同分为两种:全局变量和局部变量。
- 函数内部可以使用全局变量。
- 函数外部不可以使用局部变量。
- 当函数执行完毕,本作用域内的局部变量会销毁。
12.2. 什么是闭包
闭包(closure)指有权访问另一个函数作用域中变量的函数。
简单理解就是 ,一个作用域可以访问另外一个函数内部的局部变量。
<script>
function fn1(){ // fn1就是闭包函数
var num = 10 ;
function fn2(){
console.log(num); // 10
}
fn2();
}
fn1();
</script>
12.3. 闭包的作用
作用:延伸变量的作用范围。
function fn() {
var num = 10;
function fun() {
console.log(num);
}
return fun;
}
var f = fn();
f();
12.4. 闭包的案例
- 利用闭包的方式得到当前li 的索引号
// 以前:
var lis = document.querySelectorAll('li');
for (var i = 0; i < lis.length; i++) {
lis[i].index = i; //设置自定义属性保存i
lis[i].onclick = function () {
// console.dir(this); dir,打印所有属性和方法
console.log(this.index);
}
}
// 闭包:
for (var i = 0; i < lis.length; i++) {
// 利用for循环创建了4个立即执行函数
// 立即执行函数也成为小闭包,因为立即执行函数里面的任何一个函数都可以使用它的i变量
(function(index) {
lis[index].onclick = function() {
console.log(index);
}
})(i);
}
- 闭包应用-3秒钟之后,打印所有li元素的内容
for (var i = 0; i < lis.length; i++) {
(function(i) {
setTimeout(function() {
console.log(lis[i].innerHTML);
}, 3000)
})(i);
}
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function() {
return function() {
return this.name;
};
}
};
console.log(object.getNameFunc()()) // 不是闭包
-----------------------------------------------------------------------------------
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function() {
var that = this;
return function() {
return that.name;
};
}
};
console.log(object.getNameFunc()())
13. 递归
13.1. 什么是递归
**递归:**如果一个函数在内部可以调用其本身,那么这个函数就是递归函数。
简单理解:函数内部自己调用自己, 这个函数就是递归函数。
**注意:**递归函数的作用和循环效果一样,由于递归很容易发生“栈溢出”错误(stack overflow),
所以必须要加退出条件return。
13.2. 利用递归求1~n的阶乘
//利用递归函数求1~n的阶乘 1 * 2 * 3 * 4 * ..n
function fn(n) {
if (n == 1) { //结束条件
return 1;
}
return n * fn(n - 1);
}
console.log(fn(3)); // -----6
13.3. 利用递归求斐波那契数列
// 利用递归函数求斐波那契数列(兔子序列) 1、1、2、3、5、8、13、21...
// 用户输入一个数字 n 就可以求出 这个数字对应的兔子序列值
// 只需要知道用户输入的n 的前面两项(n-1 n-2)就可以计算出n 对应的序列值
function fb(n) {
if (n === 1 || n === 2) {
return 1;
}
return fb(n - 1) + fb(n - 2);
}
console.log(fb(3));
13.4. 遍历数据
<script type="text/javascript">
var data = [
{
id : 1,
name : '家电',
goods : [
{
id : 11,
name : '冰箱',
goods : [
{
id : 111,
name : '海尔'
},{
id : 112,
name : '美的'
}
]
},{
id : 12,
name : '洗衣机 '
}
]
},
{
id : 2,
name : '服饰'
}
];
// var data = [{},{}];
// 写一个函数,根据给出得到id去能够获取指定的数据
// 获取数据
function getId (arr, id) {
// 定义一个变量
var obj = {};
// 遍历
arr.forEach(function (val, index) {
if (val.id == id) {
obj = val;
} else if (val.goods && val.goods.length > 0) {
obj = getId(val.goods, id);
}
});
return obj;
}
console.log( getId(data,111) );
</script>
14. 拷贝
- 拷贝不能直接赋值,对象赋值的是地址
//对象是复杂数据类型,直接赋值,赋值的是地址
var obj = {
name : '张三丰',
age : 22
};
var newObj = obj;
obj.name ="李寻欢";
console.log(newObj.name); ----->李寻欢
14.1. 浅拷贝assign()
浅拷贝: 只拷贝对象最外面一层
es6:新方法
Object.assign(target, sources);
<script type="text/javascript">
var obj = {
name : '张三丰',
age : 22,
color : ['red','blue','yellow'],
message : {sex : '男',score : 99}
};
var newObj = {};
// 遍历
for (var key in obj ) {
newObj[key] = obj[key];
}
// obj有什么,newObj就有什么
// newObj.name = obj.name;
// newObj.age = obj.age;
//es6:新方法assign()
Object.assign(newObj, obj);
obj.name = '李寻欢';
console.log(newObj.name); ----->张三丰
obj.message.sex = '女';
console.log(newObj.message.sex); ----->女
</script>
14.2. 深拷贝
数据对象里面的所有层次都拷贝
<script type="text/javascript">
var obj = {
name : '张三丰',
age : 22,
color : ['red','blue','yellow'],
message : {sex : '男',score : 99}
};
var newObj = {};
// 封装
function kaobei (newObj, obj) {
// 要把obj拷贝给newObj
// 发现,obj[key]可能是数组,还能是对象,具有复杂数据
// 那么此时直接拷贝,又会变成复杂数据类型传递
for (var key in obj ) {
// newObj[key] = obj[key];
if ( obj[key] instanceof Array ) { // obj[key]是数组,就再遍历
// newObj[key] = obj[key]
newObj[key] = [];
kaobei(newObj[key], obj[key]);
} else if ( obj[key] instanceof Object ) { // obj[key]是对象,就再遍历
// newObj[key] = obj[key]
newObj[key] = {};
kaobei(newObj[key], obj[key]);
} else {
newObj[key] = obj[key];
}
}
}
kaobei(newObj, obj);
obj.name = 'aaa';
obj.message.sex = '女';
console.log(obj);
console.log(newObj.name); ------->张三丰
console.log(newObj.message.sex); ------->男
</script>