(78)Wangdao.com第十五天_JavaScript 面向对象
面向对象编程(Object Oriented Programming,缩写为 OOP)
是目前主流的编程范式。
是单个实物的抽象,
是一个容器,封装了属性(property)和方法(method),属性是对象的状态,方法是对象的行为(完成某种任务)。
将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。
每一个对象都是功能中心,具有明确分工,可以完成接受信息、处理数据、发出信息等任务。
对象可以复用,通过继承机制还可以定制。
具有灵活、代码可复用、高度模块化等特点,容易维护和开发,比起由一系列函数或指令组成的传统的过程式编程(procedural programming),更适合多人合作的大型软件项目。
- 构造函数
“类” 就是对象的模板,对象就是 “类” 的实例。
JavaScript 语言的对象体系,不是基于“类”的,而是基于构造函数(constructor)和原型链(prototype)。
- 使用构造函数(constructor)作为对象的模板,描述实例对象的基本结构。。
- 所谓 ”构造函数”,就是专门用来生成实例对象的函数。
- 一个构造函数,可以生成多个实例对象,这些实例对象都有相同的结构。
- 为了与普通函数区别,构造函数名字的第一个字母通常大写。
- 特点:
- 函数体内部使用了
this
关键字,代表所要生成的实例 - 生成对象的时候,必须使用
new
命令
- 函数体内部使用了
- new 命令
- 通过
new
命令,让构造函数生成一个实例对象 new
命令执行时,构造函数内部的this
,就代表了新生成的实例对象- 使用
new
命令时,根据需要,构造函数也可以接受参数-
var Vehicle = function (p) { this.price = p; }; var v = new Vehicle(500);
new
命令本身就可以执行构造函数,所以后面的构造函数可以带括号,也可以不带括号。但是为了表示是函数调用,推荐使用括号 -
var Vehicle = function (){ this.price = 1000; }; var v = Vehicle(); // 将构造函数当成普通函数调用,不抱错 v // undefined // 但是变量v变成了undefined price // 1000 // price属性变成了全局变量 // 一个解决办法是: // 构造函数内部使用严格模式,即第一行加上use strict。 // 这样的话,一旦忘了使用new命令,直接调用构造函数就会报错。
function Fubar(foo, bar){ 'use strict'; this._foo = foo; // 由于 严格模式中,函数内部的
this
不能指向全局对象,默认等于undefined
this._bar = bar; } Fubar(); // TypeError: Cannot set property '_foo' of undefined
// 另一个解决办法:
// 造函数内部判断是否使用new
命令,如果发现没有使用,则直接返回一个实例对象。function Fubar(foo, bar) { if (!(this instanceof Fubar)) { return new Fubar(foo, bar); } this._foo = foo; this._bar = bar; }
// 此时不管加不加new
命令,都会得到同样的结果 Fubar(1, 2)._foo // 1 (new Fubar(1, 2))._foo // 1
-
- 通过
- new 命令的原理
使用new
命令时,它后面的函数依次执行下面的步骤
1. 创建一个空对象,作为将要返回的对象实例
2. 将这个空对象的隐式原型对象 __proto__,指向构造函数的 prototype 属性
3. 将这个空对象赋值给函数内部的 this 关键字
4. 开始执行构造函数内部的代码
-
- 如果构造函数内部有 return 语句,而且 return 后面跟着一个对象,new 命令会返回 return 语句指定的对象;否则,就会不管 return 语句,返回 this 对象
- 如果对普通函数(内部没有
this
关键字的函数)使用new
命令,则会返回一个空对象 - 因为
new
命令总是返回一个对象,要么是实例对象,要么是return
语句指定的对象function getMessage() { return 'this is a message'; } var msg = new getMessage(); msg // {} typeof msg // "object"
- 如果对普通函数(内部没有
new
命令简化的内部流程,可以用下面的代码表示
-
function _new(/* 构造函数 */ constructor, /* 构造函数参数 */ params) { // 将 arguments 对象转为数组 var args = [].slice.call(arguments); // 取出构造函数 var constructor = args.shift(); // 创建一个空对象,继承构造函数的 prototype 属性 var context = Object.create(constructor.prototype); // 执行构造函数 var result = constructor.apply(context, args); // 如果返回结果是对象,就直接返回,否则返回 context 对象 return (typeof result === 'object' && result != null) ? result : context; } // 实例 var actor = _new(Person, '张三', 28);
-
- 如果构造函数内部有 return 语句,而且 return 后面跟着一个对象,new 命令会返回 return 语句指定的对象;否则,就会不管 return 语句,返回 this 对象
- 函数内部可以使用 new.target 属性
- 如果当前函数是
new
命令调用,new.target
指向当前函数,否则为undefined
-
function f() { console.log(new.target === f); } f(); // false //
new.target
指向undefined
new f(); // true //new.target
指向 当前函数
-
- 使用这个属性,可以判断函数调用的时候,是否使用
new
命令-
function f() { if (!new.target) { throw new Error('请使用 new 命令调用!'); } // ... } f(); // Uncaught Error: 请使用 new 命令调用!
-
- 如果当前函数是
- Object.create() 创建实例对象
- 有时拿不到构造函数,只能拿到一个现有的对象
- 可以使用 Object.create() 方法,生成新的实例对象
-
var Person= { name: '张三', age: 38, greeting: function() { console.log('Hi! I\'m ' + this.name + '.'); } }; var person2 = Object.create(Person); person2.name; // 张三 person2.greeting(); // Hi! I'm 张三.
-
- this 关键字
this除了可以在构造函数中表示实例对象,还可以用在其他场合
但是不管什么场合,this 总是返回一个对象
- this 的指向是可变的
- 只要函数被赋给另一个变量,this 的指向就会变
-
var A = { name: '张三', describe: function () { return '姓名:'+ this.name; } }; var name = '李四'; // window.name var f = A.describe; // window.f f(); // "姓名:李四"
-
- 根据据当前所在的对象不同,this 的指向也不同
-
function f() { return '姓名:'+ this.name; } var A = { name: '张三', describe: f }; var B = { name: '李四', describe: f }; A.describe(); // "姓名:张三" B.describe(); // "姓名:李四"
-
- 一个网页编程的例子
-
<input type="text" name="age" size=3 onChange="validate(this, 18, 99);"> <script> function validate(obj, lowval, hival){ if ((obj.value < lowval) || (obj.value > hival)) console.log('Invalid Value!'); } </script>
// 一个文本输入框,每当用户输入一个值,就会调用onChange
回调函数,验证这个值是否在指定范围。
// 浏览器会向回调函数传入当前对象,因此this
就代表传入当前对象(即文本框),然后就可以从obj.value
上面读到用户的输入值JavaScript 支持运行环境动态切换,也就是说,
this
的指向是动态的,没有办法事先确定到底指向哪个对象
-
- 只要函数被赋给另一个变量,this 的指向就会变
- this 实质
-
var obj = { foo: 5 }; // 原始的对象以字典结构保存,每一个属性名都对应一个属性描述对象。 // 举例来说,上面例子的 foo 属性,实际上是以下面的形式保存的 { foo: { [[value]]: 5 [[writable]]: true [[enumerable]]: true [[configurable]]: true } }
-
- 有 this 的设计,跟内存里面的数据结构有关系
- 由于函数是一个单独的值,所以它可以在不同的环境(上下文)执行
- 由于函数可以在 不同的运行环境(上下文) 执行,所以需要有一种机制,能够在函数体内部获得当前的运行环境(context)
- 所以,this 就出现了,它的设计目的就是在函数体内部,指代函数当前的运行环境。
-
var f = function () { console.log(this.x); } var x = 1; var obj = { ff: f, x: 2, }; // 单独执行 f(); // 1 // obj 环境执行 obj.ff(); // 2
-
- 几个使用场合下,this 的指向
- 全局环境
- 全局环境使用this,它指的就是顶层对象window
- 不管是不是在函数内部,只要是在全局环境下运行,this 就是指顶层对象window
- 构造函数
- 构造函数中的 this ,指的是实例对象构造函数中的 this ,指的是实例对象
- 方法调用时
- this 指向调用该对象的对象。
- call() 和 apply() 时
- 指向指定的对象
- 全局环境
- 有 this 的设计,跟内存里面的数据结构有关系
-
- 由于 this 的指向是不确定的,所以切勿在函数中包含多层的 this
- 使用一个变量固定 this 的值,然后内层函数调用这个变量,是非常常见的做法,请务必掌握
-
var o = { f1: function () { console.log(this); var f2 = function () { console.log(this); }(); } } o.f1(); // Object // Window
- 使用一个变量固定 this 的值,然后内层函数调用这个变量
var o = { f1: function() { console.log(this);
var that = this; var f2 = function() { console.log(that); }(); } } o.f1() // Object // Object
// 以上代码等价于var temp = function () {
console.log(this);
};
var o = { f1: function () { console.log(this); var f2 = temp(); } }
- 严格模式下,如果函数内部的 this 指向顶层对象,就会报错。
- 使用一个变量固定 this 的值,然后内层函数调用这个变量,是非常常见的做法,请务必掌握
- 由于 this 的指向是不确定的,所以切勿在函数中包含多层的 this
-
- 避免数组处理方法中的 this
- 数组的
map
和foreach
方法,允许提供一个函数作为参数。这个函数内部不应该使用this
- 如果有
- 使用中间变量固定this
- 将 this 当作 foreach( , this) 方法的第二个参数,固定它的运行环境
-
var o = { v: 'hello', p: [ 'a1', 'a2' ], f: function f() { this.p.forEach(function (item) { console.log(this.v + ' ' + item); }); } } o.f() // undefined a1 // undefined a2 // 解决方法1 使用变量固定 this var o = { v: 'hello', p: [ 'a1', 'a2' ], f: function f() { var that = this; this.p.forEach(function (item) { console.log(that.v+' '+item); }); } } o.f() // hello a1 // hello a2 // 解决方法2 forEach( , this) var o = { v: 'hello', p: [ 'a1', 'a2' ], f: function f() { this.p.forEach(function (item) { console.log(this.v + ' ' + item); }, this); } } o.f() // hello a1 // hello a2
- 数组的
- 回调函数中的 this 往往会改变指向,最好避免使用
-
-
var o = new Object(); o.f = function () { console.log(this === o); } // jQuery 的写法 $('#button').on('click', o.f);
上面代码中,点击按钮以后,控制台会显示
false
。原因是此时this
不再指向o
对象,而是指向按钮的 DOM 对象,因为f
方法是在按钮对象的环境中被调用的。
-
-
- 避免数组处理方法中的 this
-
- 固定 this 指向
JavaScript 提供了call
、apply
、bind
这三个方法,来切换/固定this
的指向
-
-
- Function.prototype.call()
- 函数实例的call方法,可以指定函数内部this的指向(即函数执行时所在的作用域),然后在所指定的作用域中,调用该函数
-
var obj = {}; var f = function () { return this; }; f() === window // true f.call(obj) === obj // true
-
- call 方法的参数,应该是一个对象。如果参数为空、null和undefined,则默认传入全局对象
-
-
- 如果 call 方法的参数是一个原始值,那么这个原始值会自动转成对应的包装对象,然后传入 call 方法
-
var f = function () { return this; }; f.call(5) // Number {[[PrimitiveValue]]: 5}
-
- call 方法还可以接受多个参数 func.call(thisObj, arg1, arg2, ...)
- 后面的参数是函数调用时所需的参数
-
function add(a, b) { return a + b; } add.call(this, 1, 2); // 3
- 应用 之 调用对象的原生方法
-
var obj = {}; obj.hasOwnProperty('toString'); // false // 覆盖掉继承的 hasOwnProperty 方法 obj.hasOwnProperty = function () { return true; }; obj.hasOwnProperty('toString'); // true // hasOwnProperty是obj对象继承的方法,如果这个方法一旦被覆盖,就不会得到正确结果 // 将hasOwnProperty方法的原始定义放到obj对象上执行,这样无论obj上有没有同名方法,都不会影响结果 Object.prototype.hasOwnProperty.call(obj, 'toString'); // false
-
- 如果 call 方法的参数是一个原始值,那么这个原始值会自动转成对应的包装对象,然后传入 call 方法
-
-
- Function.prototype.apply()
- 与 call 方法类似,也是改变 this 指向,然后再调用该函数。
- 唯一的区别就是,它只有 2 个参数,第二个参数接收一个数组作为函数执行时的参数 func.call(thisObj, [arg1, arg2, ...])
- 第一个参数也是 this 所要指向的那个对象,如果设为null或undefined,则等同于指定全局对象
- 很多有趣的应用
- 找出数组最大元素
-
var a = [10, 2, 4, 15, 9]; // 结合使用apply方法和Math.max方法,就可以返回数组的最大元素 Math.max.apply(null, a); // 15
-
- 将数组的空元素变为 undefined
-
// 利用 Array构造函数 和 apply结合 // 将数组的空元素变成undefined Array.apply(null, ['a', ,'b']); // [ 'a', undefined, 'b' ]
空元素与undefined的差别在于,数组的forEach方法会跳过空元素,但是不会跳过undefined
-
- 转换类似数组的对象
- 利用数组对象的slice方法,将一个类似数组的对象(比如arguments对象)转为真正的数组
-
// 前提是 //被处理的对象必须有length属性 //以及相对应的数字键 Array.prototype.slice.apply({0: 1, length: 1}); // [1] Array.prototype.slice.apply({0: 1}); // [] Array.prototype.slice.apply({0: 1, length: 2}); // [1, undefined] Array.prototype.slice.apply({length: 1}); // [undefined]
- 绑定回调函数的对象
-
var o = new Object(); o.f = function () { console.log(this === o); } var f = function (){ o.f.apply(o); // 或者 o.f.call(o); }; // jQuery 的写法 $('#button').on('click', f); //点击按钮以后,控制台将会显示true。 // 由于apply方法(或者call方法)不仅绑定函数执行时所在的对象,还会立即执行函数,因此不得不把绑定语句写在一个函数体内。 // 更简洁的写法是采用下面介绍的bind方法。
-
- 找出数组最大元素
-
-
-
- Function.prototype.bind()
- 用于将函数体内的 this 绑定到某个对象,然后返回一个新函数
-
-
var d = new Date(); d.getTime() // 1481869925657 var print = d.getTime; print() // Uncaught TypeError: this is not a Date object. // 因为getTime方法内部的this,绑定Date对象的实例,赋给变量print以后,内部的this已经不指向Date对象的实例了
bind方法可以解决这个问题
-
var print = d.getTime.bind(d); print(); // 1481869925657
bind 方法将 getTime 方法内部的 this 绑定到 d 对象,这时就可以安全地将这个方法赋值给其他变量了
-
-
-
- bind 方法的参数就是所要绑定 this 的对象
- 下面是一个更清晰的例子
-
var counter = { count: 0, inc: function () { this.count++; } }; var func = counter.inc.bind(counter); func(); counter.count // 1
counter.inc() 方法被赋值给变量 func 。这时必须用 bind 方法将 inc 内部的 this,绑定到 counter,否则就会出错
-
- this绑定到其他对象也是可以的
-
var counter = { count: 0, inc: function () { this.count++; } }; var obj = { count: 100 }; var func = counter.inc.bind(obj); func(); obj.count // 101
-
- bind 还可以接受更多的参数,将这些参数绑定原函数的参数
-
var add = function (x, y) { return x * this.m + y * this.n; } var obj = { m: 2, n: 2 }; var newAdd = add.bind(obj, 5); newAdd(5) // 20
bind() 方法除了绑定 this 对象,还将 add 函数的第一个参数 x 绑定成 5,然后返回一个新函数 newAdd(),这个函数只要再接受一个参数 y 就能运行了
-
- 如果bind() 方法的第一个参数是 null 或 undefined,等于将 this 绑定到全局对象,函数运行时 this 指向顶层对象(浏览器为window)
-
function add(x, y) { return x + y; } var plus5 = add.bind(null, 5); plus5(10); // 15
上面代码中,函数 add 内部并没有 this,使用 bind() 方法的主要目的是绑定参数 x,以后每次运行新函数 plus5,就只需要提供另一个参数 y 就够了。
-
而且因为 add 内部没有 this,所以 bind() 的第一个参数是 null,不过这里如果是其他对象,也没有影响
-
-
-
- bind() 使用注意
- 每一次返回一个新函数
- bind方法每运行一次,就返回一个新函数,这会产生一些问题。
- 比如,监听事件的时候,不能写成下面这样
-
element.addEventListener('click', o.m.bind(o)); // 上面代码中,click事件绑定bind方法生成的一个匿名函数。 // 这样会导致无法取消绑定,所以,下面的代码是无效的 element.removeEventListener('click', o.m.bind(o)); // 正确写法 var listener = o.m.bind(o); element.addEventListener('click', listener); // ... element.removeEventListener('click', listener);
-
- 结合回调函数使用
- 回调函数是 JavaScript 最常用的模式之一,
- 但是一个常见的错误是,将包含 this 的方法直接当作回调函数
-
var counter = { count: 0, inc: function () { 'use strict'; this.count++; } }; function callIt(callback) { callback(); }
// 上面代码中,callIt方法会调用回调函数。
// 这时如果直接把counter.inc传入,调用时counter.inc内部的this就会指向全局对象。 callIt(counter.inc.bind(counter)); // 使用bind方法将counter.inc绑定counter以后,就不会有这个问题,this总是指向counter counter.count // 1
-
- 某些数组方法可以接受一个函数当作参数。这些函数内部的
this
指向,很可能也会出错-
var obj = { name: '张三', times: [1, 2, 3], print: function () { this.times.forEach(function (n) { console.log(this.name); }); } }; obj.print(); // 没有任何输出 // obj.print内部this.times的this是指向obj的,这个没有问题。 // 但是,forEach方法的回调函数内部的this.name却是指向全局对象,导致没有办法取到值。 obj.print = function () { this.times.forEach(function (n) { console.log(this === window); }); }; obj.print() // true // true // true
解决方法
-
// 通过bind方法绑定this obj.print = function () { this.times.forEach(function (n) { console.log(this.name); }.bind(this)); }; obj.print() // 张三 // 张三 // 张三
-
-
- 结合call方法使用
- 利用bind方法,可以改写一些 JavaScript 原生方法的使用形式,以数组的slice方法为例
-
[1, 2, 3].slice(0, 1); // [1] // 等同于 Array.prototype.slice.call([1, 2, 3], 0, 1); // [1]
call方法实质上是调用Function.prototype.call方法,因此上面的表达式可以用bind方法改写
-
var slice = Function.prototype.call.bind(Array.prototype.slice); slice([1, 2, 3], 0, 1); // [1]
-
var push = Function.prototype.call.bind(Array.prototype.push); var pop = Function.prototype.call.bind(Array.prototype.pop); var a = [1 ,2 ,3]; push(a, 4) a // [1, 2, 3, 4] pop(a) a // [1, 2, 3]
-
-
- 如果再进一步,将 Function.prototype.call 方法绑定到 Function.prototype.bind 对象,就意味着 bind 的调用形式也可以被改写
-
function f() { console.log(this.v); } var o = { v: 123 }; var bind = Function.prototype.call.bind(Function.prototype.bind); bind(f, o)(); // 123
所以
bind
方法就可以直接使用,不需要在函数实例上使用
-
- 利用bind方法,可以改写一些 JavaScript 原生方法的使用形式,以数组的slice方法为例
- 每一次返回一个新函数
- bind() 使用注意