与其它OOP语言 (C++ Java)不同,JS 中没有class 的概念, JS中的对象是以构造函数和原型链作为模板的.
构造函数类似于普通的函数,但是有其特点,一般约定函数名首字母大写,函数体中有this, 带表生成的对象实例.
生成对象时,必须使用 new 命令.
New 命令
-
基本用法
new 命令的作用就是执行构造函数,返回一个实例对象.
var Vehicle = function(){ this.price = 1000; }; var v = new Vehicle(); v.price //1000
如果忘了使用 new 命令,直接调用构造函数会发生什么事?
这时候,构造函数就变成了普通函数,不会生成实例对象,而且this 会代表全局对象,将造成一些意想不到的结果.
为了保证构造函数必须与 new 命令一起使用,可以在构造函数内容使用严格模式,如此一来,一旦忘记使用 new 命令,就会报错.
function Cons(){ 'use strict'; this.a = 1; } Cons(); // TypeError:Cannot set property 'a' of undefined
这是因为严格模式下,this不能指向全局变量,默认为undefined 导致报错.
反之,如果对普通函数使用 new 命令,则会返回一个空对象,不管这个普通函数内部返回的是什么.
-
new 命令的原理
使用 new 命令时,会发生以下事情:
- 创建一个空对象,作为要返回的对象实例;
- 将这个对象的原型指向构造函数的prototype属性;
- 把这个对象赋值给函数内部的this;
- 开始执行构造函数内部的代码;
也就是说,this指向的是一个新生成的空对象,任何对this的操作都会发生在这个空对象上, 如果构造函数内部有 return ,并且 return 后面跟着一个对象,那么 new 命令就返回 return 语句指定的对象,否则就忽略 return 语句,返回 this 对象.
-
**Object.create( ) 创建实例对象 **
利用现有的对象作为模板,生成新的实例对象.
var person1 = { name:'wjk', age:'33', greeting:function(){ console.log('Hi! I\'m'+this.name + '.'); } } var person2 = Object.create(person1); person2.name //wjk person2.greeting() // Hi! I'm wjk
this 关键字
this 关键字是个非常重要的语法点,不理解它的含义,大部分开发任务都无法完成.
this 有一个共同点:它总是返回一个对象. this 就是属性或者方法”当前”所在的对象.
对象的属性可以赋值给另和个对象,所以属性所在的当前对象是可变的,也就是说 this 的指向是可变的.
function f() {
return '姓名:'+ this.name;
}
var A = {
name: '张三',
describe: f
};
var B = {
name: '李四',
describe: f
};
A.describe() // "姓名:张三"
B.describe() // "姓名:李四"
JavaScript 语言之中,一切皆对象,运行环境也是对象,所以函数都是在某个对象之中运行,this
就是函数运行时所在的对象(环境)。这本来并不会让用户糊涂,但是 JavaScript 支持运行环境动态切换,也就是说,this
的指向是动态的,没有办法事先确定到底指向哪个对象,这才是感到困惑的地方。
this 的实质
对象名和函数名其实是对象和函数的内存地址,所以拿到函数的引用后,就可以在不同的环境中执行,而JS允许在函数体中引用当前环境的其它变量,所以要有一种机制,能够在函数体中获得当前的运行环境,this就出现了,它的设计目的就是在函数体内部指代函数当前的运行环境.
使用 this 的注意点
由于 this 的指向是不确定的,所以不要在函数中包含多层的 this.
var o = {
f1: function () {
console.log(this);
var f2 = function () {
console.log(this);
}();
}
}
o.f1()
// Object
// Window
绑定 this 的方法
this
的动态切换,固然为 JavaScript 创造了巨大的灵活性,但也使得编程变得困难和模糊。有时,需要把this
固定下来,避免出现意想不到的情况。JavaScript 提供了**call
、apply
、bind
**这三个方法,来切换/固定this
的指向。
-
Function.prototype.call( )
var obj = {}; var f = function(){ return this; } f() === window //true f.call(obj) === obj //true
call 方法的参数,应该是一个对象,如果参数为空 null 和 undefined 则默认传入全局对象 .
call 方法还可以接受多个参数
func.call(thisObj,arg1,arg2, ...)
后面的 arg 们 是函数调用时所需的参数.
-
Function.prototype.apply
用法与 call 方法一样,不同之处是它接收一个数组作为函数执行时的参数.
找出数组中最大的数
var a = [10,2,4,15,9]; Math.max.apply(null,a) //15
把类数组对象转换为数组
Array.prototype.slice.apply({0:1,length:1}) // [1] Array.prototype.slice.apply({0:1}) //[]
-
Fuction.prototype.bind( )
用于将某个对象绑定到函数体内的 this ,然后返回一个新函数
var counter = { count: 0, inc: function () { this.count++; } }; var func = counter.inc.bind(counter); func(); counter.count // 1
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的第一个参数绑定成了5,然后返回一个新函数 newAdd, 此函数只要再接受一个参数y就行了.
注意:bind 方法每次运行都会返回一个新函数
所以这在事件绑定写法中要注意,否则绑定事件后,无法接触绑定
正确的方法是写成下面这样:
var listener = o.m.bind(o); element.addEventListener('click', listener); // ... element.removeEventListener('click', listener);
利用bind方法可以改写一些原生 JS 方法的使用形式
[1,2,3].slice(0,1); //等同于 Array.prototype.slice.call([1,2,3],0,1)
上面的call 方法实际上是调用的Function.prototype.call
var newSlice = Function.prototype.call.bind(Array.prototype.slice);
所以
newSlice([1,2,3],0,1)
同样等同于Array.prototype.slice.call([1,2,3],0,1)
上面的含义是将Array.prototype.slice 变成 Function.prototype.call 方法所在的对象,调用时,就变成了slice在call
如果再进一步,把bind方法变成call所在的对象,就意味着返回的新bind方法就会变成bind在call
var newBind = Function.prototype.call.bind(Function.prototype.bind); function f(){ console.log(this.v); } var o = {v:123} newBind(f,o)()//123
上面代码 newBind(f,o) 返回一个新函数,这个函数绑定了执行对象o,所以会打印出log 123.
对象的继承
大部分面向对象的编程语言,都是通过“类”(class)实现对象的继承。传统上,JavaScript 语言的继承不通过 class,而是通过“原型对象”(prototype)实现
-
原型对象
-
1.1构造函数的缺点
同一个构造函数的多个实例之间,无法共享属性:
function Cat(name, color) { this.name = name; this.color = color; this.meow = function () { console.log('喵喵'); }; } var cat1 = new Cat('大毛', '白色'); var cat2 = new Cat('二毛', '黑色'); cat1.meow === cat2.meow // false
每新建一个实例,就会新建一个 meow 方法,这既没有必要又浪费了系统资源,因为方法都是同样的行为,完全应该共享.
这个问题的解决方法就是JS的原型对象 prototype
-
1.2 prototype 属性的作用
如果属性和方法定义在原型上,那么所有的实例对象都能共享,不仅节省了内存,还体现了实例对象之间的联系.
JS 规定 每个函数都具有prototype属性;
普通函数的prototype属性没有多大意义;
但是对于构造函数的prototype来说, 通过构造函数生成的实例对象,它们的prototype都会指向构造函数的prototype属性;
prototype属性的值是一个对象,也叫做原型对象,它负责定义所有实例对象共享的属性和方法,这也是它被叫做原型对象的原因,而实例对象可以看作是从原型对象衍生出来的子对象.
-
1.3 原型链
所有对象都有自己的原型对象(prototype)。一方面,任何一个对象,都可以充当其他对象的原型;另一方面,由于原型对象也是对象,所以它也有自己的原型。因此,就会形成一个“原型链”(prototype chain):对象到原型,再到原型的原型……
所有对象都继承了
Object.prototype
的属性。这就是所有对象都有valueOf
和toString
方法的原因,因为这是从Object.prototype
继承的。那么,
Object.prototype
对象有没有它的原型呢?回答是Object.prototype
的原型是null
。null
没有任何属性和方法,也没有自己的原型。因此,原型链的尽头就是null
。 -
1.4 constructor 属性
prototype 对象有一个 constructor 属性,默认指向 prototype 对象所在的构造函数.
由于 constructor 属性定义在 prototype 上面,意味着可以被所有实例对象继承.
constructor 属性的作用是,可以得知某个对象到底是哪一个构造函数产生的.
function F(){}; var f = new F(); f.constructor === F //true f.constructor === F.prototype.constructor //true
-
-
instanceof 运算符
instanceof 运算符的左边是实例对象,右边是构造函数
返回一个布尔值,表示对象是否为某个构造函数的实例.
它会检查右边构造函数的原型对象(prototype),看是否在左边对象的原型链上。
由于 instanceof 检查实例对象的整个原型链,因此同一个实例对象,可能会对多个构造函数都返回 true
var d = new Date(); d instanceof Date // true d instanceof Object // true
任意对象 除了 null 都是Object 的实例,所以 instanceof 运算符可以判断一个值是不是 null 的对象
var target;
target instanceof Object
有一种情况除外,就是左边实例对象的原型是null 这样,instanceof 的判断就会失真,
Object.create(null) instanceof Object
在这句代码中,会检查Object 的prototype属性是否在Object.create(null) 实例的原型链上,而这个实例的原型是null 所以不在它的原型链上,结果返回 false,但实际上 Object.create(null) 就是一个Object实例.
注意: instanceof 运算符是能用于对象 ,不能用于原始类型的值
利用
instanceof
运算符,还可以巧妙地解决,调用构造函数时忘了加new
命令的问题。function Fubar (foo, bar) { if (this instanceof Fubar) { this._foo = foo; this._bar = bar; } else { return new Fubar(foo, bar); } }
-
构造函数的继承
function Shape(){ this.x = 0; this.y =0; } Shape.prototype.move = function(x, y){ this.x += x; this.y += y } //新建 Rectangle 继承 Shape function Rectangle(){ Shape.call(this); } Rectangle.prototype = Object.create(Shape.prototype); Rectangle.prototype.constructor = Rectangle;
-
多重继承
JS 不提供多重继承,可以通过变通方法来实现
function M1(){ this.hello = 'hello'; } function M2(){ this.world = 'world'; } function S(){ M1.call(this); M2.call(this); } S.prototype =Object.create(M1.prototype); Object.assign(S.prototype,M2.prototype); S.prototype.constructor = S;
-
模块
如何利用对象实现模块化的效果
-
5.1 基本的实现方法
模块是实现特定功能的一组属性和方法的封装.
简单的做法是把模块写成一个对象
但是这样做会暴露内部所有成员,内部属性也可以随时被外部改写.
-
5.2 封装私有变量:构造函数的写法
function StringBuilder() { var buffer = []; this.add = function (str) { buffer.push(str); }; this.toString = function () { return buffer.join(''); }; }
上面代码中,
buffer
是模块的私有变量。一旦生成实例对象,外部是无法直接访问buffer
的。但是,这种方法将私有变量封装在构造函数中,导致构造函数与实例对象是一体的,总是存在于内存之中,无法在使用完成后清除。这意味着,构造函数有双重作用,既用来塑造实例对象,又用来保存实例对象的数据,违背了构造函数与实例对象在数据上相分离的原则(即实例对象的数据,不应该保存在实例对象以外)。同时,非常耗费内存。 -
5.3 封装私有变量:立即执行函数的写法
将相关的属性和方法封装在一个函数作用域里面,可以达到不暴露私有成员的目的.
var module1 = (function(){ var _count = 0; var m1 = function(){}; var m2 = function(){}; return { m1:m1, m2:m2 } })();
-
5.4 模块的放大模式
(function($, window, document) { function go(num) { } function handleEvents() { } function initialize() { } function dieCarouselDie() { } //attach to the global scope window.finalCarousel = { init : initialize, destroy : dieCarouselDie } })( jQuery, window, document );
Object 对象的相关方法
-
Object.getPrototypeOf( )
获取原型的标准方法.
// 空对象的原型是 Object.prototype Object.getPrototypeOf({}) === Object.prototype // true // Object.prototype 的原型是 null Object.getPrototypeOf(Object.prototype) === null // true // 函数的原型是 Function.prototype function f() {} Object.getPrototypeOf(f) === Function.prototype // true
-
Object.setPrototypeOf( )
为参数对象设置原型,接受两个参数,一个是现有对象,二个是原型对象
Object.setPrototypeOf 方法实现对 new 命令的模拟
var F = function (){ this.foo = 'bar'; } var f = Object.setPrototypeOf({},F.prototype); // then bind f to this F.call(f);
-
Object.create( )
生成实例一般通过 new 加构造方法实现,但是有时候,只有一个现成的实例,或者找不到相应的构造方法怎么办?
Object.create( ) 用来接受一个对象实例,然后以这个对象实例为原型,返回一个对象,该对象完全继承原型对象的属性.
此方法的模拟:
if(typeof Object.create !=='function'){ //如果当前环境没有Object.create方法 Object.create = function(obj){ var F = function(){}; F.prototype = obj; return new F(); }
除了原型对象,该方法还接受第二个参数:属性描述对象; 它所描述的对象属性会添加到返回的实例对象,作为该实例对象自身的属性.
var obj = Object.create({},{ p:{ value:123, enumerable:true, configurable:true, writable:true }, p2:{ value:"abc", enumerable:true, configurable:true, writable:true } }); // 等同于 var obj = Object.create({}); obj.p = 123; obj.p2 = "abc";
-
Object.prototype.isPrototypeOf( )
实例对象的 isPrototypeOf 方法用来判断该对象是否是参数对象的原型, 方法由原型对象来调用.
只要是处在参数对象原型链上的原型,调用此方法时,都会返回 true
-
Object.prototype.__proto__
实例对象的 __proto__ 属性,返回该对象的原型. 该属性可读写.
根据语言标准,__proto__ 属性只有浏览器才需要部署,其它环境可以没有这个属性.它的前后分别有两个下划线,表明是一个内部属性,不应该对使用者暴露,因此,应该尽量少用这个属性,而总是用 Object.getPrototypeOf() and Object.setPrototypeOf() 进行原型对象的读写.
-
Object.getOwnPropertyNames( )
返回所有自身属性名组成的数组,包括不可枚举属性,
如果要获取可枚举属性,用Object.keys( );
-
Object.prototype.hasOwnProperty( )
返回参数是在对象自身定义,还是定义在原型链上.
它是JS 中 唯一一个处理对象属性时,不会遍历原型链的方法.
-
in 运算符和 for … in 循环
in 运算符返回一个布尔值, 表示一个对象是否具有某个属性,它不区分该属性是自身属性还是继承来的属性.
获取对象所有可遍历属性,不管是自身的还是继承的,可以使用 for in 循环.
var o1 = {p1:123}; var o2 = Object.create(o1,{ p2:{value:"ab",enumerable:true} }); for(t in o2){ console.log(t); } //p2 //p1
下面的方法用于获取对象的所有属性,包括继承来的和不可枚举的
function getAllpropertyNames(obj){ var props = {}; while(obj){ Object.getOwnPropertyNames(obj).forEach(function(p){ props[p] = true; }); obj = Object.getPrototypeOf(obj); } return Object.getOwnPropertyNames(props) }
-
对象的拷贝
- 确保拷贝后的对象与原对象有同样的原型
- 确保拷贝后的对象与原对象有同样的属性
function objCopy(origin){ var obj = Object.create(Object.getPrototypeOf(origin)); copyOwnPropertiesFrom(copy,orig); return copy; } function copyOwnPropertiesFrom(target,from){ Object.getOwnPropertyNames(from).forEach(function(p){ var des = Object.getOwnPropertyDescriptor(from,p); Object.defineProperty(target,p,des); }); return target; }
另一种更简便的写法是 ES2017引入的
Object.getOwnProperyDescriptors
function copyObject(orig){ return Object.create(Object.getPrototypeOf(orig), Object.getOwnPropertyDescriptors(orig) ); }