JavaScript面向对象

构造函数和原型

ES5创建对象的方法

  • 在ES6之前,如果想创建对象有三种方式:
    • 通过字面量{}创建对象
    • 利用new Object创建对象
    • 使用构造函数创建对象
// 1. 通过字面量{}创建对象
var obj = {
    uname: 'brokyz',
    agr: 18,
    sex: 'male',
    sayHi: function(){
        console.log('hi');
    }
} 
// 2. 利用new Object创建对象
var obj = new Object();
obj.uname = 'brokyz';
obj.age = 18;
obj.sex = 'male';
obj.sayHi = function(){
    console.log('hi');
}
// 3. ES5使用构造函数创建对象
function Apple(name){
    this.name = name;
    this.sayHi = function(){
        console.log('hi');
    }
}
var iphone = new Apple('iphone12');
console.log(iphone.name);
console.log(iphone['name']);

new在执行时会做四件事

  1. 在内存中创建一个新的空对象。
  2. this指向这个新的对象。
  3. 执行构造函数里面的代码,给这个新的对象添加属性和方法。
  4. 返回这个新的对象(所以构造函数里面不需要return

实例成员和静态成员

  1. 实例成员:在构造函数内部通过this来添加的成员。只能通过实例化的对象来访问。
  2. 静态成员:在构造函数本身上添加的成员。只能通过构造函数来访问。
function Apple(name){
    // 实例成员
    this.name = name;
    this.sayHi = function(){
        console.log('hi');
    }
}
// 实例成员只能通过实例化对象来访问
console.log(new Apple('brokyz').name);
// 静态成员需要在构造函数本身上添加
Apple.age = 15;
// 只能通过构造函数来访问
console.log(Apple.age);

构造函数的资源浪费问题

function Apple(name){
    this.name = name;
    this.sayHi = function(){
        console.log('hi');
    }
}
var apple1 = new Apple('iphone12');
var apple2 = new Apple('iphone13');
console.log(apple1.sayHi === apple2.sayHi);  // false
  • 构造函数中的方法sayHi属于复杂数据类型,创建实例时会在内存中单独开辟空间来存放。
  • 实例化对象之间并不能公用相同的方法。所以实例化对象会分别在内存中开辟空间来存放自己的方法。
  • 比如apple1在内存中有一个自己的sayHi方法。当继续实例化apple2时,构造函数会为其在内存开辟一个新的sayHi方法。

构造函数原型对象prototype

  • 解决了构造函数的资源浪费问题。
  • 构造函数通过原型分配的函数时所有对象共享的
  • 在JS中,每一个构造函数都有一个prototype属性,指向另一个对象。注意这个prototype就是一个对象,这个对象的所有属性和方法,都会被构造函数所拥有。
  • 我们可以把不变的方法,直接定义在prototype上,这样所有对象的实例就可以共享这些方法。这样一来所有的实例化对象,都不用被独自开辟方法,可以共同去ptototype中寻找使用方法。
function Apple(name){
    this.name = name;
}
Apple.prototype.sayHi = function(){
    console.log('hi');
}
var apple1 = new Apple('iphone12');
var apple2 = new Apple('iphone13');
console.log(apple1.sayHi === apple2.sayHi); // true
console.log(apple1._proto_ === Apple.prototype); // true

为什么方法定义在构造函数的prototype上,而实例化对象可以使用

  • 在实例化对象身上还有__proto__属性指向构造函数的prototype原型对象,我们之所以可以使用构造函数prototype原型对象的属性和方法,就时因为对象有_proto_原型的存在。
  • __proto__对象原型原型对象prototype时等价的。

constructor构造函数

  • 对象原型(_proto)和构造函数(prototype)原型对象里面都有一个constructor属性,我们将其称为构造函数,因为他会指回构造函数本身。
  • constructor主要用于记录该对象引用哪个构造函数,它可以让原型对象重新指向原来的构造函数。
function Apple(name){
    this.name = name;
}
Apple.prototype.sayHi = function(){
    console.log('hi');
}
var apple1 = new Apple('iphone12');
var apple2 = new Apple('iphone13');
console.log(Apple.prototype);	// 里面存在constructor属性
console.log(apple1.__proto__);	// 里面存在constructor属性
console.log(Apple.prototype.constructor);	// 指向构造函数本身Apple(name){...}
console.log(apple1.__proto__.constructor);	// 指向构造函数本身Apple(name){...}
  • 我们可以在原型对象中直接赋值添加方法会覆盖掉constructor,这时我们需要手动指会构造函数。
function Apple(name){
    this.name = name;
}
Apple.prototype = {
    // 手动指回
    constructor: Apple,
    sayHi: function() {
        console.log('hi');
    },
    open: function() {
        console.log('open');
    }
}
var apple1 = new Apple('iphone12');
var apple2 = new Apple('iphone13');

console.log(Apple.prototype);	// 如果没有手动指回,里面的constructor被覆盖掉,所以不存在constructor
console.log(apple1.__proto__);	// 如果没有手动指回,里面的constructor被覆盖掉,所以不存在constructor
console.log(Apple.prototype.constructor);
console.log(apple1.__proto__.constructor);

总结:如果我们修改了原来的原型对象,修改时如果以对象的形式来进行赋值的话,必须手动利constructor指向回原来的构造函数。

原型链

JS成员查找机制

  1. 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。
  2. 如果没有就查找它的原型(也就是__proto__指向的prototype原型对象)。
  3. 如果还有没就查找原型对象的原型(Object的原型对象)。
  4. 依此类推一直找到Object为止(null)。

原型对象中this的指向

  • 构造函数和原型对象中,this都指向实例对象。
function Apple(name){
    this.name = name;
}
let that;
Apple.prototype.sayHi = function(){
    that = this;
    console.log('hi');
}
var apple1 = new Apple('iphone12');
apple1.sayHi();
console.log(that === apple1); // true

原型对象扩展内置对象

  • 我们可以通过原型对象,对原来的内置对象进行拓展自定义的方法。比如给数组增加自定义求偶数和的功能。
// 因为数组对象中并没有内置求和方法,所以我们可以通过原型对象进行添加
Array.prototype.sum = function() {
    var sum = 0;
    for (let i = 0; i < this.length; i++) {
        sum += this[i];
    }
    return sum;
    let arr = [1, 2, 3];
    console.log(arr.sum()); // 返回求和结果6
}

注意:

  1. 通过Array.prototype.sum = function()方法添加的时在原型对象中进行追加。不会覆盖掉原来的原型对象。
  2. 如果使用Array.prototype = {}方法进行添加时,这里时重新定义整个原型对象,会覆盖掉原来所有的方法和属性如排序和反转数组。

所以对内置对象添加新的方法只能通过第一种方式添加,第二种方式会覆盖掉本来的原型对象中的方法

继承

ES6之前并没有给我们提供extends继承。我们可以通过构造函数+原型对象模拟实现继承,被称为组合继承

call()

调用这个函数,并且修改函数运行时的this指向

fun.call(thisArg, arg1, arg2, ...)
  • thisArg:当前调用函数this的指向对象
  • arg1, arg2:传递的其他参数
function fn(x, y) {
    console.log('fun');
    console.log('this');
    console.log(x + y);
}
var o = {
    name: 'andy'
};
fn();
// 1. call() 可以调用函数
fn.call();
// 2. call() 可以改变这个函数的this指向 此时这个函数的this 就指向了0这个对象
fn.call(o, 1, 1);

借用父构造函数继承属性

function Father(uname,age){
    this.uname = uname;
    this.age = age;
}

function Son(uname, age, score){
    // 调用Father将里面的Father中的this指向为Son中的this,本质指向了实例对象
    Father.call(this, uname, age);
    this.score = score;
}
var son = new Son('brokyz', 18);
console.log(son);
  • 在Son中通过call()调用了Father,将Father中的this指向了Son中的this,也就是实例对象son,将相应参数传入调用的Father,这样就可以利用Father生成相应的属性,实现了函数的继承。
  • 注意这种方法是继承属性,不是继承方法。方法的继承需要借用原型对象继承。

借用原型对象继承方法

function Father(uname, age) {
    this.uname = uname;
    this.age = age;
}

function Son(uname, age, score) {
    // 调用Father将里面的Father中的this指向为Son中的this,本质指向了实例对象
    Father.call(this, uname, age);
    this.score = score;
}
// Son.prototype = Father.prototype; 
// 浅拷贝这样直接赋值会有问题,如果修改了子原型对象,父原型对象也会跟着修改
// 让Son的原型对象指向Father的一个实例对象,Father的实例可以使用其原型对象中的方法,这样就实现了方法的继承
Son.prototype = new Father();
// 如果利用对象的形式修改了原型对象,别忘了利用constructor指回原来的原型对象
// 因为这里是用Father实例对象覆盖了Son的原型对象
Son.prototype.consturctor = Son;
// 子构造函数专门的方法
Son.prototype.exam = function () {
    console.log('exam');
};
Father.prototype.money = function() {
    console.log('money');
}
var son = new Son('brokyz', 18, 100);
console.log(son);
son.money()

原理

类和对象(ES6新增)

创建类

语法:

class Star {
    // class body
}

创建实例:

var xx = new Star();

构造函数

  • 当生成实例时将会自动执行构造函数。
  • 如果省略不写时,会自动生成构造函数。

语法:

class Star {
    constructor(name){
        this.name = name;
    }
}

方法

  • 共有的方法可以直接写道类中。

语法:

class Star {
    constructor(name){
        this.name = name;
    }
    
    sayHi(){
        console.log("Hi!")
    }
}

继承

  • 子类可以继承父类的方法。

语法:

class Father {
}
class Son extends Father {
}

super

  • 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);
    }
}
new Son(1, 2).sum();

