JS面向对象笔记二

Posted 谈晓鸣

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JS面向对象笔记二相关的知识,希望对你有一定的参考价值。

菜单导航,《JS面向对象笔记一》,  参考书籍:阮一峰之《JavaScript标准参考教程》

一、构造函数和new命令

二、this关键字

三、prototype对象

四、对象的Copy和OOP三大特性模拟(继承、多态、封装)

五、异步执行之Ajax和Promise

六、DOM对象

七、元素的clientWidth、offsetWidth、scrollWidth的区别

 

一、构造函数和new命令

1、构造函数

  • javascript语言的对象体系,不是基于“类”的,而是基于构造函数(constructor)和原型链(prototype)
  • 为了与普通函数区别,构造函数名字的第一个字母通常大写,比如:  var Person = function(){  this.name = \'王大锤\';  }
  • 构造函数的特点: 
           a、函数体内部使用了this关键字,代表了所要生成的对象实例;
       b、生成对象的时候,必需用new命令调用此构造函数

2、new 

  作用:就是执行构造函数,返回一个实例对象 

var Person = function(name, age){
    this.name = name;
    this.age = age;
    this.email = \'cnblogs@sina.com\';
    this.eat = function(){
        console.log(this.name + \' is eating noodles\');
    }
}

var per = new Person(\'王大锤\', 18);
console.log(per.name + \', \' + per.age + \', \' + per.email); //王大锤, 18, cnblogs@sina.com
per.eat();  //王大锤 is eating noodles

执行new命令时的原理步骤:

  1. 创建一个空对象,作为将要返回的对象实例
  2. 将这个空对象的原型,指向构造函数的prototype属性
  3. 将这个空对象赋值给函数内部的this关键字
  4. 开始执行构造函数内部的代码

注意点:当构造函数里面有return关键字时,如果返回的是非对象,new命令会忽略返回的信息,最后返回时构造之后的this对象;
  如果return返回的是与this无关的新对象,则最后new命令会返回新对象,而不是this对象。示例代码:

console.log(\'---- 返回字符串 start ----\');
var Person = function(){
    this.name = \'王大锤\';
    return \'罗小虎\';
}

var per = new Person();
for (var item in per){
    console.log( item + \': \' + per[item] );
}
//---- 返回字符串 start ----
//name: 王大锤

console.log(\'----- 返回对象 start ----\');
var PersonTwo = function(){
    this.name = \'倚天剑\';
    return {nickname: \'屠龙刀\', price: 9999 };
}
var per2 = new PersonTwo();
for (var item in per2){
    console.log(item + \': \' + per2[item]);
}
//----- 返回对象 start ----
//nickname: 屠龙刀
//price: 9999
View Code

   new命令执行的内部过程,可以用下面的代码模拟:

/*
    第一个参数:constructor 表示构造函数名
    后面的params指该构造函数需要传递的参数,可以多个参数
*/
function testNew(constructor, params){
    //1、将当前函数testNew的所有参数arguments转成数组
    var args = [].slice.call(arguments);

    //2、取出数组args中的第一个元素,也就是构造函数名constructor
    var constructor = args.shift();

    //3、创建一个空对象,继承构造函数的prototype属性
    var context = Object.create(constructor.prototype);

    //4、传入构造函数参数,执行构造函数
    constructor.apply(context, args);

    //5、返回实例化的对象
    return context;
}


//测试
var Person = function(name, age){
    this.name = name;
    this.age = age;
    this.run = function(){
        console.log(this.age + "岁的" + this.name + "正在跑步");
    }
}

var per = testNew(Person, "王大锤", 18);

console.log( per );
per.run();
/*
Person {name: "王大锤", age: 18, run: ƒ}
18岁的王大锤正在跑步
*/
View Code

 

如果调用构造函数的时候,忘记使用new关键字,则构造函数里面的this为全局对象window,属性也会变成全局属性,

则被构造函数赋值的变量不再是一个对象,而是一个未定义的变量,js不允许给undefined添加属性,所以调用undefined的属性会报错。

示例:

var Person = function(){ 
    console.log( this == window );  //true
    this.price = 5188; 
}
var per = Person();
console.log(price); //5188
console.log(per);  //undefined
console.log(\'......_-_\'); //......_-_
console.log(per.price); //Uncaught TypeError: Cannot read property \'helloPrice\' of undefined
View Code

为了规避忘记new关键字现象,有一种解决方式,就是在函数内部第一行加上 : \'use strict\';

表示函数使用严格模式,函数内部的this不能指向全局对象window, 默认为undefined, 导致不加new调用会报错

var Person = function(){ 
    \'use strict\';
    console.log( this );  //undefined
    this.price = 5188; //Uncaught TypeError: Cannot set property \'helloPrice\' of undefined
}

var per = Person(); 
View Code

另外一种解决方式,就是在函数内部手动添加new命令:

var Person = function(){ 
    //先判断this是否为Person的实例对象,不是就new一个
    if (!(this instanceof Person)){
        return new Person();
    }
    console.log( this );  //Person {}
    this.price = 5188; 
}

var per = Person(); 
console.log(per.price); //5188
View Code

 

 

二、this关键字

var Person = function(){
    console.log(\'1111\'); 
    console.log(this); 
    this.name = \'王大锤\';
    this.age = 18;

    this.run = function(){
        console.log(\'this is Person的实例对象吗:\' + (this instanceof Person) ); 
        console.log(this); 
    }
}

var per = new Person();
per.run();
/* 打印日志:
1111
Person {}
this is Person的实例对象吗:true
Person {name: "王大锤", age: 18, run: function}
*/

console.log(\'---------------\');

var Employ = {
    email: \'cnblogs@sina.com\',
    name: \'赵日天\',
    eat: function(){
        console.log(this);
    }
}

console.log(Employ.email + \', \' + Employ.name);
Employ.eat();
/* 打印日志:
---------------
cnblogs@sina.com, 赵日天
Object {email: "cnblogs@sina.com", name: "赵日天", eat: function}
*/
View Code

1、this总是返回一个对象,返回属性或方法当前所在的对象, 如上示例代码

2、对象的属性可以赋值给另一个对象,即属性所在的当前对象可变化,this的指向可变化

var A = { 
    name: \'王大锤\', 
    getInfo: function(){
        return \'姓名:\' + this.name;
    } 
}

var B = { name: \'赵日天\' };

B.getInfo = A.getInfo;
console.log( B.getInfo() ); //姓名:赵日天

//A.getInfo属性赋给B, 于是B.getInfo就表示getInfo方法所在的当前对象是B, 所以这时的this.name就指向B.name
View Code

 3、由于this指向的可变化性,在层级比较多的函数中需要注意使用this。一般来说,在多层函数中需要使用this时,设置一个变量来固定this的值,然后在内层函数中这个变量。

示例1:多层中的this

//1、多层中的this (错误演示)
var o = {
    f1: function(){
        console.log(this); //这个this指的是o对象

        var f2 = function(){
            console.log(this);
        }();
        //由于写法是(function(){ })() 格式, 则f2中的this指的是顶层对象window
    }
}

o.f1();
/* 打印日志:
Object {f1: function}

Window {stop: function, open: function, alert: function, confirm: function, prompt: function…}
*/


//2、上面代码的另一种写法(相同效果)
var temp = function(){
    console.log(this);
}
var o = {
    f1: function(){
        console.log(this); //这个this指o对象
        var f2 = temp(); //temp()中的this指向顶层对象window
    }
}
o.f1(); 
/* 打印日志
Object {f1: function}

Window {stop: function, open: function, alert: function, confirm: function, prompt: function…}
*/
//表示上面两种写法是一样的效果,this的错误演示


//3、多层中this的正确使用:使用一个变量来固定this对象,然后在内层中调用该变量
var o = {
    f1: function(){
        console.log(this); //o对象
        var that = this;
        var f2 = function(){
            console.log(that); //这个that指向o对象
        }();
    }
}
o.f1();
/* 打印日志:
Object {f1: function}
Object {f1: function}
*/
View Code

示例2: 数组遍历中的this

//1、多层中数组遍历中this的使用 (错误演示)
var obj = {
    email: \'大锤@sina.com\', 
    arr: [\'aaa\', \'bbb\', \'333\'],
    fun: function(){
        //第一个this指的是obj对象
        this.arr.forEach(function(item){
            //这个this指的是顶层对象window, 由于window没有email变量,则为undefined
            console.log(this.email + \': \' + item);
        });
    }
}

obj.fun(); 
/* 打印结果:
undefined: aaa
undefined: bbb
undefined: 333
 */

//2、多层中数组遍历中this的使用 (正确演示,第一种写法)
var obj = {
    email: \'大锤@sina.com\', 
    arr: [\'aaa\', \'bbb\', \'333\'],
    fun: function(){
        //第一个this指的是obj对象
        var that = this; //将this用变量固定下来
        this.arr.forEach(function(item){
            //这个that指的是对象obj
            console.log(that.email + \': \' + item);
        });
    }
}
obj.fun(); //调用
/* 打印日志:
大锤@sina.com: aaa
大锤@sina.com: bbb
大锤@sina.com: 333
 */


//3、多层中数组遍历中this正确使用第二种写法:将this作为forEach方法的第二个参数,固定循环中的运行环境
var obj = {
    email: \'大锤@sina.com\', 
    arr: [\'aaa\', \'bbb\', \'333\'],
    fun: function(){
        //第一个this指的是obj对象
        this.arr.forEach(function(item){
            //这个this从来自参数this, 指向obj对象
            console.log(this.email + \': \' + item);
        }, this);
    }
}
obj.fun(); //调用
/* 打印日志:
大锤@sina.com: aaa
大锤@sina.com: bbb
大锤@sina.com: 333
 */
View Code

 

4、关于js提供的call、apply、bind方法对this的固定和切换的用法

  1)、function.prototype.call(): 函数实例的call方法,可以指定函数内部this的指向(即函数执行时所在的作用域),然后在所指定的作用域中,调用该函数。
  如果call(args)里面的参数不传,或者为null、undefined、window, 则默认传入全局顶级对象window;
  如果call里面的参数传入自定义对象obj, 则函数内部的this指向自定义对象obj, 在obj作用域中运行该函数

var obj = {};
var f = function(){
    console.log(this);
    return this;
}

console.log(\'....start.....\');
f();
f.call();
f.call(null);
f.call(undefined);
f.call(window);
console.log(\'**** call方法的参数如果为空、null和undefined, 则默认传入全局等级window;如果call方法传入自定义对象obj,则函数f会在对象obj的作用域中运行 ****\');
f.call(obj);
console.log(\'.....end.....\');

/* 打印日志:
....start.....
Window {stop: function, open: function, alert: function, confirm: function, prompt: function…}
Window {stop: function, open: function, alert: function, confirm: function, prompt: function…}
Window {stop: function, open: function, alert: function, confirm: function, prompt: function…}
Window {stop: function, open: function, alert: function, confirm: function, prompt: function…}
Window {stop: function, open: function, alert: function, confirm: function, prompt: function…}
**** call方法的参数如果为空、null和undefined, 则默认传入全局等级window;如果call方法传入自定义对象obj,则函数f会在对象obj的作用域中运行 ****
Object {}
.....end.....
*/
View Code

   call可以接收多个参数:第一个参数是this所指向的那个对象,后面的参数就是函数调用时需要的参数,模拟简写为:
  func.call(thisValue, arg1, arg2, ...); 示例:

var obj = {
    name: \'张三\', 
    age: 10,
    email: \'张三@sina.com\'
}

function change(name, age){
    this.name = name;
    this.age = age;
    console.log(this);
}

change.call(obj, \'王大锤\', \'28\'); 
//Object {name: "王大锤", age: "28", email: "张三@sina.com"}
View Code

 

 2)、function.prototype.apply(): 函数实例的apply方法,和call的作用差不多,也是改变this的指向,调用该函数。
  唯一的区别就是,它接受一个数组作为函数执行时的参数,使用格式如下:
  func.apply( thisValue, [arg1, arg2, ...] );  

var obj = {
    name: \'张三\', 
    age: 10,
    email: \'张三@sina.com\'
}

function change(name, age){
    this.name = name;
    this.age = age;
    console.log(this);
}

change.apply(obj, [\'王大锤\', \'28\']); 
//Object {name: "王大锤", age: "28", email: "张三@sina.com"}
View Code

 

 3)、function.prototype.bind() : bind方法主要用于将函数体内的this绑定到某个对象,然后返回新函数。
  先来个简单示例,展示普通将方法赋值给变量,和使用bind()绑定的区别:

var d = new Date();

//第一次:将getTime方法赋值给print变量
var print = d.getTime;

try{
    print();  //执行时报错,进入异常处理
}catch(e){
    console.log(\'报错:\' + e.message);  //捕捉异常
    //打印结果:报错:this is not a Date object.
}

//第二次赋值:bind()应用,bind方法将getTime方法内部的this绑定到d对象,这时就可以安全的将这个方法赋值给其他变量
print = d.getTime.bind(d);
print(); //1498740682340
View Code
var obj = {
    count:0,
    run: function(){
        console.log(this);
        this.count++;
    } 
}

//1、对象直接调用自己方法,run里面的this指向obj对象
obj.run();
console.log( obj.count );

/* 打印日志:
Object {count: 0, run: function}
1
*/

//2、将对象方法赋值给变量,这时run里面的this指向了顶层对象window
var run2 = obj.run;
run2();  //执行obj.count不会变化,创建了全局变量count=undefined
console.log( obj.count ); //值未变化
console.log( count );  //undefined++ 等于NaN

/* 打印日志:
Window {stop: function, open: function, alert: function, confirm: function, prompt: function…}
1
NaN
*/


//3、应用bind方法将run方法内部的this, 绑定到obj对象。
var run2 = obj.run.bind(obj); 
run2();
console.log( obj.count ); //值有变化了

/* 打印日志:
Object {count: 1, run: function}
2
 */
View Code

  bind除了绑定this外,还可以绑定原函数的参数

var add = function(x, y){
    return x * this.m + y * this.n;
}

var obj = { m: 2, n: 2};

var newAdd = add.bind(obj, 5);
console.log( newAdd(5) ); //20


//另一种写法
var newAdd2 = add.bind(obj, 5, 5);
console.log( newAdd2() ); //20
View Code

   bind方法使用注意点:

  a、bind方法没执行一次,都返回一个新函数。所以在我们日常进行绑定监听事件的时候要特别注意:
  举个列子,给某个标签绑定点击事件:

/*
给element标签绑定和单击事件,和取消绑定单击事件

//1、第一种绑定和取消绑定方法
element.addEventListener(\'click\', o.m.bind(o)); //绑定
element.removeEventListener(\'click\', o.m.bind(o)); //取消绑定事件

//2、第二种绑定和取消绑定方法
var listenter = o.m.bind(o);
element.addEventListener(\'click\', listenter); //绑定
element.removeEventListener(\'click\', listenter);//取消绑定

//说明:第一种绑定的事件是不能取消绑定的,为什么,因为这种绑定bind方法生成的一个匿名函数
//第二种绑定的事件才能取消绑定
*/
View Code

   b、将包含this的方法直接当做回调函数

var obj = {
    name: \'王大锤\',
    arr: [\'aa\', \'bb\', \'cc\'],
    print: function(){
        this.arr.forEach(function(n){
            console.log( n + \':\' + this.name ); //这个this指向window
        });
    },
    print2: function(){
        this.arr.forEach(function(n){
            console.log( n + \':\' + this.name ); //这个this指向obj
        }.bind(this));
    }
}

obj.print();

/* 打印结果:此时print里面的forEach里面的this指向顶层对象window
aa:
bb:
cc:
*/

obj.print2();
/* 打印结果:通过bind绑定this
aa:王大锤
bb:王大锤
cc:王大锤
*/
View Code

 

 

三、prototype对象

先看关于原型对象的一张经典图片,其次要了解prototype和__proto__的区别

配合上图,相关的代码如下:

var Foo = function(){

}

var f1 = new Foo();
var f2 = new Foo();

var o1 = new Object();
var o2 = new Object(); 


var isTrue1 = Foo.prototype === f1.__proto__,
isTrue2 = Foo.prototype === f2.__proto__,
isTrue3 = Object.prototype === o1.__proto__,
isTrue4 = Object.prototype === o2.__proto__,
isTrue5 = Foo.prototype.__proto__ === Object.prototype,
isTrue6 = f1.__proto__.__proto__ === Object.prototype,
isTrue7 = f1.__proto__.__proto__.__proto__ === Object.prototype.__proto__,
isTrue8 = f1.__proto__.__proto__.__proto__ === null;

console.log(isTrue1 + ", " + isTrue2 + ", " + isTrue3 + ", " + 
isTrue4 + ", " + isTrue5 + ", " + isTrue6 + ", " + isTrue7 + ", " 
+ isTrue8);
//true, true, true, true, true, true, true, true

var isTrue9 = Foo.prototype.constructor === Foo;
var isTrue10 = f1.constructor === Foo;
var isTrue11 = Foo.prototype.__proto__ === Object.prototype;
var isTrue12 = Foo.prototype.__proto__.__proto__ === null;
var isTrue13 = Foo.__proto__ === Function.prototype;

/*Foo.constructor这里把Foo看成是Function构造函数的一个实例对象
所以构造函数Function的对象Foo的constructor 指向原构造函数Function
*/
var isTrue14 = Foo.constructor === Function;

/*
    Function要特别理解一下:
Function.constructor === Function === Function.prototype.constructur
当调用Function.constror时这时把Function看做是构造函数Function的一个实例对象,
即构造函数是Function, 实例对象也是Function, 然后对象Function的属性constructor指向构造函数Function
*/
var isTrue15 = Function.constructor === Function;
var isTrue16 = Function.constructor === Function.prototype.constructor;

console.log(isTrue9 + ", " + isTrue10 + ", " + isTrue11 + ", " + 
isTrue12 + ", " + isTrue13 + ", " + isTrue14 + " ," + isTrue15 + ", " + 
isTrue16);

//true, true, true, true, true, true ,true, true

1、在JS里,万物皆对象。方法(Function)是对象,方法的原型(Function.prototype)也是对象。

2、对象具有属性__proto__,可称为隐式原型,一个对象的隐式原型指向构造该对象的构造函数的原型,

  这也保证了实例能够访问在构造函数原型中定义的属性和方法。

  比如上图例子: f1.__proto__, f2.__proto__ , Foo.prototype这三个对象指向同一个对象。

3、方法(Function): 方法这个特殊的对象,除了和其他对象一样有上述_proto_属性之外,

  还有自己特有的属性——原型属性(prototype),这个属性是一个指针,指向一个对象,

  这个对象的用途就是包含所有实例共享的属性和方法(我们把这个对象叫做原型对象)。

  原型对象也有一个属性,叫做constructor,这个属性包含了一个指针prototype,指回原构造函数。

  比如上图例子:Foo.prototype.constructor == Foo 

 

总结:1)、对象有属性__proto__,指向该对象的构造函数的原型对象;

         2)、方法除了有属性__proto__,还有属性prototype,prototype指向该方法的原型对象。

     (方法调用__proto__属性的时候,其实是可看做把该方法当做构造函数Function的一个实例对象来使用;

      调用prototype则是把该方法当做一个构造函数来使用)

配合上图和相关代码,我们看到:

4、构造函数Foo()的原型属性Foo.prototype指向了原型对象,在原型对象里有共有的方法,所有构造函数声明的实例(这里是f1,f2)都可以共享这个方法。

5、原型对象Foo.prototype有一个指针constructor指回构造函数。

6、f1和f2是Foo这个对象的两个实例,这两个对象也有属性__proto__,指向构造函数的原型对象,这样子就可以访问原型对象的所有方法了。

7、构造函数Foo()除了是方法,也是对象,也有__proto__属性,指向它的构造函数的原型对象。函数的构造函数是Function嘛,因此这里的__proto__指向了Function.prototype。

  其实除了Foo(),Function(), Object()也是一样的道理。

8、原型对象也是对象,它的__proto__属性指向它的构造函数的原型对象。这里是Object.prototype。

  最后,Object.prototype的__proto__属性指向null。终于走到大结局,null没有自己的原型对象。

 

Object和Function的关联:

/*
Object和Function的关系:
1、Function.prototype.__proto__ === Object.prototype
2、Object.__proto__ === Function.prototype
3、Object.constructor === Function
*/

//Function.prototype是可看做是构造函数Object的一个实例对象,原型指向Object
/*
不过Function.prototype和其他如Array, String, Number,还有自定义构造函数的prototype有点不一样
typeof Function.prototype === "function"
其他如Array, String, Number, 还有自定义构造函数的typeof都是"object"
*/
var isTrue17 = Function.prototype.__proto__ === Object.prototype; //Object.__proto__ 这里的Object可看做是构造函数Function的一个实例对象 var isTrue18 = Object.__proto__ === Function.prototype; //Object.constructor 这里的Object可看做是构造函数Function的一个实例对象 var isTrue19 = Object.constructor === Function; console.log(isTrue17 + ", " + isTrue18 + ", " + isTrue19); //true, true, true

 

  

原型对象的属性为所有实例对象所共有,修改原型对象属性的值,变动体现在所有的实例对象上;

当实例对象有与原型对象同名属性,则优先寻找实例对象自有属性,当没找到对象自有属性时,才会去原型对象去寻找该属性。

举个例子: 第一步,先创建一个构造函数,再实例化两个对象

var Person = function(name){
    this.name = name;
}

var per1 = new Person(\'王大锤\');
var per2 = new Person(\'艾边城\');

console.log( per1 );
console.log( per2 );

展开日志截图所示:

第二步:通过构造函数的原型对象添加一个属性Color

Person.prototype.Color = "white";

console.log(per1);

展开日志,看到在实例对象的构造函数里面多了一个所有该构造函数实例对象所共享的属性Color

第三步:给per1对象设置Color属性

per1.Color = "Green";

console.log( per1 );
console.log( per2 );

 

 四、对象的Copy, 和面向对象三大特性模拟(继承,多态,封装)

1、拷贝对象,需要满足以下两个条件:

  • 拷贝后的对象,与原对象具有同样的prototype原型对象。
  • 拷贝后的对象,与原对象具有同样的属性。

对象拷贝的函数,和示例测试code

function copyObject(orig) {
    //创建要复制的对象
  var copy = Object.create(Object.getPrototypeOf(orig));
  copyOwnPropertiesFrom(copy, orig);
  return copy;
}

//拷贝对象的属性
function copyOwnPropertiesFrom(target, source) {
  Object
  .getOwnPropertyNames(source)
  .forEach(function(propKey) {
    var desc = Object.getOwnPropertyDescriptor(source, propKey);
    Object.defineProperty(target, propKey, desc);
  });
  return target;
}



//测试code
var Student = function(name, age, otherInfo){
    this.name = name;
    this.age = age;
    this.otherInfo = otherInfo;
    
    this.run = function(){
        console.l

以上是关于JS面向对象笔记二的主要内容,如果未能解决你的问题,请参考以下文章

Java学习笔记1

js面向对象笔记

面向对象笔记一

JS笔记加强版3

[js笔记整理]面向对象篇

5月17日上课笔记-js面向对象