07. JS 面向对象编程

一、对象

  • instanceof
  • 连续调用多个方法
  • 对象属性访问的方法

二、对象属性

三、对象的创建方法

  • 构造函数

四、原型链

五、this

六、call/apply

七、继承模式

  • 圣杯模式

八、对象枚举

  • for in
  • hasOwnProperty

九、JS 对象的深拷贝和浅拷贝

一、对象

在 JS 中一切皆对象,并提供了多个内置对象,比如:String、Array、Date 等,此外还支持自定义对象。对象只是一种特殊类型的数据,并拥有属性和方法,属性是与对象相关的值,方法是能够在对象上执行的动作。
  • 对象是一个引用类型值

  • 创建对象的方式有两种形式:“字面量形式”与“构造函数形式”

  • 对象的属性可以随时修改,且一旦改动,所有引用对象的地方,其属性值均会被改变(这也是引用类型的特点)

  • 对象可以通过原型链实现继承

instanceof:判断对象的具体类型

  • var result = objectName instanceof objectType

  • 如果是指定类型返回true,否则返回false

function Person() {
}
var person = new Person();
// A对象  是不是 B构造函数构造出来的?
//看 A对象的原型链上  有没有 B的原型
// A instanceof B
console.log(person instanceof Person);//true
console.log(person instanceof Object);//true
console.log({} instanceof Object);//true
console.log(person instanceof Array);//false

连续调用多个方法:

var qs = {
    a : function () {
        console.log("aaa");
        return this;//在每个函数的末尾返回this对象,可以实现对象连续调用多个方法
        //因为this就是谁调用这个函数,函数里面的this就指向谁。
    },
    b : function () {
        console.log("bbb");
        return this;
    },
    c : function () {
        console.log("bbb");
    }
}
qs.a().b().c();//aaa bbb ccc

对象访问属性:

  1. 对象.属性 --> obj.name

  2. 对象[字符串形式的属性名] --> obj['name']

  • 内部原理每当你访问 obj.name 的时候,—>本质上系统隐式的访问的是 obj['name']

  • 如果你直接使用这样的方式 obj['name'] 更方便,因为它内部就是这样执行的,这样访问速度更快。

// 示例:实现输入索引就输出对应索引属性名属性的值(就是实现属性名字符串的拼接)
var q = {
    a1 :{name : "dong"},
    a2 :{name : "nan"},
    a3 :{name : "xi"},
    a4 :{name : "bei"},
    sayA :function (num){
        return this['a' + num];
        //实现属性名的拼接,只能用对象['属性名' + ...]的方式
    } 
}
console.log(q.sayA(1));//Object { name: "dong" }
console.log(q.sayA(2));//Object { name: "nan" }
console.log(q.sayA(3));//Object { name: "xi" }
console.log(q.sayA(4));//Object { name: "bei" }

二、对象属性

JavaScript对象有三个内部方法和一个特征属性,他们分别是:

  • 内置方法:[[Put]][[Set]][[Delete]]

  • 特征属性:[[Extensible]]

    • 它的值是一个布尔值,用来表示对象本身是否可以被修改,简单来说对象是否可以添加属性- 返回布尔值.

    • Object.isExtensible()查询一个对象是否可以修改.

    • Object.preventExtensions()使一个对象变成不可修改.

var o = {x:1};
console.log(Object.isExtensible(o)); //true

Object.preventExtensions(o);
console.log(Object.isExtensible(o));    //false

注意: 由于没有Object.preventExtensions()的反操作语句,因此一旦某个对象被设定为非extensible后,将没有办法重新再将其设定为extensible。
Object.preventExtensions()语句作用的范围是对象自身,原型对象不受影响。如果某个对象被设定为非extensible,那么其原型对象中依然可以动态添加property,而这些动态添加的property也依然可以被对象继承到。

数据属性 & 访问器属性:

  • 一个js对象(Object、Function)可以定义两种类型的属性:数据属性访问器属性

  • 数据属性和访问器属性有两个共通的特征可以设置:

    • enumerable 是否能够被 for-in 遍历枚举

    • configurable 是否能够被 delete 关键字删除

1. 数据属性

数据属性的特征除了共同特征两种外,还有两种

  • value 定义值

  • writable 是否能够被重新赋值

数据属性是基本的值属性,我们使用普通方法创建对象时 var obj = {age:12} ,产生的属性就是数据属性,并且其 enumerableconfigurablewritable 默认设置为true

var person = {
    age:12
}
Object.getOwnPropertyDescriptor(person,'age'); 
//Object {value: 12, writable: true, enumerable: true, configurable: true}
修改默认的特性:

Object.defineProperty(obj, prop, descriptor)

  • obj 要在其上定义属性的对象

  • prop 要定义或修改的属性的名称

  • descriptor 将被定义或修改的属性描述符

var person = {}
Object.defineProperty(person,'name',{
    configurable:false,
    enumerable:false,
    writable:false,
    value:'xiaoming'
});

console.log(person);    //name:'xiaoming'

person.name = 'qiang';
console.log(person);    //name:'xiaoming
//因为writable设置为不可修改属性值

for(var i in person){   //无结果,不可循环
    console.log(person[i])
}

delete person.name;
console.log(person.name);   //'xiaoming' 不可删除

Object.defineProperty(person,'name',{
    configurable:true   //不可修改,将抛出错误.
});

注意: 一旦把 configurable:false定义为不可配置后,再调用object.defineProperty()方法修改除writable之外的特性,都会导致错误.
也就是说可以多次调用object.defineProperty()方法修改同一个属性,但在把configurable特性设置为false之后就会有限制了.

2. 访问器属性

访问器属性的特征除了共同特征两种外,还有两种:

  • get 访问该属性时自动调用, 默认值为undefined

  • set 重新赋值该属性时自动调用, 默认值为undefined

  • 访问器属性不包含数据值;它们包含一对儿gettersetter函数.

  • 在读取访问器属性时,会调用getter函数,这个函数负责返回有效的值.

  • 在写入访问器属性时,会调用setter函数并传入新值,这个函数负责决定如何处理数据.

  • 单独定义 get 表示该属性只可访问,不可重写, 只制定setter意味着属性不能访问.

访问器属性可以方便我们在属性赋值或访问时做其他的一些操作,在定义getset方法时,注意不要使用this来访问本属性,会造成无限循环导致内存溢出。

var book = {
    _year: 2004,    //属性前面加_,代表属性只能通过对象方法访问
    edition: 0
}
Object.defineProperty(book,'year',{
    get: function(){    //get函数返回_year的值
        return this._year;
    },
    set: function(newValue){
        if(newValue > 2004){
            this._year = newValue;
            this.edition += newValue - 2004
        }
    }
});

console.log(book.year); //2004
book.year = 2006;
console.log(book.year); //2006
console.log(book.edition);  //2
定义多个属性 - Object.defineProperties
var book = {};
Object.defineProperties(book,{
    _year:{
        value:2004,
        writable:true
    },
    edition:{
        value: 0,
        writable:true 
    },
    year: {
        get: function(){    //get函数返回_year的值
            return this._year;
        },
        set: function(newValue){
            if(newValue > 2004){
                this._year = newValue;
                this.edition += newValue - 2004
            }
        }
    }
});

console.log(book.year); //2004
book.year = 2006;
console.log(book.year); //2006
console.log(book.edition); //2
获取属性特征属性 - Object.getOwnPropertyDescriptor()
var book = {};
Object.defineProperties(book,{
    _year:{
        value:2004,
        writable:true
    },
    edition:{
        value: 0,
        writable:true 
    },
    year: {
        get: function(){    //get函数返回_year的值
            return this._year;
        },
        set: function(newValue){
            if(newValue > 2004){
                this._year = newValue;
                this.edition += newValue - 2004
            }
        }
    }
});

console.log(book.year); //2004
book.year = 2006;
console.log(book.year); //2006
console.log(book.edition); //2

//读取属性
var descriptor__year = Object.getOwnPropertyDescriptor(book,'_year');

//返回指定对象所有自身属性(非继承属性)的描述对象
var descriptor_year = Object.getOwnPropertyDescriptor(book,'year');

console.log(descriptor__year);
/*
Object { 
    value: 2006, 
    writable: true, 
    enumerable: false, 
    configurable: false }
*/
console.log(descriptor_year);
/*
Object { 
    get: get(), 
    set: set(), 
    enumerable: false, 
    configurable: false }
*/
对象封印与对象冻结:
  • 对象封印: 通过使用Object.seal()方法使一个对象不仅不可扩展,其所有的属性都不可配置,也就是说,对于一个被封印的对象,你不能:

    • 添加新属性

    • 删除属性或改变属性类型

    • 当一个对象被封印时,你只能读写它已有的属性。另外,我们可以通过Object.isSealed()方法检验一个对象是否为被封印对象

let obj = {
    x: 1,
};

console.log(Object.isExtensible(obj));  //true
console.log(Object.isSealed(obj));  //false

//封印对象
Object.seal(obj);
console.log(Object.isExtensible(obj));  //false
console.log(Object.isSealed(obj));  //true

obj.y = 2;
console.log('y' in obj);    //false

obj.x = 3;
console.log(obj.x); //3