就近原则重写父类方法

  • 当子类重写了父类的方法时,调用时调用的时子类的方法。
  • 如果想使用父类方法需要用到super
class Father {
    say(){
        console.log("father");
    }
}
class Son extends Father {
    say(){
        // 调用父类的say()
        console.log(super.say() + "son");
    }
}
let son = new Son();
son.say();	// 调用的是子类的方法

子类可以对父类的方法进行拓展

  • 子类中必须在构造函数中使用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){
        // 使用父类的方法时需要将参数传递给父类,因为父类中的this是父类中的
        super(x,y);	// 必须写道this前面
        this.x = x;
        this.y = y;
    }
    
    subtract(x,y){
        console.log(this.x - this.y);
    }
}

let son = new Son(5, 3);
son.sum();
son.substact();

类的注意点

  • ES6中的类没有变量提升,必须先声明再使用。
  • 在类中使用共有属性和方法必须要加this,调用本类中的方法也是需要加this
  • 当获取对象时,也是需要用this的
  • this的指向问题,谁调用指向谁
let that;
class Star {
    constructor(name){
        that = this;
        this.name = name;
        this.sayHi();
        this.btn = document.querySelector('button');
        this.btn.onclick = this.sayHi; //这里调用的是函数名所以不加括号。
        // 这里btn调用了函数,所以函数中的this为btn,因此this.name无法输出name
        // 可以给一个全局变量赋值,然后调用全局变量来解决
    }
    
