web05.JavaScript高级知识(二)

闭包

闭包的概念

  1. 概念:一个用于许多变量和绑定了这些变量执行上下文环境的 表达式,通常是一个函数。

  2. 特点

    • 函数拥有的外部变量的引用,在函数返回时,该变量仍然处于活跃状态。
    • 闭包作为一个函数返回时,其执行上下文环境不会被销毁,仍处于执行上下文环境中。

​ 在JavaScript中存在一种内部函数,即函数声明和函数表达式可以位于另一个函数的函数体内,在内部函数中可以访问外部函数声明的变量,当这个内部函数在包含它们的外部函数之外被调用时,就会形成闭包。

function fn() {
    var max = 10;
    return function bar(x) {
        if (x > max) {
            console.log(x);//11
        }
    };
}
var f1 = fn();
f1(11);

闭包的用途

  1. 实现公有变量: eg:函数累加器
  2. 可以做缓存(存储结构):eg:eater
  3. 可以实现封装,属性私有化:eg:new Person();
  4. 模块化开发,防止污染全局变量

结果缓存

如果内存中有,则直接返 回,如果内存中没有,则调用函数进行计算,更新缓存并返回结果。因为闭包不会释放外部变量的引 用,所以能将外部变量值缓存在内存中。

var cacheBox = (function () {
    //缓存的容器
    var cache = {};
    return {
        searchBox: function (id) {
            //如果在内存中,则直接返回
            if (id in cache) {
                return '查找的结果为:' + cache[id];
            }
            //经过一段很耗时的dealFn()函数处理
            var result = dealFn(id);
            //更新缓存的结果
            cache[id] = result;
            //返回计算的结果
            return '查找的结果为:' + result;
        }
    };
})();
//处理很耗时的函数
function dealFn(id) {
    console.log('这是一段很耗时的数据库查询操作');
    return id;
}
//两次调用cacheBox()函数
console.log(cacheBox.searchBox(1));
console.log(cacheBox.searchBox(1));

封装

​ 在JavaScript中提倡的模块化思想是希望将具有一定特征的属性封装到一起,只需要对外暴露对应的 函数,并不关心内部实现的逻辑。

​ 例如:我们可以借助数组实现一个栈,只对外暴露出表示入栈和出栈的push()函数和pop函数,以 及表示栈长度的size()函数。

var stack = (function () {
    //实现数组模仿栈的实现
    var arr = [];
    //栈
    return {
        push: function (value) {
            arr.push(value);
        },
        pop: function () {
            return arr.pop();
        },
        size: function () {
            return arr.length;
        }
    }
})();
stack.push('abc');
stack.push('def');
console.log(stack.size());//2
stack.pop();
console.log(stack.size());//1

定时器问题

定时器setTimeout()函数和for循环在一起使用

var arr = ['one', 'two', 'three'];
for (var i = 0; i < arr.length; i++) {
    setTimeout(function () {
        console.log(arr[i]);
    }, i * 1000);
}
//undefined
//undefined
//undefined

通过闭包可以解决这个问题

var arr = ['one', 'two', 'three'];
for (var i = 0; i < arr.length; i++) {
    (function (time) {
        setTimeout(function () {
            console.log(arr[time]);
        }, time * 1000)
    })(i);
}

作用域链问题

闭包往往会设计到作用域链的问题,尤其是包含this属性时。

var name = 'outher';
var obj = {
    name: 'inner',
    method: function () {
        return function () {
            return this.name;
        }
    }
};
console.log(obj.method()());//undefined

在匿名函数中,this相当于一个外部变量,所以会形成一个闭包。

在JavaScript中,this指向的永远是函数的调用实体,而匿名函数的实体是全局对象window,因此 输出全局变量name的值是outer。

如果想要输出obj对象自身的name属性,就要改变this的指向,将其指向obj对象本身。

var name = 'outher';
var obj = {
    name: 'inner',
    method: function () {
        //用_this保存obj中的this
        var _this = this;
        return function () {
            return _this.name;
        }
    }
};
console.log(obj.method()());

闭包总结

闭包如果使用合理,在一定程度上能够提高代码的执行效率,如果使用不合理,则会造成内存浪 费,性能下降,接下来我们总结一下闭包的优缺点