delete obj.x;
console.log(obj.x); //3
  • 对象冻结: 对象冻结则更近一步,将对象属性的操作限制为只读,它更像是一个对象某一时刻的快照,除了看之外我们不能对它有任何操作。
    在JavaScript中,我们使用Object.freeze()冻结一个对象,并且使用Object.isFrozen()来判断一个对象是否被冻结。

三、对象的创建方法

1. 对象字面量(plainObject)

  • var obj = {}

  • 系统默认 new Object()

2. 构造函数

  • 系统自带的原生构造函数 (Arry,Object)

    • 自动出现在执行环境中

    • var obj = new Object()

  • 自定义

  • 按照惯例,构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头。

构造函数内部原理:

  • 普通的函数加上 new 就变成构造函数

    • 在函数体最前面隐式的加上this = {}

    • 执行 this.xxx = xxx

    • 隐式的返回 this

function Student(name,age,sex){
    // var this = Object.create(Student.prototype); // 内部标准形式 //隐式

    //var this = {
    //    name:...,
    //    age:...,
    //    ...
    };    // 隐式
    
    this.name = name;
    this.age = age;
    this.sex = sex;
    this.grade = 2017
    
    // return this;
}

var student1 = new Student;
var student2 = new Student;

// student1 和 student2 分别保存着 Student 的一个不同的实例

如果不写 new,这就是一个普通函数,它返回 undefined。但是,如果写了new,它就变成了一个构造函数,它绑定的 this 指向新创建的对象(实例),并默认返回 this,也就是说,不需要在最后写 return this

function Person(name,age,sex){
    var a = 0;
    this.name = name;
    this.age = age;
    this.sex = sex;
    function sss(){
        a ++;
        console.log(a);
    }
    this.say = sss;
}

var person1 = new Person();
person1.say();  // 1
person1.say();  // 2

var person2 = new Person(); 
// 函数 Person 执行的时候重新生成新的 AO,并且函数 sss 继承是的新的作用域
// person1 person2 互不相干的
person2.say();  // 1

3. Object.create()

var demo = {
    lastName : 'deng'
}
var obj = Object.create(demo);  

//obj = {
//    __proto__:demo
//}

四、原型链

1. 所有引用类型(数组、对象、函数)都有一个特殊的隐式原型属性叫做__proto__,你可以用这个属性去关联另外一个对象(这个对象就是所谓的原型了)

  • 使用 __proto__ 可以自己指定原型,但是系统是不会认的,会返回 undefined。

2. 创建一个函数对象,就会根据一组特定的规则为该函数创建一个 prototype 属性,这个属性指向函数的原型对象。这个对象的所有属性和方法,都会被构造函数的实例继承。

3. 所有的引用类型(数组、对象、函数),_proto_ 属性值(隐式原型属性)指向它的构造函数的 prototype 属性值

var obj = {};   // new Object
console.log(obj.__proto__ === Object.prototype) // true
// 构造函数
function Foo(name,age){
    this.name = name
}
Foo.prototype.alertName = function(){
    console.log(this.name)
}
// 创建示例
var f = new Foo('zhangsan');
f.printName = function(){
    console.log(this.name)
}
// 测试
f.printName();  // zhangsan
f.alertName();  // zhangsan

// 原型链 -- > 查找属性顺序
// f ----> Foo.prototype ----> Object.prototype ----> null

// console.log(f.__proto__ === Foo.prototype); --> true
// console.log(Foo.prototype.constructor === Foo); --> true

// f instanceof Foo // true

4. 在默认情况下,所有原型对象都会自动获得一个 constructor (构造函数)属性,这个属性包含一个指向 prototype 属性所在函数的指针。

  • Foo.prototype.constructor --> Foo

  • 创建了自定义的构造函数之后,其原型对象默认只会取得 constructor 属性;至于其他方法,则都是从 Object 继承而来的

  • 原型最初只包含 constructor 属性,而该属性也是共享的,因此可以通过对象实例访问。

5. 更简单的原型语法

function Person(){

}

Person.prototype.name = 'Nicholas';
Person.prototype.sayName = function(){
    console.log(this.name);
};

var person1 = new Person();
var person2 = new Person();

console.log(Person.prototype.constructor === Person;) // true
// Person 原型的构造函数是 Person

  • 为减少不必要的输入,也为了从视觉上更好地封装原型的功能,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象

  • 但有个列外:constructor 属性不再指向 Person 了。

    • 每创建一个函数,就会同时创建它的 prototype 对象,这个对象也会自动获得 constructor 属性。而我们在这里使用的语法,本质上完全重写了默认的 prototype 对象,因此 constructor 属性也就变成了新对象的 constructor 属性(指向 Object 构造函数),不再指向 Person 函数。

    • 如果 constructor 的值真的很重要,可以像下面这样特意将它设置回适当的值