    sayHi(){
        console.log("Hi!" + this.name);
        console.log("Hi!" + that.name);
    }
}

类的本质

  • 在ES6之前通过构造函数+原型对象实现面向对象编程
  • 在ES6通过实现面向对象编程

类的本质其实还是一个函数,我们也可以简单的认为类就是构造函数的另外一种写法。

类和构造函数都有以下特点:

  1. 构造函数有原型对象prototype
  2. 构造函数原型对象prototype里面有constructor指向构造函数(类)本身
  3. 构造函数可以通过原型对象添加方法
  4. 构造函数创建的实例对象有__proto__原型指向构造函数的原型对象
class Star {
    constructor(uname) {
        this.uname = uname;
    }
    eat() {
        console.log('eat');
    }
}
console.log(typeof Star);
// 1. 类有原型对象
console.log(Star.prototype);
// 2. 类的原型对象里面有constructor
console.log(Star.prototype.constructor);
// 3. 类可以通过原型对象添加方法
Star.prototype.sing = function() {
    console.log('sing');
}
var star = new Star('brokyz');
console.log(star);
console.log(star.__proto__ === Star.prototype); // true

ES5中的新增方法

数组方法

迭代(遍历)方法:forEach()、map()、filter()、some()、every()

forEach()

array.forEach(function(currentValue, index, arr))
  • currentValue:数组当前项的值
  • index:数组当前项索引
  • arr:数组对象本身