闭包的优点

  1. 保护函数内变量的安全,实现封装,防止变量流入其他环境发生命名冲突,造成环境污染
  2. 在适当的时候,可以在内存中维护变量并缓存,提高执行效率。

闭包的缺点

  1. 消耗内存,通过来说,函数的活动对象会随着执行的上下文环境一起被销毁,但是由于闭包引用的 是外部函数的活动对象,因此这个活动对象无法被销毁,这意味着闭包比普通函数要消耗更多的内 存。
  2. 内存泄漏,在IE9之前,如果闭包的作用域链中存在DOM对象,则意味着该DOM对象无法被销毁, 造成内存泄漏。
function closure() {
    var element = document.getElementById('elementID');
    element.onclick = function () {
        console.log(element.id);
    }
}

​ closure() 函数中,给一个element 元素绑定了click事件,而在这个事件中,输出了element元素的 id属性,即在onclick函数的闭包中存在了对外部元素element的引用,那么该element 元素在网页关闭 之前会一直存在于内存之中,不会被释放。

​ 如果这样的事件处理的函数很多,将会导致大量内存被占用,进而严重影响性能,对应的解决办法 是:先将需要使用的属性使用临时变量进行存储,然后在事件处理函数时使用临时变量进行操作;此时闭包 中虽然不直接引用element 元素,但是对值的调用仍然会等导致 element 元素的引用被保存,此时应该 手动将 element 元素设置为 null。

function closure() {
    var element = document.getElementById('elementID');
    //使用临时变量进行存储
    var id = element.id;
    element.onclick = function () {
        console.log(id);
    }
    //手动将元素设置为null
    element = null;
}

this使用详解

this使用

​ 当我们想要创建一个构造函数的实例时,需要使用new操作符,函数执行完成后,函数体中的this 就指向了这个实例,通过下面的这个实例可以访问到绑定在this上的属性。

function Person(name) {
    this.name = name;
}
var p = new Person('cao teacher');
console.log(p.name);//cao teacher

将Person()函数当做一个普通函数执行,在window对象上,我们可以找到name属性的值,这表明函数体中的this指向了window 对象。

function Person(name) {
    this.name = name;
}
Person('cao teacher');
console.log(window.name);//cao teacher

this指向的就是当前类的实例对象,而在JavaScript中,this指向是随着宿 主环境的变化而变化的,在不同的地方调用,返回的可能是不同的结果。

我们可以结论先行,在JavaScript中,this指向的永远是函数的调用者。

this指向全局对象

当函数没有所属对象直接调用时,this指向的是全局对象。

var value = 10;//相当于window.value= 10
var obj = {
    value: 100,
    method: function () {
        var foo = function () {
            console.log(this.value);//10
            console.log(this);//window,此时foo()函数的执行是没有所属对象的
        };
        foo();
        return this.value;
    }
}
obj.method();
console.log(obj.method());//100,metho()函数的调用体是obj对象

this指向所属对象

this指向对象实例

当通过new操作符调用构造函数生成对象实例时,this指向该实例。

//全局变量
var number = 10;

function Person() {
    //复写全局变量
    number = 20;
    //实例变量
    this.number = 30;
}
//原型函数
Person.prototype.getNumber = function () {
    return this.number;
}
//通过new操作符获取对象的实例
var p = new Person();
console.log(p.getNumber());

​ 定义了全局变量number和实例变量number,通过new操作符生成 Person对象的实例p后,在调用getNumber()操作时,其中的this就指向该实例p,而实例p在初始化的 时候被赋予number的值是30,所以输出的结果是30。

this重新绑定对象

通过call()函数,apply()函数和bind()函数可以改变函数的执行主体,如果函数中存在 this关键字,则this也会指向call()函数,apply()函数和bind()函数处理后的对象。

//全局变量
var value = 10;
var obj = {
    value: 20
};
//全局函数
var method = function () {
    console.log(this.value);
};
console.log(method());
console.log(method.call(obj));
console.log(method.apply(obj));
var newMethod = method.bind(obj);
console.log(newMethod());

闭包中的this

​ 函数的this变量只能被自身访问,其内部函数无法访问,因此在遇到闭包时,闭包内部的this关键字 无法访问到外部变量函数的this变量。