function Person(){

}

Person.prototype = {
    // constructor:Person, 
    name: "Nicholas",
    age: 29,
    job: "Software Engineer",
    sayName: function(){
        console.log(this.name);
    }
};

console.log(Person.prototype.constructor === Person;) // false

Person.prototype.constructor = Person;  // 将 constructor 属性重新指向了 Person 
console.log(Person.prototype.constructor === Person;) // true

6. 实列方法的重写

  • 虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。

  • 如果我们在实例中添加了一个属性,而该属性与实例原型中的一个属性同名,那我们就在实例中创建该属性(这个实例独有属性),该属性将会屏蔽原型中的同名属性,但不会修改。

  • `delete` 操作符则可以完全删除实例属性,从而让我们能够重新访问原型中的属性
function Person(){

}
Person.prototype = {
    constructor: Person,
    name: "Nicholas",
    age: 29,
    job : "Software Engineer",
    sayName : function(){
        console.log(this.name);
    }
}
var person1 = new Person();
var person2 = new Person();

person1.name = "Greg";
//person1原型中的name被新值屏蔽 // 只会屏蔽不会修改

console.log(person1.name); // "Greg" - 来自实例
console.log(person2.name); // "Nicholas" - 来自原型

delete person1.name;
console.log(person1.name); // "Nicholas" - 来自原型

7. 原型的动态性

  • 由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来——即使是先创建了实例后修改原型也照样如此。
var friend = new Person();
Person.prototype.sayHi = function(){
    console.log("hi");
};
friend.sayHi(); // "hi" 没有问题
  • 实例与原型之间的连接只不过是一个指针,而非一个副本,因此就可以在原型中找到新的sayHi 属性并返回保存在那里的函数。

  • 尽管可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来,但如果是重写整个原型对象,那么情况就不一样了。
  • 调用构造函数时会为实例添加一个指向最初原型的指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。请记住:实例中的指针仅指向原型,而不是指向构造函数。

function Person(){

}

var friend = new Person();

Person.prototype = {
    constructor : Person,
    name: "Nicholas",
    age: 29,
    job: "Software Engineer",
    sayName: function(){
        console.log(this.name);
    }
};
//friend.sayName(); //error

var koko = new Person();
//之后创建的实例将会指向重写后的原型,所以不会报错
koko.sayName(); //  ----- Nicholas

8. 原型的验证方法:

  1. isPrototypeOf()
  • 这个方法用来判断,某个原型对象和某个实例之间的关系。
console.log(Person.prototype.isprototypeOf(person1)); //true
console.log(Person.prototype.isprototypeOf(person2)); //true
  1. Object.getPrototypeOf()
  • 这个方法返回 [[Prototype]] 的值,可以方便地取得一个对象的原型
console.log(Object.getPrototypeOf(person1) == Person.prototype); //true
//返回的对象实际就是这个对象的原型
console.log(Object.getPrototypeOf(person1).name); //"Nicholas"
  • 支持这个方法的浏览器有 IE9+、Firefox 3.5+、Safari 5+、Opera 12+ 和 Chrome

五、this

  • 在 JS 中 this 总是指向调用它所在方法的对象,因为 this 是在函数运行时,自动生成的一个内部对象,只能在函数内部使用

  • 在函数中 this 到底取何值,是在函数真正被调用执行的时候确定的,函数定义的时候确定不了.因为 this 的取值是执行上下文环境的一部分,每次调用函数,都会产生一个新的执行上下文环境。

1. 函数预编译过程 this 指向 window

function test(c){
    var a = 123;
    function b(){
    
    }
}

//AO{
//    arguments:[1],
//    this:window,
//    c:1,
//    a:undefined,
//    b:function(){
//  }
//}

test(1);

2. 全局作用域里 this --> window

3. 对象方法的调用

  • 谁调用的方法,这个方法里的 this 就指向谁

  • obj.say() 是调用函数,obj.say 是调用函数引用并且赋值给某个变量。

var name = '222';
var a = {
    name : '111',
    say : function(){
        console.log(this.name);
    }
}
var fun = a.say;    // a.say 是函数引用,相当于将函数直接放到了 fun 上。
fun();  // 222  // 全局执行的函数,this 指向 window。
a.say();    // 111
var b = {
    name : '333',
    say : function(fun){
    // this --> b
        fun(); 
        // 相当于执行了 a.say 函数。
        // 因为没有人调用 fun() ,所以 a.say 方法就按照它自己的预编译找寻 this
        // a.say 方法 this --> window
    }
}
b.say(a.say);   // 222
b.say = a.say;
b.say();    // 333

4. 构造函数的调用

  • 构造函数中的 this 指向新创建的对象本身
var name = "global name";