var arr = [1, 2, 3];
arr.forEach(function(value, index, arr) {
    console.log('数组元素:' + value);
    console.log('数组索引:' + index);
    console.log('数组本身:' + arr);
})

filter()

array.filter(function(currentValue, index, arr))
  • filter()方法创建一个新的数组,新数组中的元素是同过检查指定数组中符合条件的所有元素,主要用于筛选数组
  • 注意它直接返回一个新数组
  • currentValue:数组当前项的值
  • index:数组当前项的索引
  • arr:数组对象本身
var arr = [12,66,4,88];
var newArr = arr.filter(function (value, index, arr) {
    return value >= 20;
})
console.log(newArr);

some()

array.some(function(currentValue, index, arr))
  • some()方法用于检测数组中的元素是否满足指定条件。就是查找数组中是否有满足条件的元素。
  • 注意它的返回值是布尔值,如果查找这个元素,就返回true,如果查不到就返回false。
  • 如果找到第一个满足条件的元素,则终止循环不在继续查找。
  • currentValue:数组房前项的值
  • index:数组当前项的索引
  • arr:数组对象本身
var arr = [12, 66, 4, 88];
var flag = arr.some(function (value, index, arr) {
    return value < 2;
});
console.log(flag);

some和forEach的区别

  • 在forEach()里面,遇到了return会跳出本次循环但是继续下一次迭代(filter和forEach相同)
var arr = ['pink', 'green', 'red', 'blue'];
arr.forEach(function (value) {
    if (value == 'green') {
        console.log('找到了元素');
        return true;    // 在forEach里面,遇到了return会跳出本次循环但是继续下一次迭代
    }
    console.log(22);
})
  • 在some()里面,遇到了return true就是终止遍历 迭代效率更高
var arr = ['pink', 'green', 'red', 'blue'];
arr.some(function (value) {
    if (value == 'green') {
        console.log('找到了元素');
        return true;    // 在some里面,遇到了return true就是终止遍历 迭代效率更高
    }
    console.log(11);
});

trim()去除字符串空格

  • trim()可以去除两侧空格
var str = '  an  dy  ';
console.log(str);
var str1 = str.trim();
console.log(str1);

对象方法

Object.keys()

遍历对象中的属性和方法

Object.keys(obj)

Object.defineProperty()

定义对象中新属性和修改原有的属性。

Obeject.defineProperty(obj, prop, desriptor)
  • obj:必须。目标对象
  • prop:必须。需定义或修改的属性的名字
  • descriptor:必须。目标属性所拥有的特性
    • value:设置属性的值 默认为undefined
    • writable:值是否可以重写。true|flase 默认为false
    • enumerable:目标属性是否可以被枚举。true|false 默认为false
    • configurable:目标属性是否可以被删除或是否可以再次修改特性。true|false 默认为false