var user = {
    sport: 'basketball',
    data: [{
            name: 'cao teacher',
            age: 18
        },
        {
            name: 'cao teacher',
            age: 30
        },
    ],
    clickHander: function () {
        //此时的this指向的是user对象
        this.data.forEach(function (person) {
            console.log(this);//全局
            console.log(person.name + ' is playing ' + this.sport);//undefined。
        })
    }
};
user.clickHander();

使用**临时变量将clickHander()函数的this提前存储,**对其使用user对象,而在匿名函数中,使用临时变量访问sport属性,而不是直接用this访问。

var user = {
    sport: 'basketball',
    data: [{
            name: 'cao teacher',
            age: 18
        },
        {
            name: 'cao teacher',
            age: 30
        },
    ],
    clickHander: function () {
        //使用临时变量_this保存this
        var _this = this;
        //此时的this指向的是user对象
        this.data.forEach(function (person) {
            console.log(this);//Window 
            console.log(person.name + ' is playing ' + _this.sport);//cao teacher is playing basketball
        })
    }
};
user.clickHander();

分析

function f(k) {
    this.m = k;
    return this;
}
var m = f(1);
var n = f(2);
console.log(m.m);
console.log(n.m);

在执行f(1)的时候

  1. 因为f()函数的调用没有所属对象,所以this指向window,
  2. 然后this.m=k语句执 行后,相当于window.m=1。
  3. 通过return语句返回window,而又将返回值window赋值给全局变量m, 因此变成了window.m=window,覆盖前面的window.m=1

在执行f(2)的时候

  1. this同样指向window,
  2. 此时window.m已经变成2.m即window.m=2覆盖了 window.m=window。
  3. 通过return语句将window对象返回并赋值给n,此时window.n=window。

先看m.m的输出

​ m.m=(window.m).m,实际为2.m,2是一个数值型常量,并不存在m属性,因 此返回“undefined”

再看n.m的输出,

​ n.m=(window.n)m=window.m=2,因此输出“2”。

call函数、apply函数和bind函数

call()函数的基本使用

call()函数调用一个函数时,会将该函数的执行对象上下文改变为另一个对象

function.call(thisArg,arg1,arg2);
  • function为需要调用的函数
  • thisArg表示的是新的对象上下文,函数中的this将指向thisArg,如果thisArg为null或者 undefined,则this会指向全局对象。
  • arg1arg2,····表示的是函数所接收的参数列表

call()函数的用法

//定义一个add()函数
function add(x, y) {
    return x + y;
}
//通过call()函数进行add()函数的调用
function myAddCall(x, y) {
    //调用add()函数的call函数
    return add.call(this, x, y);
}
console.log(myAddCall(10, 20));

myAddCall()函数自身是不具备运算能力的,但是我们在myAddCall()函数中,通过调用add()函数的 call()函数,并传入this值,将执行add()函数主体改变为myAddCall()函数自身,然后传入参数x,y这就 使得myAddCall()函数拥有add()函数计算求和的能力,在实际计算时就是10+20=30。

apply()函数的基本使用

apply()函数的作用与call()函数时一样的,只是在传递参数的形式上存在差别,语法格式如下:

function.apply(thisArg,[arg1,arg2]);
  • function为需要调用的函数
  • thisArg表示的是新的对象上下文,函数中的this将指向thisArg,如果thisArg为null或者 undefined,则this会指向全局对象。
  • 和call()函数的区别就是,再传值得时候,传递的是一个数组对象如果传递的不是数组对象就会报 错
//定义一个add()函数
function add(x, y) {
    return x + y;
}
//通过call()函数进行add()函数的调用
function myAddApply(x, y) {
    //调用add()函数的call函数
    return add.apply(this, [x, y]);
}
console.log(myAddApply(10, 20));//30

bind()函数的基本使用

bind()函数创建一个新的函数,在调用时设置this关键字为提供的值,在执行新函数时,将给定的参数列表作为原函数的参数序列,从前往后匹配,其语法格式如下:

function.bind(thisArg,arg1,arg2);
  • function为需要调用的函数
  • thisArg表示的是新的对象上下文,函数中的this将指向thisArg,如果thisArg为null或者 undefined,则this会指向全局对象。
  • arg1,arg2,····表示的是函数所接收的参数列表。
//定义一个add()函数
function add(x, y) {
    return x + y;
}
//通过call()函数进行add()函数的调用
function myAddBind(x, y) {
    //调用add()函数的call函数
    var bindfn = add.bind(this, x, y);
    return bindfn();
}
console.log(myAddBind(10, 20));