function Person(){
    this.name = "Tom";
}

var person1 = new Person();

console.log(person1.name); // Tom
console.log(name); //global name
  • 在构造函数的内部,我们对 this.name 进行赋值,但并没有改变全局变量 name。

5. 关于this笔试题目

原题目:

var name = "222";
var a = {
    name : "111",
    say  :function () {
        console.log(this.name);
    }
}
var fun = a.say;
fun()
a.say()
var b = {
    name : "333",
    say : function (fun) {
        fun();
    }
}
b.say(a.say);
b.say = a.say;
b.say();
// 222 111 222 333

分析:

var name = "222"; 
var a = { 
    name : "111", 
    say :function () { 
        console.log(this.name); 
    } 
} 
var fun = a.say;//a.say代表函数的引用,代表函数体 
fun();//就是把 
// function () { 
//      console.log(this.name); 
// }放到全局范围内执行,也没人调用它,所以this指向window,所以打印222 

a.say()// 这个就是对象调用这个方法,this指向对象a,打印的就会是111 
var b = { 
    name : "333", 
    say : function (fun) { 
        //this---->b 
        fun(); 
    } 
} 
b.say(a.say);//b.say,对象b调用它的say方法,现在b的say方法里面的this指向b //然后里面放进去一个a.say,也就是把
//function () { 
//  console.log(this.name); 
//}放到b的say函数里面执行,这个函数执行只不过是在另外一个函数里面,也没人调用它,这个时候走的就是预编译的环节,this它就指向window,打印出来的就是222 

b.say = a.say;//把a.say放到b.say里面,那就是var b = { 
                                        //      name : "333", 
                                        //      say : function () { 
                                        //          console.log(this.name); 
                                        //       } 
                                        //  } 
b.say();//再调用b.say方法当然是this指向自己,打印333

讨论this指向的问题:

1 
var foo = '123'; 
function print(){ 
    var foo = '456'; 
    this.foo = "789";//这个this指向window 
    console.log(foo);//456,打印自己AO里面的foo 
} 
print(); 
---------------------------------------------------------------------------
2 
var foo = 123; 
function print() { 
    this.foo = 234;//this指window console.log(foo);//打印GO里面的foo,因为自己AO里面没有foo这个属性 
} 
print(); 
--------------------------------------------------------------------------------
3 
var foo = 123; 
function print() { 
    //var this = Object.create(print.prototype) 
    this.foo = 234; 
    console.log(foo);//123 
} 
new print();//因为new一个的话会在print里面隐式的生成一个this, 
// 这个时候的this有人了,var this = Object.create(print.prototype), 
// 然后 this.foo = 234;然而并没有什么用, 
// 我现在访问的是foo,又不是this上的foo,AO里面又没有foo,所以去GO里面找 
------------------------------------------------------------------------------
4 
运行test()new test()的结果分别是什么? 
var a = 5; 
function test() { 
    a = 0; 
    alert(a); 
    alert(this.a); 
    var a; 
    alert(a); 
} 
0 5 0 /0 undefined 0 
-------------------------------------------------------------------------------
5 
function print() { 
    console.log(foo);//undefined 
    var foo = 2; 
    console.log(foo);//2 
    console.log(hello); //报错hello is not defined 
} 
print(); 
---------------------------------------------------------------------------------
6 
function print() { 
    var test; 
    test(); 
    function test() { 
        console.log(1);//1 
    } 
} 
print(); 
----------------------------------------------------------------------------------
7 
function print () { 
    var marty = { 
        name : "marty", 
        printName : function () {console.log(this.name); } 
    } 
    
    var test1 = {name : test1}; 
    var test2 = {name : test2}; 
    var test3 = {name : test3}; 
    test3.printName = marty.print; 
    var printName2 = marty.printName.bind({name : 123}); 
    
    marty.print.call(test1); 
    marty.print.apply(test2); 
    marty.printName(); 
    printName2(); 
    test3.printName(); 
} 
print(); 
-------------------------------------------------------------------------------
8 
var bar = {a : "002"}; 
function print() { 
    bar.a = 'a'; 
    Object.prototype.b = 'b'; 
    return function inner() { 
        console.log(bar.a);//a 
        console.log(bar.b);//b 
    } 
} 
print()();//第一个函数括号就是返回inner函数,第二个函数就是执行inner函数

六、call/apply

call()apply() 是预定义的函数方法,两个方法可在特定的作用域中调用函数,等于设置函数体内this对象的值,以扩充函数赖以运行的作用域.
  • call:需要把实参按照形参的个数传进去(连续参数)

  • apply:需要传一个 arguments (实参列表)

  • 用处:改变 this 指向

  • 区别:传参列表不同