var obj = {
    id: 1,
    pname: '小米',
    price: 1999
};
// 1. 以前的对象添加和修改属性的方式
// obj.num = 1000;
// obj.price = 99;
// 2. Object.defineProperty() 定义新属性或修改原有的属性
Object.defineProperty(obj, 'num', {
    value: 1000
});
Object.defineProperty(obj, 'price', {
    value: 9.9
});
Object.defineProperty(obj, 'id', {
    writable: false
});
Object.defineProperty(obj, 'address', {
    value: 'beijing',
    writable: false,
    enumerable: false,
    configurable: false // 如果为false则不允许删除属性,不允许再修改特性。
});
delete obj.address;
obj.id = 2;
console.log(obj);
console.log(Object.keys(obj)); // ['id', 'pname', 'price']
// 通过defineProperty添加的属性才会默认带有enumerable: false

函数进阶

函数的定义与调用

  1. 函数声明方式function关键字(命名函数)
function fn(){}
  1. 函数表达式(匿名函数)
var fn = function(){}
  1. new Function() 不常用效率低
var fn = new Function('a','b', 'console.log(a+b)');
fn(1,2);
console.dir(fn);
console.log(fn intanceof Object);
  • Function里面参数都必须是字符串格式
  • 第三种方式执行效率低,也不方便书写,因此较少使用
  • 所有函数都是Function实例(对象)
  • 函数也属于对象

函数的调用方式

  1. 普通函数
function fn() {
    console.log('test');
}
fn();
  1. 对象的方法
var o = {
    sayHi: function() {
        console.log('test');
    }
}
o.sayHi();
  1. 构造函数
function Star() {
    
}
new Star();
  1. 绑定事件函数
btn.onclick = function() {
    // 点击了按钮可以调用这个函数
}
  1. 定时器函数
setInterval(function(){
    // 定时器自动1秒执行一次
}, 1000)
  1. 立即执行函数
(function(){
    console.log('test');
})(); // 立即执行函数会自动调用

函数内部的this指向问题

调用方式this指向
普通函数调用window
构造函数调用实例对象,原型对象里面的方法也指向实例对象
对象方法调用该方法所属对象
事件绑定方法绑定事件对象
定时器函数window
立即执行函数window
  1. 普通函数 this指向调用者
function fn() {
    console.log(this);
}
fn(); // 指向window,因为这是window.fn()的缩写
  1. 对象方法 this指向调用者,一般为window
var o = {
    sayHi: function() {
        console.log(this);
    }
}
o.sayHi(); //指向对象o
  1. 构造函数和原型对象 this指向调用它的实例对象
var that1;
var that2;
function Star() {
    that1 = this; 
}
Star.prototype.sing = function() {
    that2 = this;
}
var brokyz = new Star();
brokyz.sing();
console.log(that1);
console.log(that2);
  1. 绑定事件函数 this指向调用者
btn.onclick = function() {
    console.log(this);
}
  1. 定时器中的this也是执行window
setInterval(function(){
    console.log(this);
}, 1000)
// 指向window因为是 window.setInterval()的缩写
  1. 立即执行函数 中的this指向window,类似于普通函数,只不过不需要我们手动调用
(function(){
    console.log(this);
})(); // 立即执行函数会自动调用

改变函数内部this指向

JS为我们专门提供了一些函数方法来帮助我们更优雅的处理函数内部this的指向问题,常用的有bind()、call()、apply()三种方法

call()

调用这个函数,并且修改函数运行时的this指向

fun.call(thisArg, arg1, arg2, ...)
  • thisArg:当前调用函数this的指向对象
  • arg1, arg2:传递的其他参数
function fn(x, y) {
    console.log('fun');
    console.log('this');
    console.log(x + y);
}
var o = {
    name: 'andy'
};
fn();
// 1. call() 可以调用函数
fn.call();
// 2. call() 可以改变这个函数的this指向 此时这个函数的this 就指向了0这个对象
fn.call(o, 1, 1);

apply()

aplly()方法调用一个函数。简单理解为调用函数的方式,但是它可以改变函数的this指向。