call()函数、apply()函数、bind()函数的比较

相同之处:

​ 都会改变函数调用的执行主体,修改this的指向

不同之处:

  1. 函数立即执行
    • call()函数与apply()函数在执行后会立即调用前面的的数
    • 而bind()函 数不会立即调用,它会返回一个新的函数,可以在任何时候进行调用。
  2. 参数传递
    • call()函数与bind()函数接收的参数相同,第一个参数表示将要改变的函数执行主体,即this的指向,从第二个参数开始到最后一个参数表示的是函数接收的参数;
    • 而对于 apply()函数。第一个参数与call()函数,bind()函数相同,第二个参数是一个数组,表示的是接收的 所有参数,如果第二个参数不是一个有效的数组或者arguments 对象,则会抛出一个TypeError 异 常。

call函数、apply函数和bind函数的巧妙用法

求数组的最大项和最小项

var arr = [3, 5, 7, 2, 9, 11];
//求数组中的最大值
console.log(Math.max.apply(null, arr));
//求数组中的最小值
console.log(Math.min.apply(null, arr));

apply()函数的第一个参数为null,这是因为没有对象去调用这个函数,我们只需要这个函数帮助我们运算,得到返回的结果。

类数组对象转换为数组对象

​ 函数的参数对象arguments是一个类数组对象,自身不能直接调用数组的方法,但是我们可以借助 call()函数,让arguments对象调用数组的slice()函数,从而得到一个真的数组,后面就能调用数组的函 数。

//通用求和函数
function sum() {
    //通过call()函数间接调用数组的slice()函数,以得到函数参数的数组。
    var arr = Array.prototype.slice.call(arguments);
    //调用数组的reduce()函数进行多个值的求和
    return arr.reduce(function (pre, cur) {
        return pre + cur;
    }, 0)
}
console.log(sum(1, 2));
console.log(sum(1, 2, 3));
console.log(sum(1, 2, 3, 4));

执行匿名函数

​ 通过一个匿名函数,在匿名函数的作用域中添加print()函数用于输出对象的各个属性值, 然后通过call()函数将该print()函数的执行主体改变为数组元素,这样就可以达成目的了。

var animals = [{
        species: 'Lion',
        name: 'cao teacher'
    },
    {
        species: 'Whale',
        name: 'cao teacher2'
    },
];
for (var i = 0; i < animals.length; i++) {
    (function (i) {
        this.print = function () {
            console.log('#' + i + ' ' + this.species + ':' + this.name);
        };
        this.print();
    }).call(animals[i], i);
}

在上面的代码中,**在call()函数中传入animals[i],**这样匿名函数内部的this就指向animals[i],在调 用print()函数时,this也会指向animals[i],从而能输出speices属性和name属性。

bind()函数配合setTimeout

在默认情况下,

使用setTimeout()函数的时候,this关键字会指向全局对象window

当使用类的函 数时,需要this引用类的实例,我们可能需要显示的把this绑定到回调函数以便继续使用实例。

//定义一个函数
function LateBloomer() {
    this.petalCount = Math.ceil(Math.random() * 12) + 1;
}
//定义一个原型函数
LateBloomer.prototype.bloom = function () {
    //在一秒后调用实例的declare()函数,很关键的一句
    window.setTimeout(this.declare.bind(this), 1000);
};
//定义原型链上的declare()函数
LateBloomer.prototype.declare = function () {
    console.log('I am a beatiful flower with' + this.petalCount + 'petals!');
}
//生成LateBloomer的实例
var flower = new LateBloomer();
flower.bloom();

对象的属性和访问方式

对象的属性

​ ECMA-262 将对象定义为一组属性的无序集合。严格来说,这意味着对象就是一组没有特定顺序的 值。对象的每个属性或方法都由一个名称来标识,这个名称映射到一个值。其中的内容就是一组名/值 对,值可以是数据或者函数。

理解对象

  1. 创建自定义对象的通常方式是创建 Object 的一个新实例,然后再给它添加属性和方法
let person = new Object();
person.name = "Nicholas";
person.age = 29;
person.job = "Software Engineer";
person.sayName = function () {
    console.log(this.name);
};
  1. 对象字面量方式创建对象