// call() 改变 this 指向 // this 默认指向是 window
// 借用别人的函数
// test();  ---> test.call() 没区别
// apply() // 第一位改变this指向,第二位参数数组

function Person(name,age){
    // var this = Object.create(Person.prototype);
    
    this.name = name;   // obj.name = name;
    this.age = age      // obj.age = age;
}

var person = new Person('zheng',100);
var obj = {}

Person.call(obj,'hai',300);   // Person 中的 this 都指向 obj
                              // call() 第一位会改变 this 指向,后面的就是对应实参。
obj.name; // 'hai'
function Person(name,age,sex){
    this.name = name;
    this.age = age;
    this.sex = sex;
}

function Student(name,age,sex,tel,grade){
    Person.call(this,name,age,sex); // 借用了 Person 的方法和属性
    // Person.apply(this,[name,age,sex]);
    this.tel = tel;
    this.grade = grade;
}

var student = new Student('sunny',123,'male',139,2019);
示例:
    function add(c,d){
        return this.a + this.b + c + d;
    }

    var s = {a:1, b:2};
    console.log(add.call(s,3,4)); // 1+2+3+4 = 10
    console.log(add.apply(s,[5,6])); // 1+2+5+6 = 14 
示例:
    window.firstName = "Cynthia"; 
    window.lastName = "_xie";

    var myObject = {firstName:'my', lastName:'Object'};

    function getName(){
        console.log(this.firstName + this.lastName);
    }

    function getMessage(sex,age){
        console.log(this.firstName + this.lastName + " 性别: " + sex + " age: " + age );
    }

    getName.call(window); // Cynthia_xie
    getName.call(myObject); // myObject

    getName.apply(window); // Cynthia_xie
    getName.apply(myObject);// myObject

    getMessage.call(window,"女",21); //Cynthia_xie 性别: 女 age: 21
    getMessage.apply(window,["女",21]); // Cynthia_xie 性别: 女 age: 21

    getMessage.call(myObject,"未知",22); //myObject 性别: 未知 age: 22
    getMessage.apply(myObject,["未知",22]); // myObject 性别: 未知 age: 22

七、继承模式

1. 传统模式:原型链

  • 过多的继承了没用的属性

2. 借用构造函数

  • 使用 call/apply 来借用其它对象的函数方法和属性

  • 不能继承借用构造函数的原型

  • 每次构造函数都要多走一个函数(借用的函数)

  • 如果你只是想要借用某个函数中的方法和属性,就用这个模式借用过来使用会便捷很多。

3. 共享原型

  • 不能随便改动自己的原型
// 多个构造函数公用同一个原型 A 继承 B 的原型。
Father.prototype.lastName = 'zheng';
function Father(){
    
}
function Son(){
    
}
Son.prototype = Father.prototype;   // 直接让 Son 的原型变成了 Father 的原型
var son = new Son();
console.log(son.lastName); // 'zheng'

4. 圣杯模式(完美模式)

//当前案例是一个圣杯模式案例
Father.prototype.lastname = 'C';
Father.prototype.fortune = 1000000;
function Father () {
    this.age = 48;
}
function Son () {
    this.age = 18;
    this.waste = function () {
        return this.fortune - 50000;
    }
}
var inherit = (function () { //创建圣杯inherit函数
/* 使用立即函数的原因:函数执行前会进行预编译,预编译过程都会产生AO,
如当前案例所示,案例中的立即执行函数(注:以下简称立函)执行前预编译的AO中有buffer函数,
由于当立函执行完毕时会返回一个匿名函数(注:以下简称匿函),这个匿函调用了buffer函数,
最终匿函也被赋予到了inherit函数中,导致立函执行前预编译产生的AO在立函执行完毕后并不会销毁,
于是buffer函数成为了一个闭包并被一同赋予到了inherit函数中去了,
这样当在外部使用inherit函数时,将会一直都在使用一个buffer函数,
而不用每次使用时都再新建一个buffer函数 */
return function (targetSon, originFather) { //让目标儿子继承源头父亲
    function buffer () {} //buffer函数是一个闭包,仅用做一个缓冲而不做他用
    buffer.prototype = originFather.prototype; 
    //targetSon.prototype = buffer.prototype; /* 不能这么写,因为这样写就相当于对象targetSon、fatherOrigin和buffer共享原型了 */
    targetSon.prototype = new buffer(); /* 使对象targetSon试图修改自身属性时仅仅是以buffer函数作为对象进行修改,而不会影响到其他对象 */
    targetSon.prototype.constructor = targetSon; //令目标儿子记得自己本质是谁
    targetSon.prototype.gene = originFather; //令目标儿子记得自己的生父是谁
    }
    })()    // 闭包封装,实现buffer函数的私有化
