概述
js语言特点
- 解释性
- 弱类型
- 宿主语言
- 轻量级
- 事件驱动和非阻塞式设计
- 面向对象和函数式编程
- 不提供
I/O相关API
语法
流程控制
function countDown(n) {
while(n-- > 0) console.log(n)
}
countDown(3) // 2 1 0
function countDown(n) {
while(--n > 0) console.log(n)
}
countDown(3) // 2 1
switch结构
switch('apple') {
case 'apple':
console.log('苹果')
break // 不加break会怎样
case 'pear':
console.log('梨')
break
default:
console.log('nothing')
}
三元表达式
const max = function(a,b,c) {
return a > b ? (a > c ? a : c) : (b > c ? b : c)
}
for循环
const f = function() {
let flag = true;
for(let i = 0;flag && i < 6;i ++) {
for(let j = 0;j < 3;j ++) {
if(j === 2) {
console.log(i + '=' + j) // 0 = 2
flag = false
break
}
}
}
}
f()
break和continue语句
break跳出当前代码块或循环continue终止本轮循环,返回循环头部开始下一轮循环
数据类型
- 基础类型:
number、string、boolean - 引用类型:
object - 特殊类型:
null、undefined ES6新增:symbol
判断数据类型的方法
typeof
以字符串的形式返回指定变量的数据类型。
注意:在ES6没有出来之前,typeof a值为undefined,即唯一一处使用未经声明过的变量不报错的情况。
缺点:引用类型中的数组、日期、正则也都有自己的具体实例,而typeof只是返回了处于原型链最顶端的Object类型,这应该不是我们想要的。
// 以下两种形式无法准确辨别
typeof [] // 'object'
typeof null // 'object'
// 优化typeof
var myTypeOf = function(o) {
var s = Object.prototype.toString.call(o);
return s.match(/\[object (.*?)\]/)[1].toLowerCase();
};
myTypeOf(null)
// 简易封装typeof
function myTypeof(target) {
var template = {
"[object Array]" : "array",
"[object Object]" : "object",
"[object Number]" : "number - object",
"[object Boolean]" : "boolean - object",
"[object String]" : "string - object"
}
if(target === null) {
return 'null'
}else if(typeof(target) == 'object') {
// 对象 数组 函数 包装类
let str = Object.prototype.toString.call(target)
return template(str)
}else typeof(target)
}
if(typeof(a) && -true + (+undefined) + "") {
console.log('基础扎实'); // 能打印出来
}
if(11 + "11" * 2 == 33) {
document.write('基础扎实');
}
!!" " + !!"" - !!false || document.write('不行了');
// 1
typeof NaN // 'number'
instanceof
验证某个对象是否为指定构造函数的实例。
关于Object()方法的三点注意:
- 若参数为对象,则返回自身,可作为判断某一变量是否为对象的依据
- 若参数为原始值,会将其转为对应包装对象的实例
- 若参数为
null、undefined、空,会返回一个空对象
缺点:instanceof只能用来判断两个对象是否属于实例关系,而不能判断一个对象实例具体属于哪种类型。
let obj = Object(1)
obj instanceof Number // true
obj instanceof Object // true
Object.prototype.toString.call() 推荐*
返回对象的类型字符串。
let obj = {}
obj.toString() // '[object Object]',第二个Object表示该值的构造函数
constructor
当函数被定义时,js引擎会为此函数添加prototype原型,然后在prototype上添加一个constructor属性,并让其指向此函数。
let f = new F(),F是构造函数,f是F的实例对象,此时F原型上的constructor传递到了f上,所以存在f.constructor == F。
F构造函数利用了原型对象上的constructor引用了自身,当F作为构造函数来创建对象时,原型上的constructor就被遗传到了实例对象上。这样做的意义在于,在新实例被创建出来之后,就具有可追溯的数据类型了。
注意:
null和undefined是无效的对象,不存在constructor
数值
js中所有数值都是以小数(64位浮点数)的形式进行存储的。当某些运算只有整数才能完成时,js会自动将64位浮点数转为32位整数进行计算。而由于浮点数计算不精确,会丢失精度,产生误差。
0.1 + 0.2 = 0.30000000004
0 / 0 // NaN
Infinity - Infinity // NaN
Infinity / Infinity // NaN
Infinity * 0 // NaN
运算规则:
NaN与任何数(包括自身)进行操作的结果都是falseInfinity与NaN比较的结果总是falseInfinity与undefined计算的结果总是NaN
全局方法
parseInt
将字符串转为整数。
- 当
parseInt后面的参数不是字符串时,会先转为字符串 - 字符串转整数是逐字符依次进行的,直到不能进行下去为止,并返回已经转换成功的部分
- 所以其返回值只有两种:十进制整数或
NaN
parseFloat
将字符串转为浮点数。
parseFloat('0.0314E+2') // 3.14
isNaN
判断值是否为NaN。
- 若传入其他类型值,会先转为数值再进行判断
isFinite
判断某个值是否为正常的数值,返回布尔值。
除了+ -Infinity、NaN、undefined,其他值都返回true。
对象
delete属性
用以删除对象自身的属性,成功返回true。
let obj = {p: 1}
Object.keys(obj) // ['p']
delete obj.p // true
obj.p // undefined
Object.keys(obj) // []
注意:
- 删除一个不存在的属性,
delete也返回true,这能说明什么? - 无法删除继承的属性,即使
delete会返回true - 只存在一种情况,
delete返回false,即属性存在且设置了不能被删除的情况
let obj = Object.defineProperty({}, 'p', {
value: 1,
configurable: false
})
遍历对象
for...in
遍历对象身上所有可遍历的属性(自身 + 继承)。
// 使用时往往结合hasOwnProperty
let obj = {a: 1,b: 2}
for(let prop in obj) {
if(obj.hasOwnProperty(prop)) {
console.log('obj自身上的属性值' + obj[prop])
}
}
数组
let arr = new Array(3) // 长度为3的稀松数组,3为数组长度 ( , , )
let arr = ['a','b']
arr.length = 0 // 清空数组
arr // []
遍历数组
for...in不推荐
Object.keys()不推荐
for循环
let a = [1,2,3]
for(let i = 0;i < a.length;i ++) {
console.log(a[i])
}
forEach推荐
let arr = [undefined,undefined]
arr.forEach(function(value,index) {
console.log('索引:' + index + '值:' + value)
})
类数组
两个要求:
- 要求对象的所有键名必须是正整数或0
- 存在
length属性
三大类数组:函数的arguments对象、大多数dom元素集、字符串。
arguments对象
function args() {return arguments}
let arrLike = args('a','b')
arrLike[0] // 'a'
arrLike.length // 2
arrLike instanceof Array // false
dom元素集
let elts = document.getElementsByTagName('p')
elts.length
elts instanceof Array // false
字符串
'abc'[1]
'abc'.length
'abc' instanceof Array // false
类数组转数组
数组的slice方法
function args() {return arguments}
let arrLike = args('a','b')
let arrNew = Array.prototype.slice.call(arrLike)
for循环遍历
var args = [];
for (var i = 0; i < arguments.length;i ++) {
args.push(arguments[i]); // 取出每一位push进数组中
}
函数
递归
两个条件:
- 函数自己调用自己
- 有出口(终止条件)
// 斐波那契数列
function fib(num) {
if (num === 0) return 0;
if (num === 1) return 1; // 设定出口
return fib(num - 2) + fib(num - 1); // 逐步递减到出口
}
fib(6) // 8
闭包
特点:
- 读取函数内部的变量
- 让变量始终存留在内存中(诞生环境一直存在着)
- 能够用于封装对象的私有属性和方法
- 不能滥用闭包,否则会造成原有作用域链不释放,造成内存泄露及网页性能等问题
function foo() {
var x = 1
function bar() {
console.log(x)
}
return bar // 将函数foo中的变量x连同函数bar一起保存到了外部,实现了变量私有化
}
var x = 2
var f = foo()
f() // 1
应用
计数器:
function createIncrementor(start) {
return function () {
console.log(start ++);
};
}
var inc = createIncrementor(5);
inc() // 5
inc() // 6
inc() // 7
私有化变量:
function Person(name) {
var _age;
function setAge(n) {
_age = n;
}
function getAge() { // 形成闭包
return _age;
}
return {
name: name,
getAge: getAge,
setAge: setAge
};
}
var p1 = Person('张三');
p1.setAge(25);
p1.getAge() // 25
做缓存(存储结构):
function eater() {
var food = "";
var obj = {
eat : function() {
console.log("food is" + " " + food);
console.log("I am eating" + " " + food);
food = "";
console.log("now,food is" + " " + food);
},
push : function(myFood) {
food = myFood;
}
}
return obj;
}
function test() {
var num = 100;
function a() {
num ++;
console.log(num);
}
function b() {
num --;
console.log(num);
}
return [a,b];
}
var myArr = test();
myArr[0](); // 101
myArr[1](); // 100
arguments对象
var f = function() {
console.log(arguments[0]); // 1
console.log(arguments[1]); // 2
console.log(arguments.length) // 3
}
f(1, 2, 3)
立即执行函数IIFE
作用:
- 避免污染全局变量
- 可封装私有变量
(function() {}());
(function() {})();
!function() {}();
~function() {}();
-function() {}();
+function() {}();
eval()
将字符串直接当作语句来执行。
常用来解析JSON数据字符串,不过还是推荐使用浏览器提供的JSON.parse方法。
运算
规则:所有运算一律转为数值后进行计算,除加法外其他运算符不会发生重载。
总结:
- 除数可以为0
- 求模结果的符号与除数无关
- 除法的结果为小数
- 浮点数计算结果不精确
- 取余运算结果的正负号由第一个运算子决定
- 先乘除模,后加减,如果优先级一致,则从左到右依次计算
对象相加
原理:
- 先调用对象的
valueOf方法,返回对象自身 - 再调用对象的
toString方法,默认返回[object Object],并将其转为字符串
var obj = {p: 1};
obj + 2 // "[object Object]2"
// 我们可以自定义valueof方法和toString方法
var obj = {
valueOf: function() {
return 1;
},
toString: function() {
return 'hello';
}
};
null和undefined比较
null是一个空对象,转为数值时为0undefined转为数值时为NaNnull和undefined与其他类型值比较为false,互相比较为true
数据类型转换
强制转换数据类型:Number、String、Boolean函数。
Number()
- 可被解析为数值的,转为对应数值
- 不能解析为数值的,返回
NaN - 空字符串转为
0
转换原理:
- 先调用对象自身的
valueOf方法,若返回原始类型数值,则直接对该值使用Number函数,不再进行后续步骤 - 若
valueOf方法返回的还是对象,则改为调用对象自身的toString方法,如果toString方法返回原始类型的值,则对该值使用Number函数,不再进行后续步骤;如果toString方法返回的是对象,报错 - 默认情况下,对象的
valueOf方法返回对象本身,所以总是会调用toString方法。 而toString方法返回对象的类型字符串,如:[object Object]。
Number函数与parseInt函数的差异
- 两者都会自动过滤掉一个字符串前导和后缀的空格
parseInt逐个解析字符Number整体转换字符Number将字符串转为数值比parseInt函数严格很多,只要有一个字符无法转换,直接NaN
String()
原理:与Number方法相似,只是互换了valueOf和toString方法的执行顺序。
- 先调用对象自身的
toString方法,如果返回原始类型的值,则对该值使用String函数,不再进行以下步骤 - 如果
toString方法还是返回对象,再调用原对象的valueOf方法,如果valueOf方法返回原始类型的值,对该值使用String函数,不再进行以下步骤;如果valueOf返回对象,直接报错
Boolean()
除以下六个值转换结果为false外,其余都为true。
false、+ -0、NaN、''、null、undefined
错误处理机制
Error实例对象
原生js提供了Error构造函数,所有抛出的错误都是这个构造函数的实例。
我们也可以自定义错误:
// 指定新建错误对象函数的原型继承自Error对象
UserError.prototype = new Error();
UserError.prototype.constructor = UserError;
new UserError('这是自定义的错误!');
throw语句
手动中断程序执行并抛出错误。
if(x < 0) {
throw new Error('x要大于0'); // 程序中断
console.log('我执行了吗')
}
try...catch
当try代码块抛出错误,js引擎立即把代码的执行转到catch代码块,被catch代码块捕获了。catch接收一个参数,表示try代码块抛出的值。
try{
throw new Error('出错了!');
console.log('这里不会执行')
}catch(e) {
console.log(e.name + ": " + e.message);
console.log(e.stack);
}
finally
try...catch结构允许在最后添加一个finally代码块,表示不管是否出现错误,都必须运行的语句。
function idle(x) {
try{
console.log(x);
return 'result';
}finally {
console.log("FINALLY");
}
}
idle('hello')
// hello FINALLY "result"
// 注意: return语句的执行是排在finally代码之前的,只是等finally代码执行完毕后才返回
function f() {
try{
console.log(0);
throw 'bug';
}catch(e) {
console.log(1);
return true; // 这句原本会延迟到finally代码块结束再执行
console.log(2); // 不会运行
}finally {
console.log(3);
return false; // 这句会覆盖掉前面那句return
console.log(4); // 不会运行
}
console.log(5); // 不会运行
}
var result = f();
// 0
// 1
// 3
作用域
执行期上下文
当函数执行前一刻,会创建一个执行期上下文的内部对象。执行期上下文定义了该函数执行时的环境,函数每次执行时所对应的执行期上下文都是独一无二的,所以多次调用同一个函数会创建多个执行期上下文。当函数执行完毕,它所产生的执行期上下文会立即被销毁。
查找变量
在当前函数中,从作用域链的顶端依次向下查找。函数刚刚出生的时候,[[scope]]作用域里面就已经存了GO,当函数执行时又会产生执行期上下文AO,并且将AO放在了作用域链的最顶端。
作用域链
每个函数都有一个执行期上下文的集合,叫作用域链。我们真正在这个函数里面去访问某一变量,要遵循这个函数的作用域链,按顺序去访问。
属性的访问
我们不能访问的属于隐式属性,这些属性仅供js引擎存取,[[scope]]就是其中之一。
[[scope]]指的就是我们所说的作用域,其中存储了运行期上下文的集合。[[scope]]中存储的是执行期上下文对象集合,这个集合呈链式链接,我们把这种链式链接叫做作用域链。
function a() { // a刚出生,是在全局下去看世界
function b() {
// 由于a执行产生b定义,所以b刚出生时的执行期上下文是a的最终结果;
// b是站在a的肩膀上去看世界,b里面产生的类似a产生的AO其实质就是a自身所产生的AO;
var b = 234;
}
var a = 123;
b(); // 当b执行的时候,产生自身的AO又会跑到执行期上下文的最上面
}
var glob = 100;
a();
// 图解
function a() {
function b() {
function c() {
}
c();
}
b();
}
a();
-----------------------------------
作用域 执行期上下文
a defined a.[[scope]] --> 0 : GO
a doing a.[[scope]] --> 0 : aAO
1 : GO
b defined b.[[scope]] --> 0 : aAO
1 : GO
b doing b.[[scope]] --> 0 : bAO
1 : aAO
2 : GO
c defined c.[[scope]] --> 0 : bAO
1 : aAO
2 : GO
c doing c.[[scope]] --> 0 : cAO
1 : bAO
2 : aAO
3 : GO
预编译
function fn(a) {
console.log(a); // function a() {}
var a = 123; // 预编译提升声明变量,但是赋值还是需要看的,将a = 123代替AO对象里a属性的值
console.log(a); // 123
function a() {} // 预编译已经将其提升,所以不用再看
console.log(a); // 123
var b = function() {} // var b不用看了,修改AO里的b,值为function() {}
console.log(b); // function() {}
function d() {}
console.log(d); // function d() {}
}
fn(1);
三部曲
- 语法分析,通篇扫描一遍,看看有没有语法错误,但不执行
- 预编译
- 解析执行,解释一行执行一行
function test(a,b) {
console.log(a); // 1
c = 0;
var c;
a = 3;
b = 2;
console.log(b); // 2
function b() {}
function d() {}
console.log(b); // 2
console.log(d); // function d() {}
}
test(1);
预编译四步走
- 创建AO对象(执行期上下文 )
AO{
}
- 找形参和变量声明,
AO找的是局部范围里的,GO找的是全局范围里的。将形参和变量名作为AO属性名,值为undefined
AO{
a : undefined,
b : undefined,
}
- 将实参值和形参相统一
AO{
a : 1,
b : undefined,
}
- 在函数体里面找函数声明,值赋予函数体
AO{
a : function a() {},
b : undefined,
d : function d() {},
}
栗子
function test(a,b) {
console.log(a); // function a() {}
console.log(b); // undefined
var b = 234;
console.log(b); // 234
a = 123;
console.log(a); // 123
function a() {}
var a;
var b = function() {}
console.log(a); // 123
console.log(b); // function() {}
}
test(1);
function test() {
var a = b = 123;
console.log(window.a); // undefined GO里面没有a定义
console.log(window.b); // 123
console.log(a); // 123
console.log(b); // 123 AO里面没有的话往上找GO
}
test();
-----------------------------------------------------------------
GO{
b : undefined,123
}
AO{
a : undefined,123
}
console.log(test); // function test() {} 和下面打印出来的fun指的不一样
function test(test) {
console.log(test); // function test() {}
var test = 234;
console.log(test); // 234
function test() {}
}
test(1);
-------------------------------------------
GO{
test : function test() {}
}
AO{
test : undefined,1,function test() {},234
}
var global = 100;
function fn() {
console.log(global); // 100 先去AO里面找,AO里面没有,再去GO上找
}
fn();
------------------------------------------------------------------
GO{
global : undefined,100
fn : function fn() {},
}
AO{
}
global = 100;
function fn() {
console.log(global); // undefined
global = 200; // 可以改变AO里对应属性的值
console.log(global); // 200
var global = 300;
}
fn();
console.log(global); // 100
var global;
----------------------------------------------------
GO{
global : undefined,100
fn : function fn() {}
}
AO{
global : undefined,200,300
}
function test() {
console.log(b); // undefined
if(a) { // a = undefined,所以b = 100不执行
var b = 100; // AO 提升
}
console.log(b); // undefined
c = 234;
console.log(c); // 234
}
var a;
test();
console.log(a); // undefined
a = 10;
console.log(a); // 10
console.log(c); // 234
-------------------------
GO{
a : undefined,10
c : 234,
test : function test() {}
}
AO{
b : undefined
}
a = 100;
function demo(e) {
function e() {}
arguments[0] = 2; // 实参列表,传参与传参的形参位相映射即 e = 2
console.log(e); // 2
if(a) { // a = undefined,所以if里面的语句不执行
var b = 123;
function c() {
}
}
var c;
a = 10;
var a;
console.log(b); // undefined
f = 123;
console.log(c); // 理想状态下是function,因为规定里刚刚新添if语句里面不能定义函数
console,log(a); // 10
}
var a;
demo(1);
console.log(a); // 100
console.log(f); // 123
----------------------------------------------------------------------
GO{
a : undefined,100
demo : function demo() {}
f : 123
}
AO{
e : undefined,1,function e() {},2
b : undefined
c : undefined,function c() {}
a : undefined,10
}
构造器
构造函数内部原理:
- 在函数体最前面隐式的加上
this = {} - 执行
this.xxx = xxx - 隐式返回
this
function Student(name, age, sex) {
// var this = {
// name : "",
// age : "",
// sex : "",
// };
this.name = name;
this.age = age;
this.sex = sex;
this.grade = 2017;
// return this;
}
console.log(new Student('sxw', 24, male).name);
包装类
new String() new Boolean() new Number()
数字123是原始值,原始值是不能有属性和方法的。属性和方法只有对象有,是对象独有的特性。
数字(字符串、布尔值)有原始值类型和对象类型,原始值类型数字是没有属性和方法的,但是对象类型有。
null、undefined没有包装类。
// 原始值为什么可以调用属性和方法呢?因为经历了包装类
var num = 4;
num.len = 3;
// new Number(4).len = 3; 然后delete
// 电脑会新建一个数字对象,让这个数字对象的len等于3,来弥补我们操作的不足,完成之后电脑又会自动去销毁;
// new Number(4).len 当我们下面要去访问的时候,电脑又会满足我们的要求,再去new一个新的number,然后把4放进去,再去访问len,因为一个对象是没有属性的,所以返回值为undefined;
console.log(num.len); // undefined
原型
- 对象查看原型:隐式属性
_proto_ - 对象查看构造函数:
constructor
一开始系统给我们生成原型的时候,就自带了一个属性叫构造器constructor,目的就是让构造函数构造出来的对象想找对应的构造函数时能够找到。就是有一天一个对象找不到是谁生了自己时,通过constructor属性,找到构造他的对应构造函数。
prototype是函数一定义就会有的,只不过我们没有给她定义的时候是一个空对象。
function Person() {
// var this = {
// _proto_ : Person.prototype;
// 当我们要找对象上面的属性时,如果对象上没有我们所要找的属性,就会通过_proto_指向的索引,到_proto_ 后面的值上去找有没有我们需要的属性。
// 把原型和自己通过_proto_连接在一起。
// _proto_后面存的是当前对象的原型,_proto_就是一个指向,但是我们可以改变这个指向,间接的去改变对象的原型;
// };
}
Person.prototype.name = 'sunny';
function Person() {
// var this = {_proto_ : Person.prototype}
}
var person = new Person();
Person.prototype = { // 把原型给修改了,相当于换了个新对象
name : 'sxw'
}
// 原型上的name是sunny
// 一个对象的原型只能是对象 或 Null
Object.create(原型); // 括号里原型处只能填 Object / null
Object.create(null); // 通过这样构造出来的对象没有原型,没有toString方法,但他是一个对象,这个对象没有原型;
对象是通过_proto_索引找到原型的。
Frand.prototype._proto_ = Object.prototype // 原型链的终端
Frand.prototype.lastName = "Deng";
function Frand() {
}
var grand = new Frand();
Father.prototype = grand;
function Father() {
this.name = 'xuming';
}
var father = new Father();
son.prototype = father;
function Son() {
this.hobbit = "smoke";
}
var son = new Son();
// 原型链上的原型只能通过自身去删除/修改属性,想要通过子孙后代去修改是实现不了的;
function Father() {
this.name = 'xuming';
this.fortune = {
card1 : 'visa'
};
this.age = 100;
// 当我们通过 son.age ++ 形式返回出来的时候, son.name 是101,原型上依旧是100,因为我们是先把原型的值取出来加等于1再赋给自身的;
}
var father = new Father();
Son.prototype = father;
function Son() {
this.hobbit = "smoke";
}
var son = new Son();
// 虽然通过后代无法改变原型链上的属性,但是可以通过给引用值加东西去达到修改的目的;
// son.fortune.name,这种是引用值自身的修改,这不算赋值的修改;
3.toString(),数字能够调用toString方法,因为数字经过包装类,一层层往上访问,包装类能够包装起来,就说明她是一个对象,对象就有原型链的终端。
标准库
Object对象
js中所有的对象都继承自Object对象,都是Object对象的实例。
Object对象的原生方法:
- 静态方法,直接定义在
Object对象上 - 实例方法,定义在
Object原型对象上,是Object.prototype上的属性和方法,被所有实例对象共享
// 栗子: 静态方法
Object.print = function(n) console.log(n)
// 栗子: 实例方法
Object.prototype.print = function() console.log(this);
Object构造函数
规则:
- 当
Object构造函数的参数是对象时,返回该对象 - 是原始类型值时,返回该值对应的包装对象,是该包装对象的一个实例
Object方法
Object.keys()
遍历对象(自身)key值,方法参数是一个对象,结果返回一个数组。
var obj = {
p1: 123,
p2: 456
};
Object.keys(obj) // ["p1", "p2"]
Object.getOwnPropertyNames()
与Object.keys类似,涉及不可枚举属性时存在区别:
Object.keys只返回可枚举的属性名Object.getOwnPropertyNames还能返回不可枚举的属性名
Object.prototype.valueof()
默认返回对象本身。
自动类型转换和对象与数字相加时,默认调用这个方法。可自定义。
Object.prototype.toString()
返回对象的字符串形式,默认返回类型字符串。
对象与字符串相加时,自动调用toString方法。
注意:数组、函数、Date对象调用toString方法,并不会返回[object Object],因为它们都自定义了toString方法,覆盖掉了原始方法。
Object.prototype.hasOwnProperty()
用以判断实例对象自身是否具有该属性。
常结合for...in遍历使用:
var obj = {
name : '123',
age : 23,
_proto_ : {
lastName : "deng"
}
}
for(var prop in obj) {
if(obj.hasOwnProperty(prop)) {
console.log(obj[prop]);
}
}
Object.prototype.isPrototypeOf()
判断当前对象是否为另一对象的原型。
Object.prototype.propertyIsEnumerable()
判断某个属性是否可枚举。
Object实例
Array
数组的所有方法都定义在了Array.prototype上。
Array.isArray()
var arr = [1, 2, 3];
typeof arr // "object"
Array.isArray(arr) // true
valueOf()
// 对对象求值
var arr = [1,2,3];
arr.valueOf() // [1,2,3]
toString()
// 返回数组的字符串形式。
var arr = [1, 2, 3, [4, 5, 6]];
arr.toString() // "1,2,3,4,5,6"
push() pop()
改变原数组,构成后进先出的栈结构。
var arr = []
arr.push(1) // 1,返回添加新元素后的数组长度
arr // [1]
arr.pop() // 1,删除数组中的最后一个元素并返回
arr // []
shift() unshift()
改变原数组。
var a = ['a', 'b', 'c'];
a.shift() // 'a',删除数组第一个元素并返回
a // ['b', 'c']
a.unshift('x') // 3,在数组第一个位置添加元素并返回新数组长度
a // ['x','b','c']
join()
以指定参数作为分隔符,将所有数组成员连接为一个字符串并返回。
若不提供参数,默认用逗号进行分隔。
var a = [1,2,3,4];
a.join(' ') // '1 2 3 4'
a.join(' | ') // "1 | 2 | 3 | 4"
a.join() // "1,2,3,4"
通过call将此方法应用于字符串或类数组。
Array.prototype.join.call('hello', '-')
// "h-e-l-l-o"
var obj = {0: 'a',1: 'b',length: 2};
Array.prototype.join.call(obj, '-')
// 'a-b'
concat()
返回新数组,原数组不变。
['hello'].concat(['world'])
// ["hello","world"]
浅拷贝:如果数组成员包含对象,concat方法返回当前数组的一个浅拷贝。
var oldArray = [{a: 1}];
var newArray = oldArray.concat();
reverse()
改变原数组。
var a = ['a', 'b', 'c'];
a.reverse() // ["c", "b", "a"]
slice()
提取目标数组中的一部分,返回新数组,原数组不变。
var a = ['a', 'b', 'c'];
a.slice(1, 2) // ["b"] - 正常情况
a.slice(1) // ["b", "c"] - 省略第二个参数情况,一直取到最后
a.slice(-2, -1) // ["b"] - 参数是负数,返回倒数计算位置
a.slice(4) // [] - 第一个参数大于数组长度
a.slice(2, 1) // [] - 第二个参数小于第一个参数
浅拷贝:
var a = ['a', 'b', 'c'];
a.slice() // ["a", "b", "c"]
类数组转为数组:
Array.prototype.slice.call({0: 'a',1: 'b',length: 2})
// ['a', 'b']
Array.prototype.slice.call(document.querySelectorAll("div"));
Array.prototype.slice.call(arguments);
splice()
删除原数组中一部分成员并返回,也可在删除的位置添加新数组成员,改变原数组。
arr.splice(start, count, addElement1, addElement2, ...);
var a = ['a', 'b', 'c', 'd', 'e', 'f'];
a.splice(4, 2, 1, 2) // ["e", "f"]
a // ["a", "b", "c", "d", 1, 2]
var a = [1, 1, 1];
a.splice(1, 0, 2) // []
a // [1, 2, 1, 1]
var a = [1, 2, 3, 4];
a.splice(2) // [3, 4],只传入一个参数等于将原数组在指定位置拆分成两个数组
a // [1, 2]
sort()
默认按字典顺序排序,排序后原数组改变。
['d','c','b','a'].sort() // ['a','b','c','d']
[4,3,2,1].sort() // [1,2,3,4]
[11,101].sort() // [101,11]
[10111,1101,111].sort(function(a, b) {
return a - b;
})
// [111, 1101, 10111]
[
{ name: "张三", age: 30 },
{ name: "李四", age: 24 },
{ name: "王五", age: 28 }
].sort(function(o1, o2) {
return o1.age - o2.age;
})
// [
// { name: "李四", age: 24 },
// { name: "王五", age: 28 },
// { name: "张三", age: 30 }
// ]
map()
适合为了得到返回值的情况。
将每次的执行结果组成一个新数组并返回,不改变原数组。
[1, 2, 3].map(function(elem, index, arr) {
return elem * index;
});
// [0, 2, 6]
var arr = ['a','b','c'];
[1, 2].map(function(e) {
return this[e];
}, arr) // 通过第二个参数,将回调函数内部的this指向arr数组
// ['b', 'c']
// map方法的回调函数会跳过空位不执行
var f = function(n) {return 'a'};
[1, , 2].map(f) // ["a", , "a"]
forEach
适合用来操作数据。
缺点:无法终止遍历,总是会遍历完所有成员,且也会跳过数组的空位。
var out = [];
[1, 2, 3].forEach(function(elem) {
this.push(elem * elem);
}, out);
out // [1, 4, 9]
filter()
满足条件的成员组成一个新数组并返回。
第一个参数是一个函数,所有数组成员依次执行该函数。若执行结果为true,则对应成员组成一个新数组并返回,不改变原数组。
[1, 2, 3, 4, 5].filter(function(elem) {
return (elem > 3);
})
// [4, 5]
[1, 2, 3, 4, 5].filter(function(elem, index, arr) {
return index % 2 === 0;
});
// [1, 3, 5]
var obj = {MAX: 3};
var arr = [2, 8, 3, 4, 1, 3, 2, 9];
arr.filter(function(item) {
if(item > this.MAX) return true
},obj) // [8,4,9],第二个参数用来绑定回调函数内部的this变量
some() every()
返回布尔值,用以表示判断数组成员是否符合某种条件。
var arr = [1, 2, 3, 4, 5];
arr.some(function(elem, index, arr) {
return elem >= 3;
}); // true,只要有一个回调返回true,结果就是true
var arr = [1, 2, 3, 4, 5];
arr.every(function(elem, index, arr) {
return elem >= 3;
}); // false,必须全部结果都返回true,整体才返回true
reduce() reduceRight()
依次处理数组的每个成员,最终累计为一个值。
[1, 2, 3, 4, 5].reduce(function(a, b) {
console.log(a, b);
return a + b;
})
// 1 2
// 3 3
// 6 4
// 10 5
// 最后结果: 15
[1, 2, 3, 4, 5].reduce(function(a, b) {
return a + b;
}, 10); // 指定累计初值为10,即数组从10为基础开始累加
// 25
function findLongest(entries) {
return entries.reduce(function(longest, entry) {
return entry.length > longest.length ? entry : longest;
}, '');
}
findLongest(['aaa', 'bb', 'c']) // "aaa"
indexOf() lastIndexOf()
var a = ['a', 'b', 'c'];
a.indexOf('b') // 1
a.indexOf('y') // -1
['a', 'b', 'c'].indexOf('a', 1)
// -1
上述数组方法中,有不少返回的还是数组,可以采用链式调用。
Number对象
是数值对应的包装对象,可作为构造函数和工具函数使用。
Number.prototype.toString()
Number对象部署了自己的toString方法,用来将一个数值转为字符串形式。
Number.prototype.toFixed()
10.005.toFixed(2) // "10.01"
String对象
字符串对象是一个类似数组的对象。
String.prototype.length
'abc'.length // 3
String.prototype.charAt()
返回指定位置的字符。
var s = new String('abc');
s.charAt(1) // "b"
String.prototype.concat()
返回新字符串,不改变原字符串。
'a'.concat('b', 'c') // "abc"
String.prototype.slice()
不改变原字符串。
'JavaScript'.slice(0, 4) // "Java"
// 如果省略第二个参数,则表示子字符串一直到原字符串结束
'JavaScript'.slice(4) // "Script"
// 如果参数是负值,表示从结尾开始倒数计算的位置
'JavaScript'.slice(-6) // "Script"
'JavaScript'.slice(0, -6) // "Java"
'JavaScript'.slice(-2, -1) // "p"
// 如果第一个参数大于第二个参数,返回一个空字符串
'JavaScript'.slice(2, 1) // ""
/pinglun?name=sxw&msg=hello
split('?') ==> name=sxw&msg=hello
split('&') ==> name=sxw msg=hello
forEach()
name=sxw.split('=')
0 key
1 value
String.prototype.substring()
不改变原字符串。
'JavaScript'.substring(0, 4) // "Java"
// 如果省略第二个参数,则表示子字符串一直到原字符串的结束;
'JavaScript'.substring(4) // "Script"
// 如果第一个参数大于第二个参数,substring会自动更换两个参数的位置;
'JavaScript'.substring(10, 4) // "Script"
// 如果参数是负数,会自动将负数转为0;
'Javascript'.substring(-3) // "JavaScript"
'JavaScript'.substring(4, -3) // "Java"
String.prototype.substr()
// 第二个参数是字符串的长度
'JavaScript'.substr(4, 6) // "Script"
// 如果省略第二个参数,则表示子字符串一直到原字符串的结束;
'JavaScript'.substr(4) // "Script"
// 如果第一个参数是负数,表示倒数计算的字符位置;
// 如果第二个参数是负数,将被自动转为0,返回空字符串;
'JavaScript'.substr(-6) // "Script"
'JavaScript'.substr(4, -1) // ""
String.prototype.indexOf() String.prototype.lastIndexOf()
'hello world'.indexOf('o') // 4
'JavaScript'.indexOf('script') // -1
'hello world'.indexOf('o', 6) // 7
String.prototype.trim()
去除字符串两端的空格(制表符、换行符、回车符等),并返回一个新字符串,不改变原字符串。
' hello world '.trim()
// "hello world"
String.prototype.toLowerCase() String.prototype.toUpperCase()
'Hello World'.toLowerCase()
// "hello world"
'Hello World'.toUpperCase()
// "HELLO WORLD"
String.prototype.match()
确定原字符串是否匹配某个子字符串,返回一个数组,成员为匹配的第一个字符串。若没有找到匹配,返回null。
'cat, bat, sat, fat'.match('at') // ["at"]
'cat, bat, sat, fat'.match('xt') // null
String.prototype.search() String.prototype.replace()
同于match,但是返回值为匹配的第一个位置。
没有匹配,返回 -1 。
'cat, bat, sat, fat'.search('at') // 1
replace 用于替换匹配到的子字符串,一般只替换第一个匹配到的,除非使用带g修饰符的正则。
'aaa'.replace('a', 'b') // "baa"
'aaa'.replace(/a/, 'b') // "baa"
'aaa'.replace(/a/g, 'b') // "bbb"
// 消除字符串首尾两端的空格
var str = ' #id div.class ';
str.replace(/^\s+|\s+$/g, '')
// "#id div.class"
String.prototype.split()
按照给定规则分割字符串,返回一个由分割出来的子字符串组成的数组。
'a|b|c'.split('|') // ["a", "b", "c"]
// 如果分割规则为空字符串,则返回数组的成员是原字符串的每一个字符;
'a|b|c'.split('') // ["a", "|", "b", "|", "c"]
// 如果省略参数,则返回数组的唯一成员就是原字符串;
'a|b|c'.split() // ["a|b|c"]
// 如果满足分割规则的两个部分紧邻着,即两个分割符中间没有其他字符,则返回的数组之中会有一个空字符串;
'a||c'.split('|') // ['a', '', 'c']
// 如果满足分割规则的部分处于字符串的开头或结尾,则返回数组的第一个或最后一个成员是一个空字符串;
'|b|c'.split('|') // ["", "b", "c"]
'a|b|'.split('|') // ["a", "b", ""]
padStart、padEnd
字符串补全长度,可在头部或尾部进行补全。
'x'.padStart(5, 'ab') // 'ababx'
'x'.padStart(4, 'ab') // 'abax'
'x'.padEnd(5, 'ab') // 'xabab'
'x'.padEnd(4, 'ab') // 'xaba'
'abc'.padStart(10, '0123456789') //'0123456abc'
'x'.padStart(4) // ' x'
'x'.padEnd(4) // 'x '
'12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12"
'09-12'.padStart(10, 'YYYY-MM-DD') // "YYYY-09-12"
Boolean对象
Boolean(null) // false
Boolean(undefined) // false
Boolean(false) // false
Boolean(0) // false
Boolean('') // false
Boolean(NaN) // false
// 使用双重否运算符`!`也可将任意值转为对应的布尔值
!!null // false
!!undefined // false
!!0 // false
!!'' // false
!!NaN // false
原型链方法
Object.create(); // 指定原型对象和属性,返回新对象
Object.getPrototypeOf(); // 获取对象的 Prototype 对象
标准库
Math对象
Math.abs()
Math.abs(-1) // 1
Math.ceil()
Math.ceil(3.2) // 4
Math.ceil(-3.2) // -3
Math.floor()
Math.floor(3.2) // 3
Math.floor(-3.2) // -4
Math.max() Math.min()
Math.max(2, -1, 5) // 5
Math.min(2, -1, 5) // -1
Math.round()
Math.round(0.1) // 0
Math.round(0.5) // 1
Math.round(-1.1) // -1
Math.round(-1.5) // -1,注意
Math.round(-1.6) // -2
Math.random()
随机返回[0,1)之间的一个数。
Date对象
以1970年1月1日00:00:00作为时间的零点。
Date.now()
返回当前时间距离时间零点(1970年1月1日 00:00:00 UTC)的毫秒数。
Date.now() // 1364026285194
Date.parse()
解析日期字符串,返回该时间距离时间零点(1970年1月1日 00:00:00)的毫秒数。
Date.parse('xxx') // NaN,解析失败,返回NaN
Date.prototype.valueOf()
返回实例对象距离时间零点(1970年1月1日00:00:00 UTC)对应的毫秒数,该方法等同于getTime方法。
var d = new Date();
d.valueOf() // 1362790014817
d.getTime() // 1362790014817
getTime()
返回实例距离1970年1月1日00:00:00的毫秒数,等同于valueOf方法。
getDate()
返回实例对象对应每个月的几号(从1开始)。
getDay()
返回星期几,星期日为 0,星期一为 1,以此类推。
getYear()
返回距离1900的年数。
getFullYear()
返回四位的年份。
getMonth()
返回月份(0表示1月,11表示12月)。
getHours()
返回小时(0 - 23)。
getMilliseconds()
返回毫秒(0 - 999)。
getMinutes()
返回分钟(0 - 59)。
getSeconds()
返回秒(0 - 59)。
var d = new Date('January 6, 2019');
d.getDate() // 6
d.getMonth() // 0
d.getYear() // 113
d.getFullYear() // 2019
RegExp对象
略
JSON对象
JSON.stringify()
将一个值转为JSON字符串,该字符串符合JSON格式,并且可以被JSON.parse方法还原。
JSON.parse()
将JSON字符串转换成对应的值。
console.log对象
开发者工具顶端有多个面板:
Elements:查看网页的HTML源码和CSS代码Resources:查看网页加载的各种资源文件(比如代码文件、字体文件CSS文件等),以及在硬盘上创建的各种内容(比如本地缓存、Cookie、Local Storage等)Network:查看网页的HTTP通信情况Sources:查看网页加载的脚本源码Timeline:查看各种网页行为随时间变化的情况Performance:查看网页的性能情况,比如 CPU 和内存消耗Console:用来运行JavaScript命令
debugger语句
设置断点。
如果有正在运行的除错工具,程序运行到debugger语句时会自动停下;如果没有除错工具,debugger语句不会产生任何结果,JavaScript引擎自动跳过这一句。
Chrome浏览器中,当代码运行到debugger语句时,会暂停运行,自动打开脚本源码界面。
for(var i = 0; i < 5; i++){
console.log(i);
if (i === 2) debugger;
}
// 上面代码打印出0,1,2以后,就会暂停,自动打开源码界面,等待进一步处理;
属性描述对象
{
value: 123, // 该属性的属性值,默认为 undefined
writable: false, // 是一个布尔值,表示属性值是否可写,默认为 true
enumerable: true, // 是一个布尔值,表示该属性是否可遍历,默认为true;如果设为false,会使得某些操作,如: for...in循环、Object.keys()...跳过该属性;
configurable: false, // 是一个布尔值,表示可配置性,默认为true;如果设为false,将阻止某些操作改写该属性,比如无法删除该属性,也不得改变该属性的属性描述对象,控制了属性描述对象的可写性;
get: undefined, // 是一个函数,表示该属性的取值函数,默认为 undefined;
set: undefined // 是一个函数,表示该属性的存值函数,默认为undefined;
}
元属性
value
var obj = {};
obj.p = 123;
Object.getOwnPropertyDescriptor(obj, 'p').value
// 123
Object.defineProperty(obj, 'p', { value: 246 });
obj.p // 246
// 上面代码是通过value属性,读取或改写obj.p;
writable
目标属性的值是否可以被改变。
var obj = {};
Object.defineProperty(obj, 'a', {
value: 37,
writable: false
});
obj.a // 37
obj.a = 25;
obj.a // 37
enumerable
目标属性是否可遍历。
如果一个属性的enumerable为false,下面三个操作不会取到该属性:
for..in循环Object.keys方法JSON.stringify方法
configurable
是否可以修改属性描述对象。
configurable为false时,value、writable、enumerable和configurable都不能被修改。
存取器
存值函数称为setter,使用属性描述对象的set属性;取值函数称为getter,使用属性描述对象的get属性。
var obj = Object.defineProperty({}, 'p', {
get: function () {
return 'getter';
},
set: function (value) {
console.log('setter: ' + value);
}
});
obj.p // "getter"
obj.p = 123 // "setter: 123"
var obj = {
get p() {
return 'getter';
},
set p(value) {
console.log('setter: ' + value);
}
};
// 取值函数get不能接受参数,存值函数set只能接受一个参数,属性的值;
控制对象状态
有时需要冻结对象的读写状态,防止对象被改变。
JavaScript提供了三种冻结方法(效果由弱到强):
Object.preventExtensionsObject.sealObject.freeze
var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach((key,i) => {
if(typeof obj[key] === 'object') {
constantize(obj{key});
}
})
}
面向对象
面向对象编程具有灵活、代码可复用、高度模块化等特点,容易维护和开发。
比起由一系列函数或指令组成的传统的过程式编程,更适合多人合作的大型软件项目。
对象是一个容器,封装了属性和方法。
属性是对象的状态,方法是对象的行为。
类就是对象的模板,对象就是类的实例。
js使用构造函数,作为对象的模板。
构造函数的两个特点:
- 函数体内部使用了
this关键字,代表了所要生成的对象实例 - 生成对象的时候,必须使用
new命令
new
执行构造函数,返回一个实例对象。
// 防止不使用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语句后面没有跟对象,则会忽略return语句,返回this对象
var Vehicle = function () {
this.price = 1000;
return 1000; // 被忽略
};
(new Vehicle()) === 1000
// false
var Vehicle = function (){
this.price = 1000;
return { price: 2000 }; // return后面对象是无关this的,new命令会返回这个对象,忽略this对象
};
(new Vehicle()).price
// 2000
// 原理
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);
Object.create()创建实例对象
构造函数作为模板,可以生成实例对象。但有时拿不到构造函数,只能拿到一个现有的对象。我们希望以这个现有对象作为模板,生成新的实例对象。
// `person1`是`person2`的模板,后者继承了前者的属性和方法。
var person1 = {
name: '张三',
age: 38,
greeting: function() {
console.log('Hi! I\'m ' + this.name + '.');
}
};
var person2 = Object.create(person1);
person2.name // 张三
person2.greeting() // Hi! I'm 张三.
this
构造函数中的this总是返回一个对象,this就是属性或方法当前所在的对象。
避免多次使用this
var o = {
f1: function() {
console.log(this);
var that = this;
var f2 = function() {
console.log(that);
}();
}
}
o.f1()
// Object
// Object
上面代码定义了变量that,固定指向外层的this,然后在内层使用that,就不会发生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
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
call
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
apply
function f(x, y){
console.log(x + y);
}
f.call(null, 1, 1) // 2,第一个参数是null就是将全局对象传递进来
f.apply(null, [1, 1]) // 2
var a = [10, 2, 4, 15, 9];
Math.max.apply(null, a) // 15
var a = ['a', , 'b'];
function print(i) {
console.log(i);
}
a.forEach(print)
// a
// b
Array.apply(null, a).forEach(print)
// a
// undefined
// b
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]
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`,否则就会出错。
var counter = {
count: 0,
inc: function () {
this.count++;
}
};
var obj = {
count: 100
};
var func = counter.inc.bind(obj);
func();
obj.count // 101
// 上面代码中,`bind`方法将`inc`方法内部的`this`绑定到`obj`对象,结果调用`func`函数以后,递增的就是`obj`内部的`count`属性。
注意点:
- 每次返回一个新函数
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);
- 结合回调函数使用
var counter = {
count: 0,
inc: function () {
'use strict';
this.count++;
}
};
function callIt(callback) {
callback();
}
callIt(counter.inc.bind(counter));
counter.count // 1
上面代码中,callIt方法会调用回调函数,这时如果直接把counter.inc传入,调用时counter.inc内部的this就会指向全局对象。使用bind方法将counter.inc绑定counter后,就不会有这个问题,this总是指向counter。
var obj = {
name: '张三',
times: [1, 2, 3],
print: function () {
this.times.forEach(function (n) {
// `forEach`方法的回调函数内部的`this.name`却是指向全局对象,导致没有办法取到值。
console.log(this === window);
console.log(this.name);
});
}
};
obj.print() // 没有任何输出
// 解决
obj.print = function () {
this.times.forEach(function (n) {
console.log(this.name);
}.bind(this));
};
obj.print()
// 张三
// 张三
// 张三
- 结合
call方法使用
[1, 2, 3].slice(0, 1) // [1]
Array.prototype.slice.call([1, 2, 3], 0, 1) // [1]
var slice = Function.prototype.call.bind(Array.prototype.slice);
slice([1, 2, 3], 0, 1) // [1]
// 将`Array.prototype.slice`变成`Function.prototype.call`方法所在的对象,调用时就变成了`Array.prototype.slice.call`,类似的写法还可以用于其他数组方法。
prototype对象
通过构造函数为实例对象定义属性虽然很方便,但有一个缺点:同一个构造函数的多个实例之间,无法共享属性,从而造成对系统资源的浪费。
function Cat(name, color) {
this.name = name;
this.color = color;
this.meow = function () {
console.log('喵喵');
};
}
// meow方法随着实例的生成生成多次,造成资源浪费,没有共享
var cat1 = new Cat('大毛', '白色');
var cat2 = new Cat('二毛', '黑色');
cat1.meow === cat2.meow
// false
作用
js继承机制的设计思想:原型对象的所有属性和方法,都能被实例对象共享。如果属性和方法定义在原型上,那么所有实例对象就能共享,不仅节省了内存,还体现了实例对象之间的联系。
JavaScript规定:每个函数都有一个prototype属性,指向一个对象。
function f() {}
typeof f.prototype // "object"
原型链
所有对象都有自己的原型对象prototype。
任何一个对象,都可以充当其他对象的原型。
原型对象也是对象,也有自己的原型。因此,就会形成一个原型链:对象到原型,再到原型的原型……
如果一层层地上溯,所有对象的原型最终都可以上溯到Object.prototype,即Object构造函数的prototype属性。
Object.prototype对象的原型是null,null没有任何属性和方法,也没有自己的原型。
因此,原型链的尽头就是null。
注意:一级级向上在整个原型链上寻找某个属性,对性能是有影响的,所寻找的属性在越上层的原型对象,对性能的影响越大。如果寻找某个不存在的属性,将会遍历整个原型链。
var MyArray = function () {};
MyArray.prototype = new Array(); // prototype 指向数组的一个实例便可以调用数组方法
MyArray.prototype.constructor = MyArray;
var mine = new MyArray();
mine.push(1, 2, 3);
mine.length // 3
mine instanceof Array // true
constructor属性
prototype对象有一个constructor属性,默认指向prototype对象所在的构造函数。
constructor属性的作用:得知某个实例对象到底是哪一个构造函数产生的。
注意:修改原型对象时,要同时修改constructor属性的指向:
// 好的写法
C.prototype = {
constructor: C,
method1: function (...) { ... },
// ...
};
// 更好的写法
C.prototype.method1 = function (...) { ... };
上面代码中,要么将constructor属性重新指向原来的构造函数,要么只在原型对象上添加方法,这样可以保证instanceof运算符不会失真。
// 如果不能确定`constructor`属性是什么函数,可通过`name`属性,从实例得到构造函数的名称。
function Foo() {}
var f = new Foo();
f.constructor.name // "Foo"
instanceof运算符
返回一个布尔值,表示对象是否为某个构造函数的实例。
var v = new Vehicle();
v instanceof Vehicle // true
instanceof运算符的左边是实例对象,右边是构造函数。它会检查右边构建函数的原型对象(prototype),是否在左边对象的原型链上。
由于instanceof检查整个原型链,因此同一个实例对象,可能会对多个构造函数都返回true。
特例:左边对象的原型链上,只有null对象,这时instanceof判断会失真。
var obj = Object.create(null);
typeof obj // "object"
Object.create(null) instanceof Object // false
上面代码中,Object.create(null)返回一个新对象obj的原型是null,右边的构造函数Object的prototype属性不在左边的原型链上,因此instanceof就认为obj不是Object的实例,但是只要一个对象的原型不是null,instanceof运算符的判断就不会失真。
用途:判断值的类型(只能用于对象,不适用原始类型的值。对于undefined和null,instanceOf总是返回false)。
// 解决调用构造函数时候没有加new的情况
function Fubar (foo, bar) {
if (this instanceof Fubar) {
this._foo = foo;
this._bar = bar;
} else {
return new Fubar(foo, bar);
}
}
对象与继承
Object.getPrototypeOf()
返回参数对象的原型。
var F = function () {};
var f = new F();
Object.getPrototypeOf(f) === F.prototype // true,实例对象f的原型是F.prototype
几种特殊对象的原型:
// 空对象的原型是 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.create()
生成实例对象的常用方法是:使用new命令让构造函数返回一个实例。
从一个实例对象,生成另一个实例对象。该方法接受一个对象作为参数,然后以它为原型,返回一个实例对象,该实例完全继承原型对象的属性。
// 原型对象
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方法以A对象为原型,生成了B对象,B继承了A的所有属性和方法。
// 想要生成一个不继承任何属性(比如没有`toString`和`valueOf`方法)的对象,可以将`Object.create`的参数设为`null`。
var obj = Object.create(null);
obj.valueOf()
// TypeError: Object [object Object] has no method 'valueOf'
上面代码,对象obj的原型是null,就不具备一些定义在Object.prototype对象上面的属性。
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';
function A() {}
var a = new A();
var b = Object.create(a);
b.constructor === A // true
b instanceof A // true
上面代码,b对象的原型是a对象,因此继承了a对象的构造函数A。
Object.prototype.isPrototypeOf()
判断该对象是否为参数对象的原型。
var o1 = {};
var o2 = Object.create(o1);
var o3 = Object.create(o2);
o2.isPrototypeOf(o3) // true
o1.isPrototypeOf(o3) // true
Object.prototype._proto__
实例对象的__proto__属性(前后各两个下划线),返回该对象的原型,该属性可读写。
var obj = {};
var p = {};
obj.__proto__ = p;
Object.getPrototypeOf(obj) === p // true
上面代码通过__proto__属性,将p对象设为obj对象的原型。
__proto__属性只有浏览器才需要部署,其他环境可以没有这个属性。
它前后的两根下划线,表明它本质是一个内部属性,不应该对使用者暴露。
因此,尽量少用这个属性,而是用Object.getPrototypeof()和Object.setPrototypeOf(),进行原型对象的读写操作。
var A = {
name: '张三'
};
var B = {
name: '李四'
};
var proto = {
print: function () {
console.log(this.name);
}
};
A.__proto__ = proto;
B.__proto__ = proto;
A.print() // 张三
B.print() // 李四
A.print === B.print // true
A.print === proto.print // true
B.print === proto.print // true
获取原型对象方法:
__proto__属性指向当前对象的原型对象,即构造函数的prototype属性。
var obj = new Object();
obj.__proto__ === Object.prototype // true
obj.__proto__ === obj.constructor.prototype // true
上述代码首先新建了一个对象obj,它的__proto__属性,指向构造函数(Object或obj.constructor)的prototype属性。
总结,获取实例对象obj的原型对象,有三种方法:
// 只有浏览器才需要部署,其他环境可以不部署
obj.__proto__
// 在手动改变原型对象时,可能会失效
obj.constructor.prototype
Object.getPrototypeOf(obj) [推荐]
var P = function () {};
var p = new P();
var C = function () {};
C.prototype = p;
C.prototype.constructor = C;
var c = new C();
c.constructor.prototype === p // true
对象的拷贝
注意两点:
- 确保拷贝后的对象,与原对象具有同样的原型
- 确保拷贝后的对象,与原对象具有同样的实例属性
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;
}
// 另一种写法
function copyObject(orig) {
return Object.create(
Object.getPrototypeOf(orig),
Object.getOwnPropertyDescriptors(orig)
);
}
编程模式
构造函数的继承
让一个构造函数继承另一个构造函数,可分为两步:
- 在子类的构造函数中,调用父类的构造函数
- 让子类的原型指向父类的原型,这样子类就可以继承父类原型
function Sub(value) {
Super.call(this);
this.prop = value;
}
上面代码,Sub是子类的构造函数,this是子类的实例。
在实例上调用父类的构造函数Super,就会让子类实例具有父类实例的属性。
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
Sub.prototype.method = '...';
上面代码,Sub.prototype是子类的原型,要将它赋值为Object.create(Super.prototype),而不是直接等于Super.prototype。否则后面两行对Sub.prototype的操作,会连父类的原型Super.prototype一起修改掉。
// 另一种写法是`Sub.prototype`等于一个父类实例【不推荐】:
Sub.prototype = new Super();
// 上面写法也有继承的效果,但是子类会具有父类实例的方法。
function Shape() {
this.x = 0;
this.y = 0;
}
Shape.prototype.move = function (x, y) {
this.x += x;
this.y += y;
console.info('Shape moved.');
};
// 让`Rectangle`构造函数继承`Shape`
// 第一步,子类继承父类的实例
function Rectangle() {
Shape.call(this); // 调用父类构造函数
}
// 另一种写法
function Rectangle() {
this.base = Shape;
this.base();
}
// 第二步,子类继承父类的原型
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;
采用这样的写法后,instanceof运算符会对子类和父类的构造函数,都返回true。
// 子类整体继承父类
var rect = new Rectangle();
rect.move(1, 1) // 'Shape moved.'
rect instanceof Rectangle // true
rect instanceof Shape // true
// 有时只需要单个方法的继承,这时可以采用下面的写法。
ClassB.prototype.print = function() {
ClassA.prototype.print.call(this);
// some code
}
模块
// 把模块写成一个对象,所有的模块成员都放到这个对象里面
var module1 = new Object({
 _count : 0,
 m1 : function (){
  //...
 },
 m2 : function (){
 //...
 }
});
module1.m1();
封装私有变量 - 构造函数
function StringBuilder() {
var buffer = [];
this.add = function (str) {
buffer.push(str);
};
this.toString = function () {
return buffer.join('');
};
}
封装私有变量 - IIFE
var module1 = (function () {
var _count = 0;
var m1 = function () {
//...
};
var m2 = function () {
//...
};
return {
m1 : m1,
m2 : m2
};
})();
模块化的方法输入
(function($, window, document) {
function go(num) {
}
function handleEvents() {
}
function initialize() {
}
function dieCarouselDie() {
}
//attach to the global scope
window.finalCarousel = {
init : initialize,
destroy : dieCouraselDie
}
})( jQuery, window, document );
上面代码中,finalCarousel对象输出到全局,对外暴露init和destroy接口,内部方法go、handleEvents、initialize、dieCarouselDie都是外部无法调用的。
异步
单线程模型
js只在一个线程上运行,同时只能执行一个任务,其他任务都必须在后面排队等待。
js之所以采用单线程,而不是多线程,跟历史有关系。
js从诞生起就是单线程,原因是不想让浏览器变得太复杂,因为多线程需要共享资源、且有可能修改彼此的运行结果,对于一种网页脚本语言来说,这就太复杂了。
-
好处:实现起来比较简单,执行环境相对单纯
-
坏处:只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行
常见的浏览器无响应,往往就是因为某一段js代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。
js语言的设计者意识到,这时 CPU 完全可以不管 IO 操作,挂起处于等待中的任务,先运行排在后面的任务。等到 IO 操作返回了结果,再回过头,把挂起的任务继续执行下去。这种机制就是 js内部采用的事件循环机制。
如果用得好,js程序是不会出现堵塞的,这就是为什么Node可以用很少的资源,应付大流量访问的原因。
同步任务和异步任务
同步任务:那些没有被引擎挂起、在主线程上排队执行的任务。只有前一个任务执行完毕,才能执行后一个任务。
异步任务:那些被引擎放在一边,不进入主线程、而进入任务队列的任务。只有引擎认为某个异步任务可以执行了(比如 Ajax 操作从服务器得到了结果),该任务(采用回调函数的形式)才会进入主线程执行。排在异步任务后面的代码,不用等待异步任务结束会马上运行,也就是说,异步任务不具有堵塞效应。
举例来说,Ajax 操作可以当作同步任务处理,也可以当作异步任务处理,由开发者决定。
- 同步任务,主线程就等着 Ajax 操作返回结果,再往下执行
- 异步任务,主线程在发出 Ajax 请求以后,就直接往下执行,等到 Ajax 操作有了结果,主线程再执行对应的回调函数
任务队列和事件循环
js运行时,除了一个正在运行的主线程,引擎还提供一个任务队列,里面是各种需要当前程序处理的异步任务。
- 首先,主线程会去执行所有的同步任务。等到同步任务全部执行完,就会去看任务队列里面的异步任务
- 如果满足条件,那么异步任务就重新进入主线程开始执行,这时它就变成同步任务了。等到执行完,下一个异步任务再进入主线程开始执行
- 一旦任务队列清空,程序就结束执行
- 异步任务的写法通常是回调函数
- 一旦异步任务重新进入主线程,就会执行对应的回调函数。如果一个异步任务没有回调函数,就不会进入任务队列,也就是说,不会重新进入主线程,因为没有用回调函数指定下一步的操作
- 异步任务有没有结果,能不能进入主线程,是引擎在不停地检查,一遍又一遍,只要同步任务执行完了,引擎就会去检查那些挂起来的异步任务,是不是可以进入主线程了。这种循环检查的机制,就叫做事件循环
js加载模式
阻塞加载(同步加载)
默认。
阻塞加载会阻止浏览器的后续处理,停止了后续文件的解析执行。
defer延迟加载
脚本可以延迟到文档完全被解析和显示之后再执行,加载脚本顺序一定。
<script defer src="1.js"></script>
<script defer="defer" src="1.js"></script>
async异步加载
立即下载脚本,但不妨碍页面中的其他操作,加载脚本顺序不一定。
<script async src="1.js"></script>
<script async="async" src="1.js"></script>
异步加载的三种方式:
asyncawaitonload:在window.onload方法里执行函数,这样就解决了阻塞onload事件触发的问题
异步操作的模式
回调函数
// f2函数必须等到f1执行完成之后执行
// 把f2写成f1的回调函数
function f1(callback) {
// ...
callback();
}
function f2() {
// ...
}
f1(f2);
-
优点:简单、容易理解和实现
-
缺点:不利于代码的阅读和维护,各个部分之间高度耦合,使得程序结构混乱、流程难以追踪,尤其是多个回调函数嵌套的情况,而且每个任务只能指定一个回调函数
事件监听
采用事件驱动模式。
异步任务的执行不取决于代码的顺序,而取决于某个事件是否发生。
// 当`f1`发生`done`事件,就执行`f2`
f1.on('done', f2);
function f1() {
setTimeout(function () {
// 执行完成后,立即触发`done`事件,从而开始执行`f2`
f1.trigger('done');
}, 1000);
}
- 优点:比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以去耦合,有利于实现模块化
- 缺点:整个程序都要变成事件驱动型,运行流程会变得很不清晰。阅读代码的时候,很难看出主流程
发布/订阅
事件完全可以理解成信号,如果存在一个信号中心,某个任务执行完成,就向信号中心发布一个信号,其他任务可以向信号中心订阅这个信号,从而知道什么时候自己可以开始执行。这就叫做发布/订阅模式,又称观察者模式。
// 首先,`f2`向信号中心`jQuery`订阅`done`信号
jQuery.subscribe('done', f2);
// f1
function f1() {
setTimeout(function () {
// `f1`执行完成后,向信号中心`jQuery`发布`done`信号,从而引发`f2`的执行
jQuery.publish('done');
}, 1000);
}
// `f2`完成执行后,可以取消订阅
jQuery.unsubscribe('done', f2);
总结:这种方法的性质与事件监听类似,但是明显优于后者。因为可以通过查看消息中心,了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行。
异步操作的流程控制
如何确定异步操作执行的顺序,以及如何保证遵守这种顺序。
串行执行
我们可以编写一个流程控制函数,让它来控制异步任务,一个任务完成以后,再执行另一个。这就叫串行执行。
var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];
function async(arg, callback) {
console.log('参数为 ' + arg +' , 1秒后返回结果');
setTimeout(function () { callback(arg * 2); }, 1000);
}
function final(value) {
console.log('完成: ', value);
}
function series(item) {
if(item) {
async( item, function(result) {
results.push(result);
return series(items.shift());
});
} else {
return final(results[results.length - 1]);
}
}
series(items.shift());
总结:上面代码中,函数series就是串行函数,它会依次执行异步任务,所有任务都完成后,才会执行final函数。items数组保存每一个异步任务的参数,results数组保存每一个异步任务的运行结果。
注意,上面的写法需要六秒,才能完成整个脚本。
并行执行
所有异步任务同时执行,等到全部完成以后,才执行final函数。
var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];
function async(arg, callback) {
console.log('参数为 ' + arg +' , 1秒后返回结果');
setTimeout(function () { callback(arg * 2); }, 1000);
}
function final(value) {
console.log('完成: ', value);
}
items.forEach(function(item) {
async(item, function(result){
results.push(result);
if(results.length === items.length) {
final(results[results.length - 1]);
}
})
});
总结:上面代码中,forEach方法会同时发起六个异步任务,等到它们全部完成以后,才会执行final函数。相比而言,上面的写法只要一秒,就能完成整个脚本。
这就是说,并行执行的效率较高,比起串行执行一次只能执行一个任务,较为节约时间。
但是问题在于如果并行的任务较多,很容易耗尽系统资源,拖慢运行速度。因此有了第三种流程控制方式。
并行与串行的结合
设置一个门槛,每次最多只能并行执行n个异步任务,这样就避免了过分占用系统资源。
var items = [ 1, 2, 3, 4, 5, 6 ];
var results = [];
var running = 0;
var limit = 2;
function async(arg, callback) {
console.log('参数为 ' + arg +' , 1秒后返回结果');
setTimeout(function () { callback(arg * 2); }, 1000);
}
function final(value) {
console.log('完成: ', value);
}
function launcher() {
while(running < limit && items.length > 0) {
var item = items.shift();
async(item, function(result) {
results.push(result);
running--;
if(items.length > 0) {
launcher();
} else if(running == 0) {
final(results);
}
});
running++;
}
}
launcher();
总结:上面代码中,最多只能同时运行两个异步任务。变量running记录当前正在运行的任务数,只要低于门槛值,就再启动一个新的任务,如果等于0,就表示所有任务都执行完了,这时就执行final函数。
这段代码需要三秒完成整个脚本,处在串行执行和并行执行之间。通过调节limit变量,达到效率和资源的最佳平衡。
定时器
setTimeout()
它返回一个整数,表示定时器的编号,以后可以用来取消这个定时器。
console.log(1);
setTimeout('console.log(2)',1000);
console.log(3);
// 1
// 3
// 2
需要注意的是:如果回调函数是对象的方法,那么setTimeout使得方法内部的this关键字指向全局环境,而不是定义时所在的那个对象。
var x = 1;
var obj = {
x: 2,
y: function () {
console.log(this.x);
}
};
setTimeout(obj.y, 1000) // 1
// 解决方式1:
var x = 1;
var obj = {
x: 2,
y: function () {
console.log(this.x);
}
};
setTimeout(function () {
obj.y();
}, 1000);
// 2
// 解决方式2:
var x = 1;
var obj = {
x: 2,
y: function () {
console.log(this.x);
}
};
setTimeout(obj.y.bind(obj), 1000)
// 2
setInterval()
无限次的定时执行。
var timer = setInterval(function() {
console.log(2);
}, 1000)
var div = document.getElementById('someDiv');
var opacity = 1;
var fader = setInterval(function() {
opacity -= 0.1;
if (opacity >= 0) {
div.style.opacity = opacity;
} else {
clearInterval(fader);
}
}, 100);
setInterval指定的是开始执行之间的间隔,并不考虑每次任务执行本身所消耗的时间。因此实际上,两次执行之间的间隔会小于指定的时间。比如,setInterval指定每 100ms 执行一次,每次执行需要 5ms,那么第一次执行结束后95毫秒,第二次执行就会开始。
如果某次执行耗时特别长,比如需要105毫秒,那么它结束后,下一次执行就会立即开始。为了确保两次执行之间有固定的间隔,可以不用setInterval,而是每次执行结束后,使用setTimeout指定下一次执行的具体时间。
var i = 1;
var timer = setTimeout(function f() {
// 保证下一次执行总是在本次执行结束之后的2000毫秒开始
timer = setTimeout(f, 2000);
}, 2000);
clearTimeout() clearInterval()
var id1 = setTimeout(f, 1000);
var id2 = setInterval(f, 1000);
clearTimeout(id1);
clearInterval(id2);
debounce函数
有时,我们不希望回调函数被频繁调用。比如,用户填入网页输入框的内容,希望通过 Ajax 方法传回服务器。如果用户连续击键,就会连续触发keydown事件,造成大量的 Ajax 通信。这是不必要的,而且很可能产生性能问题。
正确的做法应该是,设置一个门槛值,表示两次 Ajax 通信的最小间隔时间。如果在间隔时间内,发生新的keydown事件,则不触发 Ajax 通信,并且重新开始计时。如果过了指定时间,没有发生新的keydown事件,再将数据发送出去。这种做法叫做debounce,防抖动。
// 在2500毫秒内,用户再次点击,会取消上一次定时器,然后再创建一个新定时器,保证了回调函数之间的时间间隔至少是2500毫秒
$('textarea').on('keydown', debounce(ajaxAction, 2500));
function debounce(fn, delay){
var timer = null; // 声明计时器
return function() {
var context = this;
var args = arguments;
clearTimeout(timer);
timer = setTimeout(function () {
fn.apply(context, args);
}, delay);
};
}
运行机制
setTimeout和setInterval的运行机制,是将指定的代码移出本轮事件循环,等到下一轮事件循环,再检查是否到了指定时间。如果到了,就执行对应的代码;如果不到,就继续等待。
这意味着,setTimeout和setInterval指定的回调函数,必须等到本轮事件循环的所有同步任务都执行完,才会开始执行。
由于前面的任务到底需要多少时间执行完,是不确定的,所以没有办法保证,setTimeout和setInterval指定的任务,一定会按照预定时间执行。
setTimeout(f, 0)
这种写法的目的是,尽可能早地执行f,但是并不能保证立刻就执行f。
必须要等到当前脚本的同步任务,全部处理完以后,才会执行setTimeout指定的回调函数f。也就是说,setTimeout(f, 0)会在下一轮事件循环一开始就执行。
setTimeout(function () {
console.log(1);
}, 0);
console.log(2);
// 2
// 1
Promise对象
resolve函数的作用是,将Promise实例的状态从未完成变为成功(即从pending变为fulfilled),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去。
reject函数的作用是,将Promise实例的状态从未完成变为失败(即从pending变为rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
微任务
Promise的回调函数不是正常的异步任务,而是微任务。
区别在于,正常任务追加到下一轮事件循环,微任务追加到本轮事件循环。这意味着,微任务的执行时间一定早于正常任务。
setTimeout(function() {
console.log(1);
}, 0);
// then是本轮事件循环执行,setTimeout是下一轮事件循环开始时执行
new Promise(function (resolve, reject) {
resolve(2);
}).then(console.log);
console.log(3);
// 3
// 2
// 1