let person = {
    name: "Nicholas",
    age: 29,
    job: "Software Engineer",
    sayName() {
        console.log(this.name);
    }
};

数据属性

属性分两种:数据属性访问器属性

数据属性包含一个保存数据值的位置。值会从这个位置读取,也会写入到这个位置。

数据属性有 4 个特性描述它们的行为。

  1. [[Configurable]]:表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及 是否可以把它改为访问器属性。默认情况下,所有直接定义在对象上的属性的这个特性都是 true。
  2. [[Enumerable]]:表示属性是否可以通过 for-in 循环返回。默认情况下,所有直接定义在对象上的 属性的这个特性都是 true。
  3. [[Writable]]:表示属性的值是否可以被修改。默认情况下,所有直接定义在对象上的属性的 这个 特性都是 true。
  4. [[Value]]:包含属性实际的值。这就是前面提到的那个读取和写入属性值的位置。这个特性的默认 值为 undefined

修改属性的默认特性,就必须使用 **Object.defineProperty()**方法

接收 3 个参数:

  1. 要给其添加属性的对象
  2. 属性的名称
  3. 一个描述符对象
  • configurable:设置为 false,意味着这个属性不能从对象上删除
  • enumerable
  • writable
  • value

访问器属性

访问器属性不包含数据值, 相反,它们包含一个获取(getter)函数和一个设置(setter)函数

在写入访问器属性时,会调用设置函数并传入新值,这个函数必须决定对数据做出什么修改。

访 问器属性有 4 个特性描述它们的行为。

  1. [[Configurable]]:表示属性是否可以通过 delete 删除并重新定义,是否可以修改它的特性,以及 是否可以把它改为数据属性。默认情况下,所有直接定义在对象上的属性的这个特性都是 true。
  2. [[Enumerable]]:表示属性是否可以通过 for-in 循环返回。默认情况下,所有直接定义在对象上的 属性的这个特性都是 true。
  3. [[Get]]:获取函数,在读取属性时调用。默认值为 undefined。
  4. [[Set]]:设置函数,在写入属性时调用。默认值为 undefined。

访问器属性是不能直接定义的,必须使用 Object.defineProperty()

// 定义一个对象,包含伪私有成员 year_和公共成员
let book = {
    year_: 2017,
    edition: 1
};
Object.defineProperty(book, "year", {
    get() {
        return this.year_;
    },
    set(newValue) {
        if (newValue > 2017) {
            this.year_ = newValue;
            this.edition += newValue - 2017;
        }
    }
});
book.year = 2018;
console.log(book.edition); // 2

​ 在这个例子中,对象 book 有两个默认属性: year_ 和 edition。 year_ 中的下划线常用来表示该 属性并不希望在对象方法的外部被访问。另一个属性 year 被定义为一个访问器属性,其中获取函数简单 地返回 year_ 的值,而设置函数会做一些计算以决定正确的版本(edition)。因此,把 year 属性修改 为 2018 会导致 year_ 变成 2018,edition 变成 2。这是访问器属性的典型使用场景,即设置一个属性 值会导致一些其他变化发生。 获取函数和设置函数不一定都要定义。只定义获取函数意味着属性是只读的,尝试修改属性会被忽 略。在严格模式下,尝试写入只定义了获取函数的属性会抛出错误。类似地,只有一个设置函数的属性 是不能读取的,非严格模式下读取会返回 undefined,严格模式下会抛出错误。 在不支持 Object.defineProperty()的浏览器中没有办法修改[[Configurable]]或[[Enumerable]]。

var person = {
    _age: 10
};
Object.defineProperty(person, "age", {
    get: function () {
        return this._age;
    },
    set: function (newValue) {
        if (newValue > 10) {
            this._age = newValue;
            console.log('设置成功');
        }
    }
});
console.log(person.age);
person.age = 9;
console.log(person.age);
person.age = 19;
console.log(person.age);

定义的person对象包含了一个_age属性,一般遇见以下划线开头的属性时就可以 将其理解为私有属性。

属性的访问方式

在JavaScript中,对象属性的访问方式有两种,一种是使用点操作符(.),另一种是使用**[]操作符**

使用.操作符来访问属性

ObjectName.propertyName

其中ObjectName为对象名称,propertyName为属性名称

例如person.name,表示访问person对象的name属性

使用[]来访问属性