inherit(Son, Father); //调用圣杯inherit函数
Son.prototype.lastname = 'X';
var son = new Son();
var father = new Father();
console.log(son.lastname); //控制台显示x,败家儿子成功认贼作父
console.log(father.lastname); /* 控制台显示c,父亲自己的姓并没有因为败家儿子
                                 通过改姓来认贼作父的惨痛事实而改变 */
console.log(son.constructor); //控制台显示儿子自己的构造函数(本质)
console.log(son.gene); //控制台显示儿子自己的生父


// 如上例所示可瞧见,通过圣杯模式可以把一个家族中的各个对象的干扰给截断,以使每个对象在对父类有继承的情况下相互独立,以免各个对象在试图修改自身(特别是自身原型)的属性时影响到其他对象。

八、对象枚举

1. for in

var obj = {
    name : '13',
    age : 89,
    sex : "male",
    height : 180,
    weight : 75
}
for(var prop in obj) {
   //遍历对象属性,通过对象属性个数来控制循环圈数
   console.log(prop + " " + typeof(prop));
   //name string
   //age string
   //sex string
   //height string
   //weight string
      
   console.log(obj.prop); // obj['prop']
   //undefined undefined undefined undefined undefined
   //这个时候系统是把prop当做obj的一个属性,因为它没有这个属性所以会输出undefined
 
   console.log(obj[prop]);
   //用obj[prop]就是正确的,这样就会把它当做一个变量来看,不能加上obj['prop'],prop现在本来就是一个字符串
}

2. hasOwnProperty 属性

  • 过滤掉原型上的属性,留下只属于自己的属性

  • hasOwnProperty() 括号里面传进去的是要判断属性名的字符串形式

  • 经常和 for in 一起使用

var obj = {
    name : '13',
    age : 89,
    sex : "male",
    height : 180,
    weight : 75,
    __proto__ : {   //手动给原型上添加一个属性
        lastName  : "deng"
  }
}