fun.apply(thisArg, [argsArray])
  • thisArg:在fun函数运行时指定的this值
  • argsArray:传递的值,必须包含在数组里面
  • 返回值就是函数的返回值,因为它就是调用函数
var o = {
    name: 'brokyz'
}
function fn(arr) {
    console.log(this);
    console.log(arr);
}
fn.apply(o, ['pink']);
// 1. 调用函数 第二个可以改变函数内部的this指向
// 2. 但是他的参数必须是数组(伪数组)
// 3. apply的主要应用 比如我们可以利用aplly借助于数学内置对象求最大值
var arr = [1,66,3,99,4];
var max = Math.max.apply(Math, arr)

bind()

bind()方法不会调用函数。但是能够改变函数内部的this指向

fun.bind(thisArg, arg1, arg2, ...)
  • thisArg:在fun函数运行时指定的this值
  • arg1,arg2:传递的其他参数
  • 返回由指定的this值和初始化参数改造的原函数拷贝。也就是说返回的时改完的新新函数
var o = {
    name: 'brokyz'
}
function fn() {
    console.log(this);
}
var f = fn.bind(0);
f();
// 1. 不会调用原来的函数
// 2. 可以改变原来的函数内部的this指向
// 3. 返回的是原函数改变this之后的新函数

例子:当点击按钮之后就禁用这个按钮,3秒后自动开启

var btn = document.querySeletor('button');
btn.onlcick = function() {
    this.disabled = true;
    setTimeout(function(){
        this.disabled = false; // this指向的是window,所以要用bind
    }.bind(this), 3000)
}

三者的总结

相同点:

都可以改变函数内部的this指向。

区别:

  1. call和apply会调用函数,并且改变函数内部this的指向。
  2. call和apply传递的参数不一样,call传递参数arg1,arg2,…的形式,apply必须数组形式[arg]
  3. bind不会调用函数,可以改版函数内部this指向

使用场景:

  1. call经常用作继承。
  2. apply经常和数组有关系的操作。比如借助数学对象实现数组求最大值。
  3. bind不调用函数,但是还想改变this的指向。比如改变定时器内部this的指向。

严格模式

JS除了提供正常模式外,还提供了严格模式(strict mode)。ES5的严格模式是采用具有限制性JS变体的一种方式,即在严格的条件下运行JS代码。

严格模式在IE10以上的版本的浏览器中才会被支持,旧版本浏览器中会被忽略。

严格模式对正常的JavaScript语义做了一些更改:

  1. 消除了JS语法的一些不合理、不严谨之处,减少了一些怪异的行为。
  2. 消除代码运行的一些不安全之处,保证代码运行的安全。
  3. 提高编译器效率,增加运行速度。
  4. 禁用了在EMCAScript的未来版本中可能会定义的一些语法,为未来新版本的JS做好铺垫。比如一些保留字如:class,enum,export,extends,super不能作为变量名。

开启严格模式

  1. 给整个脚本添加严格模式
<script>
	'use strict';
</script>

或:

<script>
	(function(){
        'use strict';
    })();
</script>
  1. 为某个函数开启严格模式
<script>
	function fn(){
        'use strict';
    }
    function f(){
        'use strict';
    }
</script>

严格模式的变化

变量规范

  1. 在正常模式中,如果一个变量没有声明就赋值,默认是全局变量。严格模式禁止这种用法,变量都必须先用var命令声明,然后再使用。
  2. 严禁删除已经生命好的变量。
'use strict';
// 1. 变量必须先声明
// num = 10;
var num = 10;
console.log(num);
// 2. 我们不能随意删除已经声明好的变量
// delete num;

严格模式下的this指向

  1. 在正常模式下,全局作用域中的this指向window对象
  2. 严格模式下,全局作用域中的this是undefined