ObjectName[propertyName]

其中ObjectName为对象名称,propertyName为属性名称。

区别

  1. 点操作符是静态的,只能是一个以属性名称命名的简单描述符,而且无法修改,而中括号操作 符是动态的,可以传递字符串或者变量,并且支持在运行时修改

    var obj = {};
    obj.name = 'cao teacher';
    var myName = 'name';
    console.log(obj.myName);
    console.log(obj[myName]);
    
  2. 点操作符不能以数字做为属性名,而中括号操作符可以

    var obj ={};
    obj.1=1;
    obj[2]=2;
    console.log(obj.1);
    console.log(obj[2]);
    
  3. 如果属性名中包含会导致语法错误的字符,或者属性名中含有关键字或者保留字,可以使用方 括号操作符,而不能使用点操作符。

var person={};
person['first name'] = 'cao teacher';
console.log(person['first name']);
console.log(person.firset name);

创建对象

创建对象

​ 对象是一系列无序属性的集合,属性值可以为基本数据类型,对象或者函数,因此 对象实际就是一组键值对的组合。

//对象
var person = {
    //基本数据类型的属性
    name: 'cao teacher',
    age: 18,
    //函数类型的属性
    getName: function () {
        return this.name;
    },
    //对象类型的属性
    adress: {
        name: '上海',
        code: '10000'
    }
}

对象作为数据存储最直接有效的方式,具有非常高的使用频率,接下来总结了JavaScript中创建对象 的7种方式

基于Object()构造函数

通过Object()对象的构造函数生成一个实例,然后给他们增加需要的各种属性

//Object()构造函数生成实例
var person = new Object();
//为实例新增各种属性
person.name = 'cao teacher';
person.age = 11;
person.getName = function () {
    return this.name;
}
person.address = {
    name: '上海',
    code: '122'
};

基于对象字面量

对象字面量本身就是一系列键值对的组合,每个属性之间通过逗号分隔。

var person = {
    //基本数据类型的属性
    name: 'cao teacher',
    age: 18,
    //函数类型的属性
    getName: function () {
        return this.name;
    },
    //对象类型的属性
    adress: {
        name: '上海',
        code: '10000'
    }
}

对象的属性值是通过对象 自身进行设置的,如果需要同时创建若干个属性相同,而只是属性值不同的对象时,则会产生很多的重 复代码,造成代码的冗余,因此不推荐使用方法1和方法2来批量创建对象。

基于工厂方法模式

​ 工厂方法模式是一种比较重要的设计模式,用于创建对象,旨在抽象出创建对象和属性赋值的过 程,值对外暴露出需要设置的属性值。

//工厂方法 对外暴露接收的name age address属性值
function createPerson(name, age, address) {
    //内部通过Object()构造函数生成一个对象,并添加各种属性
    var o = new Object();
    o.name = name;
    o.age = age;
    o.getName = function () {
            return this.name;
        },
        o.address = address
    return o;
}
var person = createPerson('caoteacher', 18, {
            name: '上海',
            code:'10000'
})

使用工厂方法可以减少很多重复的代码,但是创建的所有实例都是Object类型,无法更近一步区分 具体类型。

基于构造函数模式

构造函数是通过this为对象添加属性的,属性值类型可以为基本类型,对象或者函数,然后通过 new操作符创建对象的实例。

function Person(name,age,address){
    this.name = name;
    this.age = age;
    this.address = address;
    this.getName = function(){
    	return this.name;
    }
}
var person = new Person('caoteacher',18,{
    name:'上海',
    code:'10000'
})

使用构造函数创建的对象可以确定其所属类型,解决了方法3中的问题。

但是使用构造函数创建的对象存在一个问题,即相同实例的函数是不一样的

function Person(name,age,address){
    this.name = name;
    this.age = age;
    this.address = address;
    this.getName = function(){
    	return this.name;
    }
}
var person = new Person('caoteacher', 18, {
    name: '上海',
    code: '10000'
})
var person2 = new Person('caoteacher', 18, {
    name: '上海',
    code: '10000'
})
console.log(person.getName === person2.getName);//false

这就意味着每个实例的函数都会占据一定的内存空间,其实这是没必要的,会造成资源的浪费,另外函 数也没必要在代码执行前就绑定到对象上。

基于原型对象的模式