Object.prototype.abc = "123";
for (var prop in obj){
   for (var prop in obj){
   if(obj.hasOwnProperty(prop)){    //过滤掉原型上的属性
    console.log(obj[prop]); //13 89 male 180 75
   }   
}
for (var prop in obj){
    //  !hasOwnProperty
   if(!obj.hasOwnProperty(prop)){
    console.log(obj[prop]);//deng 123
   }   
}

// hasOwnProperty 只会拿原型上自己设置的的属性,不会拿原型链最顶端系统自带 Object.prototype 的
// 如果加一行 Object.prototype.abc ='123' 这样是会拿出来的,因为 abc 属性是我们自己设置的。

3. in (很少使用)

  • 只能判断这个对象能不能访问到这个属性,包括原型上的。

  • 语法格式:属性的字符串形式 in 对象 (不能直接使用属性名,会被当成变量)

console.log('height' in obj);   //true
console.log('lastName' in obj); //true 
console.log(height in obj); //ReferenceError: height is not defined

九、JS 对象的深拷贝和浅拷贝(clone方法)

  • 原始值:存储在栈里的,而且存储的是变量的实际值

  • 引用值:存储在堆里,且存储的是一个指针,该指针指向内存中的某个位置,该位置存储变量的实际值

  • 如果现有var obj1 = {...}这个对象,想要复制对象obj1,一贯的做法就是obj2 = obj1,这时虽然obj2拥有了obj1的所有属性,但obj2却不是自由的,因为它的改动会影响到obj1obj1的改动也会影响到obj2,这不是我们所希望的,所以要用到深拷贝和浅拷贝。

浅拷贝:

  • 浅拷贝引用值是要相互影响的,因为它是栈内存之间的赋值,赋值的是地址,一个更改,其他的都要更改。
var obj = {
        a:10,
        b:20
    };
function copy(obj) {
     var newobj={};  
     for(arr in obj){
        newobj[arr]=obj[arr]
     }
     return newobj;
}
obj2 = copy(obj); //成功复制出obj
console.log(obj2); // Object {a: 10, b: 20}
obj2.a = 5; //更改了obj2的a 
console.log(obj2.a); //5
console.log(obj.a); //10,obj2的改变不影响obj,因为对象 obj 里面只有原始值没有引用值。
  • 浅拷贝可以解决常见的现象,但倘若对象不是常见的那种呢?比如说对象里还有子对象,那么用浅拷贝就不够彻底
var obj = {
        a: 10,
        b: 20,
        omg: {
            name: 'xuguojun',
            sex: 'male'
        },
        arrBt:['1','2','3']
} 
function copy(obj){
    var newobj = {};
    for(arr in obj){
        newobj[arr] = obj[arr]
    }
    return newobj;
}
obj2 = copy(obj);
console.log(obj2); //成功复制出obj
obj2.omg.name = 'PDD'; //改变obj2.omg.name的值为'PDD'
console.log(obj2.omg.name); // PDD
console.log(obj.omg.name); //PDD // obj.omg.name的值也随着改变 // 因为 obj里面有个引用值 omg

obj2.arrBt.push('4');
console.log(obj2.arrBt);    // ["1", "2", "3", "4"]
console.log(obj.arrBt);     // ["1", "2", "3", "4"] // obj 里面的 arrBt 一起改变因为是引用值
  • Object.assign(要拷贝谁,拷贝给谁)
var obj = {
	 a:1,
	 b:2,
	 c:3
}
var obj2 = {};
obj2 = Object.assign(obj,obj2);
console.log(obj2);//这样的也是一个浅拷贝,当obj的属性的值改变了,那么obj2的属性的值也跟着变

深度拷贝:无论是原始值还是引用值,修改后彼此相互不影响

// 遍历对象   for(var prop in obj)(for in也可以遍历数组(数组也是特殊类型的对象))
// 1. 判断是不是原始值  typeof() object--->引用值
// 2. 判断是数组还是对象 constructor,instanceof,toString
// 3. 建立相应的数组或者对象
// 4. 递归

var obj = {
    name : "abc",
    age : 123,
    card : ['visa','master'],
    wife : {
        name : "bcd",
        son : {
            name : "aaa"
        }
    }
}

var obj1 = {
    // name : obj.name,
    // age : obj.age,
    // card : [obj.card[0],obj.card[1]],
    // wife : {
    //     name : "bcd",
    //     son : {

    //     }
    // }
}

function deepClone(origin, target){
    var target = target || {},
        toStr = Object.prototype.toString,
        arrStr = "[Object Array]";

        for(var prop in origin){  
            if(origin.hasOwnProperty(prop)){    // 过滤原型对象的属性
                if(origin[prop] !== "null" && typeof(origin[prop]) == 'object'){
                // prop 绝对不等于 null 并且是 object 才可以

                //    if(toStr.call(origin[prop]) == arrStr){
                //      target[prop] = [];
                //    }else{
                //      target[prop] = {};
                //    }   
                //  上面的 if else条件判断可以用下面的三目运算符代替: 
                target[prop] = toStr.call(origin[prop]) == arrStr ? [] : {};// 三目运算符
                deepClone(origin[prop],target[prop]);  // 递归
                
                }else{  // prop 是原始值的情况
                    target[prop] = origin[prop];
                }
            }
        }
        return target;  // 如果一开就没有传 target 最后就需要返回。
}
deepClone(obj,obj1);

// 用JSON的方式也可以实现一定程度上的深拷贝
var obj = {
    name: "panda",
    age: 18,
    msg: {
        a: 1,
        b: 2
    },
    arr: [1, 2, 3]
}
var str = JSON.stringify(obj);
//转化为JSON对象,这个JSON对象str就是一个全新的对象,和obj就完全没关系了。
var obj2 = JSON.parse(str);
//这个全新的JSON对象str再转化为普通的对象的时候,那么obj2和obj也就没关系了。
console.log(obj2);
 function deepCloneArray(arr, newArr) {
    for (var i = 0; i < arr.length; i++) {
        if (arr[i] instanceof Object || arr[i] instanceof Array) {
            var tempNewObj = {};
            newArr[i] = deepClone(arr[i], tempNewObj);
        } else {
            newArr[i] = arr[i];
        }
    }
    return newArr;
}

function deepCloneObject(obj, newObj) {
    for (var temp in obj) {
        if (obj.hasOwnProperty(temp)) {
            if (obj[temp] instanceof Object || obj[temp] instanceof Array) {
                var tempNewObj = {};
                newObj[temp] = deepClone(obj[temp], tempNewObj);
            } else {
                newObj[temp] = obj[temp];
            }
        }
    }
    return newObj;
}

function deepClone(obj, newObj) {
    //数组一定要在对象前面判断
    if (obj instanceof Array) {
        //处理数组
        newObj = [];
        return deepCloneArray(obj, newObj);
    } else if (obj instanceof Object) {
        //处理对象
        newObj = {};
        return deepCloneObject(obj, newObj);
    } else {
        //可能传了个数,可能传了个字符串,也就是原始值
        return newObj = obj;
    }
}
var obj = {
    name: "panda",
    age: 18,
    msg: {
        a: 1,
        b: 2
    },
    arr: [1, 2, 3]
}
var newObj = {};
newObj = deepClone(obj, newObj);
console.log(newObj);
obj.age = 16;
obj.msg.a = 6;
obj.arr[1] = 666;
console.log(newObj);

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值