'use strict';
function fn(){
    console.log(this)
}
fn();
  1. 正常模式下,构造函数不加new也可以调用,this指向全局对象
  2. 严格模式下,构造函数如果不加new调用,this会报错
  3. 如果是通过new创建构造函数,那么this依然指向实例对象
'use strict';
function Star(){
    this.sex = '男'; // 在严格模式下这个this指向undefined
}
Star();
  1. 严格模式下,定时器的this还是指向window
  2. 严格模式下,事件、对象还是指向调用者

函数的变化

  1. 函数不允许有重名的参数。
function fn(a,a){
    console.log(a+a);
}
fn(1,2); // 输出为4,以为传入完成时a为2,计算的是2+2=4。严格模式不允许这样。
  1. 函数必须声明在顶层。新版本的JS会引入"块级作用域"(ES6已经引入)。为了与新版本接轨。不允许在非函数的代码块内声明函数。比如不允许在if和for中声明函数。但是依然可以在函数中声明函数来完成函数的嵌套。

高阶函数

高阶函数是对其他函数进行操作的函数,它接受函数作为参数将函数作为返回值输出。满足其一就是高阶函数。

// 将函数作为参数,可以为函数作为回调函数使用。
function fn(a,b,callback){
    console.log(a+b);
    callback&&callback();
}
fn(1,2,function(){alert('hi')})

// 将函数作为参数返回
function fn() {
    return function(){}
}
fn();

闭包

闭包指有权访问另一个函数作用域中变量的函数。------JavaScript高级程序设计

简单理解就是,有个作用域可以访问另外一个函数内部的局部变量。

闭包的主要作用:延伸了变量的作用范围

function fn() {
    var num = 10;
    function fun() {
        console.log(num);
    }
    fun();
}
fn();

用fn外面的作用域访问fn内部的局部变量

function fn() {
    var num = 10;
    return function() {
        console.log(num);
    }
}
var f = fn();
f();
  • 所以闭包不及让函数内部的作用域访问局部变量,也可以使用全局作用域来访问函数的局部变量。

闭包应用-点击li输出当前li的索引号:

<body>
    <ul class="nav">
        <li>榴莲</li>
        <li>小汉堡</li>
        <li>臭豆腐</li>
        <li>腐乳</li>
    </ul>
    <script>
        // 闭包应用-点击li输出当前li的索引号
        // 1. 我们可以利用动态添加属性的方式
        var lis = document.querySelector('.nav').querySelectorAll('li');
        // for (var i = 0; i < lis.length; i++) {
        //     lis[i].index = i;
        //     lis[i].onclick = function () {
        //         console.log(this.index);
        //     };
        // }
        // 2.利用闭包的方式得到当前小li的索引号
        for (var i = 0; i < lis.length; i++) {
            // 利用for循环创建了4个立即执行函数
            (function (i) {
                // console.log(i);
                lis[i].onclick = function () {
                    console.log(i); // 调用了立即执行函数的i,使用了闭包
                };
            })(i);
        }
    </script>
</body>

闭包应用-3秒钟之后,打印所有li元素的内容:

<body>
    <ul class="nav">
        <li>榴莲</li>
        <li>小汉堡</li>
        <li>臭豆腐</li>
        <li>腐乳</li>
    </ul>
    <script>
        // 闭包应用-3秒钟之后,打印所有li元素的内容:
        var lis = document.querySelector('.nav').querySelectorAll('li');
        for (var i = 0; i < lis.length; i++) {
            // 回调函数只有触发了才会执行,这是异步任务,会先进入任务队列中等待执行
            // setTimeout(function () {
            //     console.log(lis[i]);
            // }, 2000);
            (function (i) {
                setTimeout(function () {
                    console.log(lis[i].innerHTML);
                }, 2000);
            })(i);
        }
    </script>
</body>

闭包应用-计算打车价格