基于原型对象的模式是将所有的函数和属性都封装在对象的prototype属性上

//定义函数
function Person() {
    //通过prototype属性增加属性和函数
    Person.prototype.name = 'cao teacher';
    Person.prototype.age = 18;
    Person.prototype.address = {
        name: '上海',
        code: '1000'
    };
    Person.prototype.getName = function () {
        return this.name;
    };
};
//生成两个实例
var person = new Person();
var person2 = new Person();
console.log(person.getName === person2.getName);//true
console.log(person.name === person2.name);//true

使用基于原型对象的模式创建的实例,其属性和函数都是相等的,不同 的实例会共享原型上的属性和函数,解决了方法4存在的问题。

问题:因为所有的实例会共享相同的属性,那么改变其中一个实例的属性值,便会引起其他实例的属性值得变化

//定义函数
function Person() {
    //通过prototype属性增加属性和函数
    Person.prototype.name = 'cao teacher';
    Person.prototype.age = 18;
    Person.prototype.address = {
        name: '上海',
        code: '1000'
    };
    Person.prototype.getName = function () {
        return this.name;
    };
};
//生成两个实例
var person = new Person();
var person2 = new Person();
console.log(person.name);//cao teacher
person2.name='cao teacher2';
console.log(person2.name);//cao teacher2

构造函数和原型混合的模式

构造函数原型混合的模式是目前最常见的创建自定义类型对象的方式。

​ 构造函数中用于定义实例的属性,原型对象中用于定义实例共享的属性和函数,通过构造函数传递参数,这样每个实例都能拥有自己的属性值,同时实例还能共享函数的引用,最大限度的节约了内存的空间,混合模式可谓集二者之所长。

//构造函数中定义实例的属性
function Person(name, age, address) {
    this.name = name;
    this.age = age;
    this.address = address;
}
//原型中添加实例共享的函数
Person.prototype.getName = function () {
    return this.name;
}
//生成两个实例
var person = new Person('caoteacher', 11, {
    name: '上海',
    code: '1000'
});
var person2 = new Person('caoteacher2', 12, {
    name: '上海2',
    code: '10002'
});
//输出实例初始的name属性
console.log(person.name);
console.log(person2.name);
//改变一个实例的属性
person.address.name = '广州市';
person.address.code = '10003';
//不影响另一个实例的属性值
console.log(person2.address.name);
//不同的实例共享相同的函数,因此在比较时是相等的
console.log(person.getName === person2.getName);
//改变一个实例的属性,函数仍然能正确的运行
person2.name = "caoteacher3";
console.log(person.getName());
console.log(person2.getName());

基于动态原型模式

动态原型模式将原型对象放在构造函数的内部,通过变量进行控制,只在第一次生成实例的时候进行原型的设置。

​ 动态原型的模式相当于懒汉模式,只在生成实例时设置原型对象,但是功能与构造函数和原型混合模式是相同的。

//动态原型模式
function Person(name, age, address) {
    this.name = name;
    this.age = age;
    this.address = address;
    //如果对象中_initialized为undefined,则表明还没有为Person的原型对象添加函数
    if (typeof Person._initialized === "undefined") {
        Person.prototype.getName = function () {
            return this.name;
        };
        Person._initialized = true;
    }
}
//生成两个实例
var person = new Person('caoteacher', 11, {
    name: '上海',
    code: '1000'
});
var person2 = new Person('caoteacher2', 12, {
    name: '上海2',
    code: '10002'
});
//输出实例初始的name属性
console.log(person.name);//caoteacher
console.log(person2.name);//caoteacher2
//改变一个实例的属性
person.address.name = '广州市';
person.address.code = '10003';
//不影响另一个实例的属性值
console.log(person2.address.name);//上海2
//不同的实例共享相同的函数,因此在比较时是相等的
console.log(person.getName === person2.getName);//true
//改变一个实例的属性,函数仍然能正确的运行
person2.name = "caoteacher3";
console.log(person.getName());//caoteacher
console.log(person2.getName());//caoteacher3

对象克隆

对象克隆

对象克隆是指通过一定的程序将某个变量的值复制到另一个变量的过程,根据复制后的变量与原始变量值得影响情况,克隆可以分为浅克隆深克隆两种方式。

​ 针对不同的数据类型,浅克隆和深克隆会有不同的表现,主要表现于基本数据类型和引用数据类型在内存中存储的值不同

