面向对象编程 (Object Oriented Programming)
将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟
- 对象需要一个模板,来表示某一类实物的共同特征。所谓“类”就是对象的模板,对象就是“类”的实例。
- JavaScript 语言使用构造函数(constructor)作为对象的模板。
构造函数
构造函数就是一个普通的函数
构造函数名字的第一个字母通常大写
var Vehicle = function () {
this.price = 1000;
};
构造函数的特点:
- 函数体内部使用了this关键字,代表了所要生成的对象实例。
- 生成对象的时候,必须使用new命令。
new 命令
new命令的作用,就是执行构造函数,返回一个实例对象。
var v = new Vehicle(); //推荐带括号的写法
忘加new的话会使构造函数当成普通函数执行,从而出错。
解决办法一:函数内部使用严格模式
function Fubar(foo, bar){
'use strict';
this._foo = foo;
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;
}
Fubar(1, 2)._foo // 1
(new Fubar(1, 2))._foo // 1
new 命令的原理:
- 创建一个空对象,作为将要返回的对象实例。
- 将这个空对象的原型,指向构造函数的prototype属性。
- 将这个空对象赋值给函数内部的this关键字。
- 开始执行构造函数内部的代码。
如果构造函数内部有return语句,而且return后面跟着一个对象,new命令会返回return语句指定的对象;否则,就会不管return语句,返回this对象。
new.target
函数内部可以使用new.target属性。如果当前函数是new命令调用,new.target指向当前函数,否则为undefined。
使用这个属性,可以判断函数调用的时候,是否使用new命令。
function f() {
if (!new.target) {
throw new Error('请使用 new 命令调用!');
}
// ...
}
f() // Uncaught Error: 请使用 new 命令调用!
this 关键字
this
有一个共同点:它总是返回一个对象。
换句话说,this
就是属性或方法“当前”所在的对象。
this
就是函数运行时所在的对象(环境)
this
的指向是动态的
使用场合:
- 全局环境使用this,它指的就是顶层对象window。
- 构造函数中的this,指的是实例对象。
- 如果对象的方法里面包含this,this的指向就是方法运行时所在的对象。该方法赋值给另一个对象,就会改变this的指向。
// 注意
var obj ={
foo: function () {
console.log(this);
}
};
obj.foo() // obj
(false || obj.foo)() // window
/*解释:JavaScript 引擎内部,obj和obj.foo储存在两个内存地址,称为地址一和地址二。
obj.foo()这样调用时,是从地址一调用地址二,因此地址二的运行环境是地址一,this指向
obj。obj.foo是直接取出地址二进行调用,这样的话,运行环境就不是地址一了*/
使用注意点:
避免多层 this
解决:
- 使用一个变量固定this的值,然后内层函数调用这个变量。
- 严格模式下,如果函数内部的this指向顶层对象,就会报错。
避免数组处理方法中的 this
- 避免回调函数中的 this
绑定 this 的方法
使用call、apply、bind这三个方法,来切换/固定this的指向。
Function.prototype.call()
func.call(thisValue, arg1, arg2, ...)
call的第一个参数就是this所要指向的那个对象,后面的参数则是函数调用时所需的参数。
// call方法的参数,指定函数执行时所在的作用域
// 参数,应该是一个对象。如果参数为空、null和undefined,则默认传入全局对象。
var obj = {};
var f = function () {
return this;
};
f() === window // true
f.call(obj) === obj // true
function add(a, b) {
return a + b;
}
add.call(this, 1, 2) // 3
- Function.prototype.apply()
apply方法的作用与call方法类似,唯一的区别就是,它接收一个数组作为函数执行时的参数。
func.apply(thisValue, [arg1, arg2, ...])
利用这一点,可以做一些有趣的应用。
// 找出数组最大元素
var a = [10, 2, 4, 15, 9];
Math.max.apply(null, a) // 15
// 将数组的空元素变为undefined
Array.apply(null,['a',' ','b']) // [ 'a', undefined, 'b' ]
Array.from(['a',,'b'])// [ "a", undefined, "b" ]
// 转换类似数组的对象,利用数组对象的slice方法
Array.prototype.slice.apply({0: 1, length: 1}) // [1]
Function.prototype.bind()
bind
方法用于将函数体内的this
绑定到某个对象,然后返回一个新函数。
如果bind方法的第一个参数是null或undefined,等于将this绑定到全局对象,函数运行时this指向顶层对象(浏览器为window)。
var counter = {
count: 0,
inc: function () {
this.count++;
}
};
var obj = {
count: 100
};
var func = counter.inc.bind(obj);
func();
obj.count // 101
多个参数:
多的参数与原函数的参数依次绑定
var add = function (x, y) {
return x * this.m + y * this.n;
}
var obj = {
m: 2,
n: 2
};
var newAdd = add.bind(obj, 5);//第一个参数x绑定成5
//传入第二个参数y
newAdd(6) // 22
注意:
- forEach方法的回调函数内部的this是指向全局对象的,因而如果有使用this的话则要
.bind(this)
prototype 对象
每个函数都有一个prototype属性,指向一个对象。
所有对象都有自己的原型对象(prototype)。
原型链的尽头就是null。
prototype对象有一个constructor属性,默认指向prototype对象所在的构造函数。
修改原型对象(把一个新的对象赋值给原型对象)时,一般要同时修改constructor属性的指向。
instanceof
运算符
返回一个布尔值,表示对象是否为某个构造函数的实例。
var v = new Vehicle();
v instanceof Vehicle // true
instanceof的原理是检查右边构造函数的prototype属性,是否在左边对象的原型链上,检查整个原型链。
instanceof运算符只能用于对象,不适用原始类型的值。
对于undefined和null,instanceOf运算符总是返回false。
有一种特殊情况,就是左边对象的原型链上,只有null对象。这时,instanceof判断会失真。
// 小应用,检查调用构造函数时,是否忘了加new命令
function Fubar (foo, bar) {
if (this instanceof Fubar) {
this._foo = foo;
this._bar = bar;
} else {
return new Fubar(foo, bar);
}
}
Object 对象的相关方法
Object.getPrototypeOf()
返回参数对象的原型。这是获取原型对象的最佳方法。
Object.setPrototypeOf()
为参数对象设置原型,返回该参数对象。
接受两个参数,第一个是现有对象,第二个是原型对象。
var a = {};
var b = {x: 1};
Object.setPrototypeOf(a, b);
a.x // 1
Object.create()
从一个实例对象,生成另一个实例对象。
接受一个对象作为参数,然后以它为原型,返回一个实例对象。
// 原型对象
var A = {
print: function () {
console.log('hello');
}
};
// 实例对象
var B = Object.create(A);
第二个参数是一个属性描述对象,它所描述的对象属性,会添加到实例对象,作为该对象自身的属性。
Object.prototype.isPrototypeOf()
用来判断该对象是否为参数对象的原型,返回布尔值
Object.getOwnPropertyNames()
返回一个数组,成员是参数对象本身的所有属性(不管是否可以遍历)的键名,不包含继承的属性键名。
只获取那些可以遍历的属性,使用Object.keys方法。Object.prototype.hasOwnProperty()
判断某个属性定义在对象自身,还是定义在原型链上。
hasOwnProperty方法是 JavaScript 之中唯一一个处理对象属性时,不会遍历原型链的方法。
对象的拷贝
如果要拷贝一个对象,需要做到下面两件事情。
- 确保拷贝后的对象,与原对象具有同样的原型。
- 确保拷贝后的对象,与原对象具有同样的实例属性。
function copyObject(orig) {
return Object.create(
Object.getPrototypeOf(orig),
Object.getOwnPropertyDescriptors(orig)
);
}
面向对象编程的模式
构造函数的继承
一个构造函数继承另一个构造函数,共两步:
- 在子类的构造函数中,调用父类的构造函数。
// Sub是子类的构造函数,this是子类的实例
function Sub(value) {
Super.call(this); // 让子类实例具有父类实例的属性
this.prop = value;
}
- 让子类的原型指向父类的原型
Sub.prototype = Object.create(Super.prototype);
/* 这里,如果直接把Super.prototype赋值给Sub.prototype,
则对Sub.prototype的操作,会连父类的原型Super.prototype一起修改掉。*/
Sub.prototype.constructor = Sub;
只需要单个方法时,简洁写法:
ClassB.prototype.print = function() {
ClassA.prototype.print.call(this);
// some code
}
模块
模块是实现特定功能的一组属性和方法的封装。
基本写法:
var module1 = (function () {
var _count = 0;
var m1 = function () {
//...
};
var m2 = function () {
//...
};
return {
m1 : m1,
m2 : m2
};
})();
模块的放大模式:
如果一个模块很大,必须分成几个部分,或者一个模块需要继承另一个模块,这时就有必要采用“放大模式”(augmentation)。
例如:为module1模块添加了一个新方法m3()。
var module1 = (function (mod){
mod.m3 = function () {
//...
};
return mod;
})(module1);
如果上面的添加操作先于module1执行操作,可能加载一个不存在空对象,采用”宽放大模式”(Loose augmentation),允许“立即执行函数”的参数是空对象。
ar module1 = ( function (mod){
//...
return mod;
})(window.module1 || {});
为了在模块内部调用全局变量,必须显式地将其他变量输入模块。
var module1 = (function ($, YAHOO) {
//...
})(jQuery, YAHOO);
上面的module1模块需要使用jQuery库和YUI库,就把这两个库(其实是两个模块)当作参数输入module1。这样做除了保证模块的独立性,还使得模块之间的依赖关系变得明显。
补充:
__proto__
和prototype的区别?
__proto__
是对象的属性,prototype是函数的属性
var 对象 = new 函数(); //函数可以是Number/String/Boolean/Object
对象.__proto__ === 函数.prototype //true
'1'.__proto__
是什么?
// ’1’会临时转化为String对象,所有对象都有__proto__属性。
'1'.__proto__ === String.prototype //true
Object.prototype.__proto__ === null; //true
1.toString() //语法错误,不加引号JS会把.当作小数点
1..toString() //'1',第一个.当作小数点,第二个为点操作符