本系列基于阮一峰老师的《JavaScrip语言入门教程》或《JavaScript教程》记录整理,教程采用知识共享 署名-相同方式共享 3.0协议。这几乎是学习js最好的教程之一(去掉之一都不过分)
最好的教程而阮一峰老师又采用开源方式共享出来,之所以重新记录一遍,一是强迫自己重新认真读一遍学一遍;二是对其中知识点有个自己的记录,加深自己的理解;三是感谢这么好的教程,希望更多人阅读了解
面向对象编程
实例对象与 new 命令
面向对象编程(
Object Oriented Programming
,OOP
)将现实世界中的实物、逻辑操作及各种复杂关系抽象为一个个对象,每一个对象完成一定的功能,用来接受信息、处理数据或执行操作、发布信息等,通过继承还能实现复用和功能扩展。比起由一系列函数或指令组成的传统的过程式编程(procedural programming
)更适合大型项目。什么是"对象"(
object
):(1)对象是单个实物的抽象。(2)对象是一个容器,封装了属性(property)和方法(method)。属性是对象的状态,方法是对象的行为(完成某种任务)。生成对象时,通常需要一个模板,表示某一类实物的共同特征,然后根据模板生成。在C++、java、c#等语言中都有类(class)的概念。"类"就是对象的模板,对象是"类"的实例(即类的一个具体对象)。JavaScript的对象体系基于构造函数(
constructor
)和原型链(prototype
)构成。JavaScript 语言中构造函数(
constructor
)就是对象的模板,描述实例对象的基本结构。"构造函数"就是专门用来生成实例对象的函数。一个构造函数,可以生成多个实例对象,这些实例对象都有相同的结构。构造函数和普通函数一样,但是有自己的特征和用法。
如下,Vehicle
就是构造函数。通常构造函数名字第一个字母大写(与普通函数作区分)。
var Vehicle = function () {
this.price = 1000;
};
构造函数的特点:
- 函数体内部使用了
this
关键字,代表了所要生成的对象实例。 - 生成对象的时候,必须使用
new
命令。
-
new
命令的作用是执行构造函数,返回一个实例对象。
var Vehicle = function () {
this.price = 1000;
};
var v = new Vehicle();
v.price // 1000
如果忘记了new命令,就成了构造函数作为普通函数直接调用
为了保证构造函数必须使用new命令,解决办法有两种:
一、可以在构造函数内部使用严格模式。这样不使用new命令直接调用就会报错
var Vehicle = function () {
'use strict';
this.price = 1000;
};
var v = Vehicle(); // Uncaught TypeError: Cannot set property 'price' of undefined
严格模式中,函数内部的
this
不能指向全局对象,默认等于undefined
,导致不加new
调用会报错
二、在构造函数内部判断是否使用new
命令,如果没有,则根据参数返回一个实例对象。
function Vehicle(price) {
if (!(this instanceof Vehicle)) {
return new Vehicle(price);
}
this.price = price||1000;
};
var v1 = Vehicle();
var v2 = new Vehicle();
- 使用
new
命令时,后面的函数依次执行下面的步骤。
- 创建一个空对象,作为将要返回的对象实例。
- 将这个空对象的原型,指向构造函数的
prototype
属性。 - 将这个空对象赋值给函数内部的this关键字。
- 开始执行构造函数内部的代码。
构造函数内部,this
指的是一个新生成的空对象。构造函数的目的就是操作一个空对象(即this
对象),将其"构造"为需要的样子。
如果构造函数内部有return语句且return后面跟着一个对象,new命令会返回return语句指定的对象;否则,就会不管return语句,返回this对象。
var Vehicle = function () {
this.price = 1000;
return 1000; // 忽略非对象的return语句
}; (new Vehicle()) === 1000如果return返回的是其他对象而不是this,那么new命令将会返回这个新对象
如果对普通函数(内部没有this关键字的函数)使用new命令,则会返回一个空对象。
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);
- 函数内部的
new.target
属性。如果当前函数是new命令调用,new.target
指向当前函数,否则为undefined
。
function f() {
console.log(new.target === f);
}
f() // false
new f() // true
此属性可判断是否使用new命令调用了函数
function f() {
if (!new.target) {
throw new Error('请使用 new 命令调用!');
}
// ...
}
f() // Uncaught Error: 请使用 new 命令调用!
-
Object.create()
创建实例对象
通常使用构造函数作为生成实例对象的模板。但是如果没有构造函数只有对象时,可以使用Object.create()
方法以一个对象作为模板,生成新的实例对象。
如下,对象person1
是person2
的模板,后者继承了前者的属性和方法。
var person1 = {
name: '张三',
age: 38,
greeting: function() {
console.log('你好,我是' + this.name + '。');
}
};
var person2 = Object.create(person1);
person2.name; // "张三"
person2.name="李四" // "李四"
person2.greeting() // 你好,我是李四。
person1.greeting() // 你好,我是张三。
this关键字
-
this
关键字总是返回一个对象,或指向一个对象。 -
this
就是属性或方法"当前"所在的对象。也就是说,如果改变属性或方法所在的对象,就可以改变this的指向
将对象的属性赋给另一个对象,改变属性所在对象,可以改变this的指向。
如下,通过改变函数f
所在的对象,实现this的改变
function f() {
return '姓名:'+ this.name;
}
var A = {
name: '张三',
describe: f
};
var B = {
name: '李四',
describe: f
};
f() // "姓名:"
A.describe() // "姓名:张三"
B.describe() // "姓名:李四"
只要函数被赋给另一个变量,this的指向就会变。
JavaScript中,一切皆对象。运行环境也是对象(顶层函数中,this指向window对象),函数都是在某个对象之中运行,
this
就是函数运行时所在的对象(环境)。同时this的指向是动态的this
的本质或this
的设计目的:
js的对象在内存的结构是这样的,对象存在堆中,当把对象赋值给一个变量时,实际是将对象在堆中的内存地址赋值给变量。如下,将对象的地址(reference
)赋值给变量obj
var obj = { foo: 5 };
读取obj.foo
的过程是,先从obj拿到内存地址,然后从该地址读出原始的对象,返回它的foo
属性
原始的对象以字典结构保存,每一个属性名都对应一个属性描述对象。比如上面的属性foo
实际保存形式如下,foo
属性的值保存在属性描述对象的value
属性里面:
{
foo: {
[[value]]: 5
[[writable]]: true
[[enumerable]]: true
[[configurable]]: true
}
}
当属性的值是函数时
var obj = { foo: function () {} };
js将函数单独保存在内存中,将函数的地址赋值给foo
属性的value
属性。
{
foo: {
[[value]]: 函数的地址
...
}
}
因为函数是单独存在的值,所以可以在不同的环境(上下文)执行
JavaScript允许在函数体内部,引用当前环境的其他变量。
如下,函数体使用的变量x由运行环境提供。
var f = function () {
console.log(x);
};
由于函数可以在不同的运行环境执行,所以需要一种机制,可以在函数体内部获得当前的运行环境(context)。所以this
就被用来设计为,在函数体内部,指代函数当前的运行环境。
如下,函数体中this.x
就指当前运行环境的x
。
var f = function () {
console.log(this.x);
}
-
this
的使用场合
- 全局环境使用
this
,指的是顶层对象window
。 - 构造函数中的
this
,指的是实例对象。 - 对象的方法里面包含
this
,this
的指向就是方法运行时所在的对象。该方法赋值给另一个对象,会改变this
的指向。
关于this
的指向并不好把握,比如下面的例子
var obj ={
foo: function () {
console.log(this);
}
};
obj.foo() // obj
如上,通过调用boj对象的foo方法,输出this为当前的obj对象。但是,如果使用下面的形式,都会改变this的指向
// 情况一
(obj.foo = obj.foo)() // window
// 情况二
(false || obj.foo)() // window
// 情况三
(1, obj.foo)() // window
上面代码中,obj.foo
是获取出来之后再调用,相当于一个值,这个值在调用的时候,运行环境已经从obj
变为了全局环境,this
的指向变为了window
可以这样理解,在js引擎内部,obj
对象和obj.foo
函数储存在两个内存地址,称为地址一和地址二。obj.foo()
调用时,是从地址一调用地址二,因此地址二的运行环境是地址一,this
指向obj
。上面三种情况,都是直接取出地址二进行调用(即取出函数调用),这样的话,运行环境就是全局环境,this
指向的是全局环境。上面三种情况等同于下面的代码:
// 情况一
(obj.foo = function () {
console.log(this);
})()
// 等同于
(function () {
console.log(this);
})()
// 情况二
(false || function () {
console.log(this);
})()
// 情况三
(1, function () {
console.log(this);
})()
this
所在的方法不在对象的第一层时,这时this
指向当前一层的对象(即当前所在的对象),而不会继承更上面的层。
var a = {
p: 'Hello',
b: {
m: function() {
console.log(this.p);
}
}
};
a.b.m() // undefined
-
this
使用中注意点:
- 避免多层
this
。用于this
的指向可变,尽量不要在函数中包含多层this
通过添加指向this的变量,实现多层this的使用
var o = {
f1: function() {
console.log(this);
var that = this;
var f2 = function() {
console.log(that);
}();
}
}
o.f1()
// Object
// Object
JavaScript严格模式下,如果函数内部的this
指向顶层对象,就会报错。
- 避免使用数组处理方法(
map
和foreach
方法中的参数函数)中的this
map
、foreach
方法的回调函数中的this
指向window对象。解决办法是使用一个中间变量固定this,或者使用this
作为map
、foreach
方法的第二个参数
// 中间变量
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
// 第二个参数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
(往往会改变指向)。
-
this
的动态切换,既体现了灵活,又使编程变得困难和模糊。js提供了call
、apply
、bind
方法,来切换/固定this
的指向。 -
Function.prototype.call()
:函数实例的call
方法,可以指定函数内部this
的指向(即函数执行时所在的作用域),然后在指定的作用域中调用该函数
如下,使用call改变作用域6
var obj = {};
var f = function () {
return this;
};
f() === window // true
f.call(obj) === obj // true
call
方法的第一个参数,应该是一个对象。如果参数为空、null
和undefined
,则this指向全局对象。
var n = 123;
var obj = { n: 456 };
function a() {
console.log(this.n);
}
a.call() // 123
a.call(null) // 123
a.call(undefined) // 123
a.call(window) // 123
a.call(obj) // 456
call
方法的第一个参数是一个原始值,则原始值会自动转成对应的包装对象,然后传入call
方法。
var f = function () {
return this;
};
f.call(5) // Number {[[PrimitiveValue]]: 5}
call
方法除第一个参数表示调用函数的作用域,其他参数以列表的形式传递,表示函数执行时的参数
func.call(thisValue, arg1, arg2, ...)
call方法的一个应用是调用对象的原生方法。
var obj = {};
obj.hasOwnProperty('toString') // false
// 覆盖掉继承的 hasOwnProperty 方法
obj.hasOwnProperty = function () {
return true;
};
obj.hasOwnProperty('toString') // true
Object.prototype.hasOwnProperty.call(obj, 'toString') // false
-
Function.prototype.apply()
:apply
方法的作用,也是改变this
指向,然后再调用该函数。但是它接收的是一个数组作为函数执行时的参数,
func.apply(thisValue, [arg1, arg2, ...])
和call
一样,第一个参数是this
指向的对象。null或undefined表示全局对象。第二个参数是数组,表示传入原函数的参数
apply
数组,call
列表
(1)找出数组最大元素
js默认没有找出数组最大元素的函数,结合apply
和Math.max
可实现返回数组的最大元素
var a = [10, 2, 4, 15, 9];
Math.max.apply(null, a) // 15
(2)将数组的空元素变为undefined
结合apply
和Array
构造函数将数组的空元素变成undefined
。
Array.apply(null, ['a', ,'b']) // [ 'a', undefined, 'b' ]
forEach
等循环方法会跳过空元素,但是不会跳过undefined
(3)转换类似数组的对象
利用数组对象的slice
方法,可以将一个类似数组的对象(如arguments
对象)转为真正的数组。
Array.prototype.slice.apply({0: 1, length: 1}) // [1]
Array.prototype.slice.apply({0: 1}) // []
Array.prototype.slice.apply({0: 1, length: 2}) // [1, 空]
Array.prototype.slice.apply({length: 1}) // [空]
(4)绑定回调函数的对象
可以在事件方法等回调函数中,通过apply
/call
绑定方法调用的对象,修改this指向
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);
因为apply()
/call()
方法在绑定函数执行时所在的对象时,还会立即执行函数,因此需要把绑定语句写在一个函数体内。
-
Function.prototype.bind()
:bind()
方法将函数体内的this
绑定到某个对象,然后返回一个新函数。
如下是一个通过赋值导致函数内部this指向改变的示例。
var d = new Date();
d.getTime() // 1596621203097
var print = d.getTime;
print() // Uncaught TypeError: this is not a Date object.
将d.getTime
赋值给变量print
后,方法内部的this由原来指向Date对象实例改为了window对象,print()
执行报错。
使用bind()
方法绑定函数执行的this指向,可以解决这个问题。
var print = d.getTime.bind(d);
undefined
print() // 1596621203097
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
绑定的是全局对象(浏览器环境为window
)
-
bind()
方法特定:
- 每一次返回一个新函数
这就导致,如果绑定事件时直接使用bind()
会绑定为一个匿名函数,导致无法取消事件绑定
element.addEventListener('click', o.m.bind(o));
// 如下取消是无效的
element.removeEventListener('click', o.m.bind(o));
正确写法:
var listener = o.m.bind(o);
element.addEventListener('click', listener);
// ...
element.removeEventListener('click', listener);
结合回调函数使用。将包含
this
的方法直接当做回调函数,会导致函数执行时改变了this的指向,从而出错。解决办法是使用bind()
方法绑定回调函数的this
对象。当然,也可使用中间变量固定this
结合
call()
方法使用。改写一些JS原生方法的使用
如下数组的slice方法
[1, 2, 3].slice(0, 1) // [1]
// 等同于
Array.prototype.slice.call([1, 2, 3], 0, 1) // [1]
call()
方法实质上是调用Function.prototype.call()
方法。
// 上面等同于
var slice = Function.prototype.call.bind(Array.prototype.slice);
slice([1, 2, 3], 0, 1) // [1]
相当于在Array.prototype.slice
调用Function.prototype.call
,参数为(对象,slice的参数)
类似的写法:
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]
更进一步bind
的调用也可以改写:在Function.prototype.bind
上调用call
方法(返回的是一个新方法),方法参数是(this对象,bind方法参数)
。即最终结果是在this对象
上执行bind
方法并传递参数。(有些绕)
function f() {
console.log(this.v);
}
var o = { v: 123 };
var bind = Function.prototype.call.bind(Function.prototype.bind);
bind(f, o)() // 123
对象的继承
- 对象的继承可以实现代码的复用
- 传统JavaScript的继承是通过"原型对象"(prototype)实现的。即js的原型链继承。ES6引入了class语法,实现基于class的继承
- 构造函数的缺点:构造函数中通过给
this
对象的属性赋值,可以很方便地定义实例对象属性。但是这种方式,同一个构造函数的多个实例之间无法共享属性。
function Cat(name, color) {
this.name = name;
this.color = color;
this.features = {
species:'猫',
habits:'肉食夜行动物'
};
this.meow = function () {
console.log('喵喵');
};
}
var cat1 = new Cat('大毛', '白色');
var cat2 = new Cat('二毛', '黑色');
cat1.meow === cat2.meow // false
cat1.features === cat2.features // false
cat1
和cat2
是同一个构造函数的两个实例,因为所有meow
方法和features
对所有实例具有同样的行为和属性,应该共享而不是每个实例都创建新的方法和属性,没必要又浪费系统资源。
原型对象(prototype
)用来在实例间共享属性。
- JavaScript继承机制的设计思想:原型对象的所有属性和方法,都能被实例对象共享
- JavaScript规定,每个函数都有一个
prototype
属性,指向一个对象。
function f() {}
typeof f.prototype // "object"
普通函数基本不会用prototype
属性
构造函数生成实例的时候,构造函数的prototype
属性会自动成为实例对象的原型。
function Cat(name, color) {
this.name = name;
}
Cat.prototype.color = 'white';
Cat.prototype.features = {
species:'猫',
habits:'肉食夜行动物'
};
Cat.prototype.meow = function () {
console.log('喵喵');
};
var cat1 = new Cat('大毛');
var cat2 = new Cat('二毛');
原型对象的属性不是实例对象自身的属性。其变动体现在所有实例对象上。
当实例对象本身没有某个属性或方法的时候,它会到原型对象去寻找该属性或方法。如果实例对象自身就有某个属性或方法,则不会再去原型对象寻找这个属性或方法。
原型对象的作用,是定义所有实例对象共享的属性和方法。这也是被称为原型对象的原因。实例对象可以视作从原型对象衍生出来的子对象。
JavaScript规定,所有对象都有自己的原型对象(
prototype
)。任何一个对象,都可以充当其他对象的原型;而由于原型对象也是对象,所以它也有自己的原型。这就形成一个"原型链"(prototype chain
):对象到原型,再到原型的原型...所有对象的原型最终都可以上溯到
Object.prototype
,即Object
构造函数的prototype
属性。所有对象都继承了Object.prototype
的属性。
比如所有对象都有valueOf
和toString
方法,就是从Object.prototype
继承的
而Object.prototype
对象的原型是null
。原型链的尽头是null
null
没有任何属性和方法,也没有自己的原型
Object.getPrototypeOf(Object.prototype) // null
如果对象自身和它的原型,都定义了一个同名属性,则优先读取对象自身的属性,这叫做"覆盖"(
overriding
)。prototype
对象有一个constructor
属性,默认指向prototype
对象所在的构造函数。
function P() {}
P.prototype.constructor === P // true
constructor
属性的作用是,可以得知某个实例对象由哪一个构造函数产生。另外,有了constructor
属性就可以从一个实例对象新建另一个实例。
function Constr() {}
var x = new Constr();
var y = new x.constructor();
y instanceof Constr // true
借助constructor
可以在实例方法中调用自身的构造函数
Constr.prototype.createCopy = function () {
return new this.constructor();
};
-
constructor
属性表明了原型对象与构造函数之间的关联关系。因此如果修改原型对象,一般需要同时修改constructor
属性
function Person(name) {
this.name = name;
}
Person.prototype.constructor === Person // true
Person.prototype = {
method: function () {}
};
Person.prototype.constructor === Person // false
Person.prototype.constructor === Object // true
修改原型对象时,一般要同时修改constructor
属性的指向
// 坏的写法
C.prototype = {
method1: function (...) { ... },
// ...
};
// 好的写法
C.prototype = {
constructor: C,
method1: function (...) { ... },
// ...
};
// 更好的写法
C.prototype.method1 = function (...) { ... };
constructor
属性的name
属性返回构造函数的名称。instanceof
表示对象是否为某个构造函数的实例。instanceof
做判断时会检查右边构造函数的原型对象(prototype
)是否在左边实例对象的原型链上。
v instanceof Vehicle
// 等同于
Vehicle.prototype.isPrototypeOf(v)
instanceof
会检查整个原型链,因此使用instanceof
判断时,实例对象的原型链上可能返回多个构造函数的原型对象
var d = new Date();
d instanceof Date // true
d instanceof Object // true
任意对象(除了null
)都是Object
的实例。
var nullObj=null;
typeof nullObj === 'object' && !(nullObj instanceof Object); // true
如果一个对象的原型是null
,instanceof
的判断就会失真。
利用instanceof
可以解决调用构造函数时忘了加new
的问题
- 构造函数的继承
子类整体继承父类
一、在子类的构造函数中调用父类的构造函数
function Sub(value) {
Super.call(this); // 继承父类实例的属性
this.prop = value;
}
// 或者使用另一种写法
function Sub() {
this.base = Super;
this.base();
}
二、让子类的原型指向父类的原型,继承父类原型
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
Sub.prototype.method = '...';
使用Object.create(Super.prototype)
赋值给子类的原型,防止引用赋值,后面的修改影响父类的原型。
上面是比较正确或严谨的写法。比较粗略的写法是直接将一个父类实例赋值给子类的原型
Sub.prototype = new Super();
这种方式在子类中会继承父类实例的方法(通常可能不需要具有父类的实例方法),不推荐
子类中继承父类的单个方法
ClassB.prototype.print = function() {
ClassA.prototype.print.call(this);
// self code
}
- 多重继承:JavaScript不提供多重继承功能,即不允许一个对象同时继承多个对象。
但是可以通过合并两个父类的原型的形式,间接变通的实现多重继承
function M1() {
this.hello = 'hello';
}
function M2() {
this.world = 'world';
}
function S() {
M1.call(this);
M2.call(this);
}
// 继承 M1
S.prototype = Object.create(M1.prototype);
// 继承链上加入 M2
Object.assign(S.prototype, M2.prototype);
// 指定构造函数
S.prototype.constructor = S;
var s = new S();
s.hello // 'hello'
s.world // 'world'
这种子类S
同时继承了父类M1
和M2
的模式又称为 Mixin
(混入
)
-
JavaScript
不是一种模块化编程语言,ES6
才开始支持"类"和"模块"。但是可以利用对象实现模块的效果 - 模块是实现特定功能的一组属性和方法的封装。所以模块的实现最简单的方式就是把模块写成一个对象,所有模块成员都位于对象里面
- 把模块写成一个对象
var module1 = new Object({
_count : 0,
m1 : function (){
//...
},
m2 : function (){
//...
}
});
函数m1
、m2
和属性_count
都封装在module1
对象中。使用中直接调用这个对象的属性即可。
但是,这种写法暴露了所有的模块成员,内部状态可以被外部改写。比如,在外部直接改写内部_count
的值:module1._count = 5;
- 使用构造函数封装私有变量
如下,通过构造函数封装实例的私有变量
function StringBuilder() {
var buffer = [];
this.add = function (str) {
buffer.push(str);
};
this.toString = function () {
return buffer.join('');
};
}
如下,私有变量buffer
在实例对象中,外部是无法直接访问的。
但是,这种方法将私有变量封装在构造函数中,构造函数会和实例对象一直存在于内存中,无法在使用完成后清除。即构造函数的作用既用来生成实例对象,又用来保存实例对象的数据,违背了构造函数与实例对象在数据上相分离的原则(即实例对象的数据,不应该保存在实例对象以外)。同时占用内存。
- 构造函数中将私有变量设置为实例属性
function StringBuilder() {
this._buffer = [];
}
StringBuilder.prototype = {
constructor: StringBuilder,
add: function (str) {
this._buffer.push(str);
},
toString: function () {
return this._buffer.join('');
}
};
这样私有变量就放在了实例对象中。但是私有变量仍然可以从外部读写
- 通过立即执行函数封装私有变量
通过"立即执行函数"(Immediately-Invoked Function Expression
,IIFE
),通过返回"闭包"的方法和属性,实现将属性和方法封装在一个函数作用域里面,函数内的属性作为私有成员不被暴露。
这就是js模块的基本写法:
var module1 = (function () {
var _count = 0;
var m1 = function () {
//...
};
var m2 = function () {
//...
};
return {
m1 : m1,
m2 : m2
};
})();
-
模块的放大模式
如果一个模块很大,必须分成几个部分,或者一个模块需要继承另一个模块,这时可以采用"放大模式"(augmentation
)。
如下,为模块module1
添加新方法,并返回新的module1
模块
var module1 = (function (mod){
mod.m3 = function () {
//...
};
return mod;
})(module1);
- "宽放大模式"(
Loose augmentation
)
在立即执行函数的参数中添加空对象,防止加载一个不存在的对象,从而报错或出意外
var module1 = (function (mod) {
//...
return mod;
})(window.module1 || {});
- 全局变量的输入
模块最重要的是"独立性"。因此为了在模块内部调用(使用)全局变量,必须显式地将其他变量输入模块内。
比如,下面module1
用到了jQuery库(模块),则可以将其作为参数输入module1
。保证模块的独立性,并且表明模块之间的依赖关系
var module1 = (function ($) {
//...
})(jQuery);
立即执行函数还可以起到类似命名空间的作用
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(obj,prototypeObj)
new
命令可以使用Object.setPrototypeOf
方法模拟。
var F = function () {
this.foo = 'bar';
};
var f = new F();
// 等同于
var f = Object.setPrototypeOf({}, F.prototype);
F.call(f);
-
Object.create
方法以一个对象为原型,返回一个实例对象。该实例完全继承原型对象的属性。
// 原型对象
var A = {
print: function () {
console.log('hello');
}
};
// 实例对象
var B = Object.create(A);
Object.getPrototypeOf(B) === A // true
B.print() // hello
B.print === A.print // true
Object.create
方法的实现可以用下面的代码代替
if (typeof Object.create !== 'function') {
Object.create = function (obj) {
function F() {}
F.prototype = obj;
return new F();
};
}
生成新的空对象,如下四种是等价的
var obj1 = Object.create({});
var obj2 = Object.create(Object.prototype);
var obj3 = new Object();
var obj4 = {};
Object.create
的参数为null
可以生成一个不继承任何属性(没有toString
和valueOf
方法)的对象
var obj = Object.create(null);
Object.create
方法必须指定参数且为对象,否则报错。Object.create
创建的对象的原型是引用赋值,即动态继承原型。
Object.create
方法还可以接受的第二个参数是属性描述对象,描述的对象属性会添加到实例对象的自身属性上。
var obj = Object.create({}, {
p1: {
value: 123,
enumerable: true,
configurable: true,
writable: true,
},
p2: {
value: 'abc',
enumerable: true,
configurable: true,
writable: true,
}
});
// 等同于
var obj = Object.create({});
obj.p1 = 123;
obj.p2 = 'abc';
Object.create
方法生成的对象会继承它的原型对象的构造函数。
-
Object.prototype.isPrototypeOf()
:实例对象的isPrototypeOf
方法判断该对象是否为参数对象原型链上的原型。
Object.prototype
位于除了直接继承自null的对象之外的所有对象的原型链上。
Object.prototype.isPrototypeOf({}) // true
Object.prototype.isPrototypeOf([]) // true
Object.prototype.isPrototypeOf(/xyz/) // true
Object.prototype.isPrototypeOf(Object.create(null)) // false
- 关于
__proto__
属性。__proto__
属性是实例对象的属性,表示实例对象的原型(可读写)。实例对象(或非函数对象)无法通过prototype
属性获取原型(只有参数才有prototype
属性),而__proto__
属性默认应该是私有属性,不应该被读写,并且__proto__
属性只有浏览器才需要部署。因此,对原型的读写操作正确做法是使用Object.getPrototypeOf()
和Object.setPrototypeOf()
Obj可以用__proto__
直接设置原型
- 关于
__proto__
和prototype
属性
如下,为构造函数、实例对象、普通对象中__proto__和prototype的对比
/** 构造函数的__proto__和prototype **/
var P=function(){}
P.prototype
// {constructor: ƒ}
P.__proto__
// ƒ () { [native code] }
P.__proto__===P.prototype
// false
P.__proto__===P.constructor.prototype
// true
P.__proto__===Object.getPrototypeOf(P)
// true
P.__proto__===Function.prototype
// true
P.constructor===Function
// true
/** 实例对象的__proto__和prototype **/
var p=new P()
p.prototype
// undefined
p.__proto__
// {constructor: ƒ}
p.__proto__===Object.getPrototypeOf(p)
// true
p.__proto__===P
// false
p.__proto__===P.prototype
// true
p.constructor===P
// true
/** 实例对象的__proto__和prototype **/
var obj={}
obj.prototype
// undefined
obj.__proto__===Object.getPrototypeOf(obj)
// true
obj.__proto__===Object.prototype
// true
obj.constructor===Object
// true
var nullObj=Object.create(null)
nullObj.__proto__
// undefined
nullObj
// {}无属性
几点总结:
js中,对象的原型通过
__proto__
属性获取,由此组成原型链及原型链的继承。__proto__
是对象自带的属性,除了null
和原型对象为null
的对象之外,所有的对象都有__proto__
属性。函数是对象,因此函数也有__proto__
属性prototype
属性是函数独有的属性,每个函数都有一个prototype
属性对象,作用是在实例对象间共享属性和方法。因此prototype
只会在构造函数中使用,表示实例对象的原型对象。面向对象中的继承由此实现。__proto__
属性指向当前对象的原型对象,即构造函数的prototype
属性。constructor
属性表示当前对象的构造函数函数也是对象,因此也拥有
__proto__
属性,指向当前函数的构造函数的prototype
属性。一个函数的constructor
是Function
,__proto__
是Function.prototype
-
__proto__
属性指向当前对象的原型对象,即构造函数的prototype
属性。
var obj = new Object();
obj.__proto__ === Object.prototype
// true
obj.__proto__ === obj.constructor.prototype
// true
- 获取一个对象
obj
的原型对象,有三种办法:
obj.__proto__
obj.constructor.prototype
Object.getPrototypeOf(obj)
但是 __proto__
属性只有浏览器环境才需要部署。obj.constructor.prototype
在手动改变原型对象时,可能会失效
如下,将构造函数C
的原型对象改为p
后。实例对象c.constructor.prototype
却没有指向p
。Object.getPrototypeOf(obj)
正确获取原型对象,是获取原型对象推荐使用的方法
var P = function () {};
var p = new P();
var C = function () {};
C.prototype = p;
var c = new C();
c.constructor.prototype === p // false
c.constructor.prototype === P.prototype // true
Object.getPrototypeOf(c) === p // true
上面变更原型对象的方法是不正确的。通常修改prototype
时,要同时设置constructor
属性。
C.prototype = p;
C.prototype.constructor = C;
var c = new C();
c.constructor.prototype === p // true
Object.getOwnPropertyNames()
返回对象自身所有属性的键名组成的数组(包括可遍历和不可遍历的所有属性)。Object.keys
返回对象自身所有可遍历的属性名组成的数组Object.prototype.hasOwnProperty()
返回一个属性是否为对象自身的属性
hasOwnProperty方法是 JavaScript 之中唯一一个处理对象属性时,不会遍历原型链的方法
-
in
运算符表示一个对象是否具有某个属性。即检查一个属性是否存在。
'length' in Date // true
'toString' in Date // true
for...in
循环可以获取一个对象所有可遍历的属性(自身和继承的属性)
通常使用如下方式,遍历对象自身的属性
for ( var name in object ) {
if ( object.hasOwnProperty(name) ) {
/* loop code */
}
}
- 获取一个对象的所有属性(包含自身的和继承的,以及可枚举和不可枚举的所有属性)
function inheritedPropertyNames(obj) {
var props = {};
while(obj) {
Object.getOwnPropertyNames(obj).forEach(function(p) {
props[p] = true;
});
obj = Object.getPrototypeOf(obj);
}
return Object.getOwnPropertyNames(props);
}
- 对象的拷贝
要拷贝一个对象,需要做到下面两点:
- 确保拷贝后的对象,与原对象具有同样的原型。
- 确保拷贝后的对象,与原对象具有同样的实例属性。
如下,为对象拷贝的实现:
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;
}
利用ES2017
引入的Object.getOwnPropertyDescriptors
可以更简便的实现
function copyObject(orig) {
return Object.create(
Object.getPrototypeOf(orig),
Object.getOwnPropertyDescriptors(orig)
);
}
严格模式(strict mode
)
- JavaScript提供代码执行的第二种模式:严格模式。严格模式从ES5引入,主要目的为:
- 明确禁止一些不合理、不严谨的语法,减少 JavaScript 语言的一些怪异行为。
- 增加更多报错的场合,消除代码运行的一些不安全之处,保证代码运行的安全。
- 提高编译器效率,增加运行速度。
- 为未来新版本的 JavaScript 语法做好铺垫。
- 严格模式的启用:在代码头部添加一行
'use strict';
即可。老版本的引擎会把它当作一行普通字符串,加以忽略。新版本的引擎就会进入严格模式。 -
use strict
放在脚本文件的第一行,整个脚本都将以严格模式运行。不在第一行则无效。 -
use strict
放在函数体的第一行,则整个函数以严格模式运行。 - 有时需要把不同脚本文件合并到一个文件。这时,如果一个是严格模式另一个不是,则合并后结果将会是不正确的。解决办法是可以把整个脚本文件放在一个立即执行的匿名函数中:
(function () {
'use strict';
// some code here
})();
- 严格模式下的显式报错
严格模式下js的语法更加严格,许多在正常模式下不会报错的错误代码都会显式的报错
如下几项操作严格模式下都会报错:
只读属性不可写;比如字符串的
length
属性不可配置属性无法删除(
non-configurable
)只设置了取值器的属性不可写
禁止扩展的对象不可扩展
eval
、arguments
不可用作标识名
正常模式下,如果函数有多个重名的参数,可以用arguments[i]
读取。严格模式下属于语法错误。
函数不能有重名的参数
禁止八进制的前缀
0
表示。八进制使用数字0和字母O表示
- 严格模式下的安全限制
- 全局变量显式声明
- 禁止
this
关键字指向全局对象。避免无意中创造全局变量
// 正常模式
function f() {
console.log(this === window);
}
f() // true
// 严格模式
function f() {
'use strict';
console.log(this === undefined);
}
f() // true
严格模式下,函数直接调用时,内部的this
表示undefined
(未定义),因此可以用call
、apply
和bind
方法,将任意值绑定在this
上面。正常模式下,this
指向全局对象,如果绑定的值是非对象,将被自动转为对象再绑定上去,而null
和undefined
这两个无法转成对象的值,将被忽略。
函数内部禁止使用
fn.callee
、fn.caller
禁止使用
arguments.callee
、arguments.caller
arguments.callee
和arguments.caller
是两个历史遗留的变量,从来没有标准化过,现在已经取消
- 禁止删除变量。严格模式下使用
delete
命令删除一个变量,会报错。只有对象的属性,且属性的描述对象的configurable
属性设置为true
,才能被delete
命令删除。
- 静态绑定
禁止使用
with
语句创设
eval
作用域
正常模式下,JavaScript
语言有两种变量作用域(scope
):全局作用域和函数作用域。严格模式创设了第三种作用域:eval
作用域。
eval
所生成的变量只能用于eval
内部。
(function () {
'use strict';
var x = 2;
console.log(eval('var x = 5; x')) // 5
console.log(x) // 2
})()
eval
语句使用严格模式:
// 方式一
function f1(str){
'use strict';
return eval(str);
}
f1('undeclared_variable = 1'); // 报错
// 方式二
function f2(str){
return eval(str);
}
f2('"use strict";undeclared_variable = 1') // 报错
-
arguments
不再追踪参数的变化。严格模式下参数修改,arguments
不再联动跟着改变
- 面向
ECMAScript 6
- ES5的严格模式只允许在全局作用域或函数作用域声明函数。
- 保留字。严格模式新增了一些保留字:
implements
、interface
、let
、package
、private
、protected
、public
、static
、yield
等