// 闭包应用-计算打车价格
// 打车起步价13(3公里内),之后每多一公里增加5元,用户输入公里数就可以计算打车价格
// 如果有拥堵状况,总价格多收取10块钱拥堵费
var car = (function () {
    var start = 13; // 起步价
    var total = 0;    // 总价
    return {
        price: function (n) {
            if (n <= 3) {
                total = start;
            } else {
                total = 13 + (n - 3) * 5;
            }
            return total;
        },
        yd: function (flag) {
            return flag ? total + 10 : total;
        }, // 拥堵之后的费用
    };
})();
console.log(car.price(1));
console.log(car.yd(true));
console.log(car.price(5));
console.log(car.yd(true));
  • 由于函数price和yd使用了其他函数中的局部变量start和total,所以产生了闭包,而包含这个变量的函数就被称为闭包函数

递归函数

如果一个函数在内部可以调用其本身,那么这个函数就是递归函数

递归函数的作用和循环效果一样

由于递归很容易发生“栈溢出”错误(stack overflow),所以必须要加退出条件return

// 递归函数:函数内部自己调用自己,这个函数就是递归函数
var num = 1;
function fn(){
    console.log('我要打印6句话');
    if (num == 6) {
        return; // 递归里面必须加退出条件
    }
    num++;
    fn();
}
fn();

利用递归求1-n阶乘

function fn(n){
    if(n == 1) {
        return 1;
    }
    return n * fn(n-1);
}
fn(3);
// return 3 * fn(2)
// return 3 * 2 * fn(1)
// return 3 * 2 * 1

利用递归函数求斐波那契数列

function fn(n) {
    if (n == 1 || n == 2){
        return 1;
    }
	return fn(n-1) + fn(n-2);
}
fn(4);

根据id号返回相应的数据

var data = [{
    id: 1,
    name: '家电',
    goods: [{
        id: 11,
        gname: '冰箱',
        goods: [{
            id: 111,
            gname: '海尔'
        }]
    }, {
        id: 12,
        gname: '洗衣机'
    }]
}, {
    id: 2,
    name: '服饰'
}];
// 1. 利用 forEach去遍历里面的每一个对象
function getID(json, id) {
    var o = {};
    json.forEach(function (item) {
        if (item.id == id) {
            // console.log(item);
            o = item;
            // 2. 我们想要得到里层的数据 11 12 可以利用递归函数完成
            // 里面应该有goods数组并且数组的长度不为0
        } else if (item.goods && item.goods.length > 0) {
            o = getID(item.goods, id);
        }
    });
    return o;
}
console.log(getID(data, 11));
console.log(getID(data, 111));

浅拷贝 Object.assign()

浅拷贝只拷贝一层,更深层次对象级别的只拷贝引用

可以通过Object.assign()来实现浅拷贝

Object.assign(obj2,obj1)
// 将obj1浅拷贝给obj2
var obj = {
    id: 1,
    name: 'andy',
    msg: {
        age: 18
    }
};
var o = {};
// for(var k in obj) {
//     // k是属性名字 obj[k]是属性值
//     o[k] = obj[k];
// }
// console.log(o)
// o.msg.age = 20;
// console.log(obj)
Object.assign(o, obj);
console.log(o);
// 由于浅拷贝更深层次对象只拷贝引用,所以o修改msg.age时obj也会同时跟着修改

深拷贝

拷贝所有层

var obj = {
    id: 1,
    name: 'andy',
    msg: {
        age: 18
    }
};
var o = {};
function deepCopy(newobj, oldobj) {
    // 判断我们的属性值属于哪种数据类型
    for (var k in oldobj) {
        // 1. 获取属性值
        var item = oldobj[k];
        // 2. 判断这个值是否是数组
        if (item instanceof Array) {
            newobj[k] = [];
            deepCopy(newobj[k], item);
        } else if (item instanceof Object) {
            // 3. 判断这个值是否是对象
            newobj[k] = {};
            deepCopy(newobj[k], item);
        } else {
            // 4. 属于简单数据类型
            newobj[k] = item;
        }
    }
}
deepCopy(o, obj);
console.log(o);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值