对应基本数据类型的值,变量存储的是值本身,存放在栈内存的简单数据段中,可以直接进行访问。

对于引用数据类型的值,变量存储的是值在内存中的地址,地址指向内存中的某个位置,如果有多个变量同时指向同一个内存地址,则其中一个变量对值进行修改时,会影响到其他的变量

var arr1 = [1,2,3];
var arr2 = arr1;
arr2[1] = 4;
console.log(arr1);
console.log(arr2);

注意:

  1. 基本数据类型由于数据的差异性,导致克隆之后值的修改不会影响到值的本身。
  2. 引用数据类型如果执行的浅克隆,对克隆后值得修改会影响到原始值,如果执行的是深克隆,则克隆对象和原始对象相互独立,不会独立影响。

对象浅克隆

​ 浅克隆只克隆最外层的属性,若对象存在根更深层的属性,则不进行处理,这就会导致克隆对象和原始对象的深层属性仍然执行同一块内存

简单的引用复制

简单的引用复制,即遍历对象最外层的所有属性,直接将属性值复制到另一个变量中。

//JavaScript实现对象浅克隆--引用复制
function shallowClone(origin) {
    var result = {};
    //遍历最外层属性
    for (var key in origin) {
        //判断是否是对象自身的属性
        if (origin.hasOwnProperty(key)) {
            result[key] = origin[key];
        }
    }
    return result;
}

定义一个具有复合属性的对象,并进行测试

//原始对象
var origin = {
    a: 1,
    b: [2, 3, 4],
    c: {
        d: 'name'
    }
};
//克隆后的对象
var result = shallowClone(origin);
console.log(origin);//[ 1, 4, 3 ]
console.log(result);//[ 1, 4, 3 ]

ES6的Object.assign()函数

在ES6中,Object对象新增了一个**assign()**函数,用于将原对象的可枚举属性赋值到目标对象中。

//原始对象
var origin = {
    a: 1,
    b: [2, 3, 4],
    c: {
        d: 'name'
    }
};
//通过Object.assign()函数克隆对象
var result = Object.assign({}, origin);
console.log(origin);
console.log(result);
//修改克隆对象的内部属性
result.c.d = 'city';
console.log(origin);
console.log(result);

浅克隆实现方案都会存在一个相同的问题,即如果原始对象时引用数据类型的值,则对克隆对象的值的修改会影响到原始对象的值

对象深克隆

JSON序列化和反序列化

JSON.stringify()函数:将原始对象序列化为字符串

JSON.parse()函数:将字符串反序列化为一个对象

这种方法能解决大部分JSON类型对象的深克隆问题,存在几个问题

  1. 无法实现对函数,RegExp等特殊对象的。
  2. 对象的constructor会被抛弃,所有的构造函数会指向Object,原型链关系会破裂。
  3. 对象中如果存在循环引用,会抛出异常。

自定义实现深克隆

在自定义实现深克隆时,需要针对不同的数据类型做针对性的处理,因此我们会先实现判断数据类型的函数,并将所有函数封装在一个辅助类对象中,这里用’_'表示,(类似于underscore类库对外暴露的对象)。

(function(_){    
    //列举可能出现的数据类型    
    var types='Array Object String Date RegExp Function Boolean Number Null Undefined'.split(' ');    
    function type(){        
        //通过调用toString()函数,从索引值为8时截取字符串,得到数据类型的值        
        return Object.prototype.toString.call(this).slice(8,-1);    
    }    
    for(var i= types.length;i--;){        
        _['is'+types[i]] = (function(){            
            return function (elem){                
                return type.call(elem) === self;            
            };        
        })(types[i]);    
    }    
    return _;
})(_={});

执行上面的代码后,对象便具有了isArray()函数,isObject()函数等一系列判断数据类型的函数,然后再调用isArray(param)函数判断param是否是数组类型,调用_isObject(param)函数判断param是否是对象类型。

$.clone()函数和$.extend()函数

jQuery中提供了一个$.clone()函数,但是它是用来复制DOM对象的。真正用于实现克隆的函数是$.extend()

使用$.extend()函数可以实现函数与正则表达式等类型的克隆,还能保持克隆对象的原型链关系,解决了深克隆中存在的3个问题中的前两个,但是却无法解决循环引用的问题。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值