共计 10342 个字符,预计需要花费 26 分钟才能阅读完成。
这是 ES5 的入门篇教程的笔记,网址:JavaScript 教程,以下内容中黑体表示大标题,还有一些重点;斜体表示对于自身,还需要下功夫学习的内容。这里面有一些自己的见解,所以若是发现问题,欢迎指出~
实例对象与 new 命令
面向对象编程是目前主流的编程范式,它将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。
对象是单个事物的抽象。
对象是一个容器,封装了属性(property)和方法(method)。
属性是对象的状态,方法是对象的行为。两者最主要的区别在于属性属于对象静态的一面,方法属于对象动态的一面。
构造函数
面向对象编程的第一步,就是要生成对象,通常需要一个模板,表示某一类实物的共同特征,然后对象根据这个模板生成。
典型的面向对象编程语言(比如 C ++ 和 JAVA),都有“类”(class)这个概念。所谓“类”就是对象的模板,对象就是“类”的实例。但是 JavaScript 语言的对象体系,不是基于“类”的,而是基于构造函数(constructor)和原型链(prototype)。
构造函数,为了与普通函数区别,构造函数名字的第一个字母通常大写。它有两个特点:1)函数体内部使用了 this 关键字,代表了所要生成的对象实例;2)生成对象的时候,必须使用 new 命令。
new 命令
new 命令的作用,就是执行构造函数,返回一个实例对象。
new 命令本身就可以执行构造函数,所以后面的构造函数可以带括号,也可以不带括号。但是为了表示这里是函数调用,推荐使用括号。
// 推荐的写法
let v = new Vehicle();
// 不推荐的写法
let v = new Vehicle(); // 这两种写法都是等价的
new 命令的原理
使用 new 命令时,它后面的函数依次执行下面的步骤:
1、创建一个空对象,作为将要返回的对象实例;2、将这个空对象的原型,指向构造函数的 prototype 属性;3、将这个空对象赋值给函数内部的 this 关键字;4、开始执行构造函数内部的代码。
如果构造函数内部由 return 语句,而且 return 后面跟着一个对象,new 命令会返回 return 语句指定的对象;否则,就会不管 return 语句,返回 this 对象。(也就是说,new 后,只能返回对象,要么是自身,要么是一个新对象。)
构造函数与普通函数最主要的区别,是内部有没有 this 关键字的函数。
对普通函数使用 new 命令,会返回一个空对象。
function getMessage() {
let a = 1;
return 'this is a message';
}
let msg = new getMessage(); // {}
typeof msg // "object"
构造函数作为模板,可以生成实例对象,但是,有时拿不到构造函数,只能拿到一个现有的对象,通过 Object.create()方法,可以将这个现有的对象作为模板,生成新的实例对象。
let person1 = {
name: '张三',
greeting: function() {console.log('Hi! I\'m '+ this.name +'.');
}
}
let person2 = Obejct.create(person1); // person2 继承了 person1 的属性和方法。person2.name // 张三
person2.greeting() // Hi! I'm 张三.
this 关键字
如果 this 所在的方法不在对象的第一层,这时 this 只是指向当前一层的对象,而不会继承更上面一层。
由于 this 的指向是不确定的,所以切勿在函数中包含多层的 this。
let a = {
p: 'Hello',
b: {m: function() {console.log(this.p);
}
}
};
a.b.m() // undefined a.b.m 方法在 a 对象的第二层,该方法内部的 this 不是指向 a,而是指向 a.b。
let o = {f1: function () {console.log(this);
let f2 = function () {console.log(this);
}(); // 这里是执行函数了,就变成了值}
}
o.f1()
// Object 第一层指向对象 o
// Window 第二层指向全局对象
// 实际执行的如下
let temp = function () {console.log(this);
};
let o = {f1: function() {console.log(this);
let f2 = temp();}
}
数组中的 map 和 foreach 方法,允许提供一个函数作为参数,这个函数内部不应该使用 this。因为两者回调的 this,是指向 window 对象的。(内层的 this 不指向外部,而是指向顶层对象)解决这种方法可以用中间变量,也可以将 this 当作 foreach 方法的第二个参数,固定运行环境。
JavaScript 提供了 call、apply、bind 三个方法,来切换 / 固定 this 的指向。
函数实例的 call 方法,可以指定函数内部 this 的指向(即函数执行时所在的作用域),然后在所指定的作用域中,调用该函数。
call 的第一个参数就是 this 所要指向的那个对象,后面饿参数则是函数调用时所需的函数。
apply 方法的作用与 call 方法类似,也是改变 this 指向,然后再调用该函数。唯一的区别就是,它接收一个数组作为函数执行时的参数。
如果两个方法没有参数,或者参数为 null 或 undefined,则等同于指向全局对象。
let obj = {};
let f = function () {return this;};
f() === window // true
f.call(obj) === obj // true
func.call(thisVlaue, arg1, arg2, ...)
func.apply(thisValue, [arg1, arg2, ...])
function f(x, y) {console.log(x + y);
}
f.call(null, 1, 1) //2
f.apply(null, [1, 1]) //2
// JS 不提供找出数组最大元素的函数,结合 apply 方法和 Math.max 方法,就可以返回数组的最大元素
let a = [10, 2, 4, 15, 9];
Math.max.apply(null, a) // 15
bind 方法用于将函数体内的 this 绑定到某个对象,然后返回一个新函数。
let d = new Date();
d.getTime() // 1561974996108
let print = d.getTime; // 赋值后,内部的 this 已经不指向 Date 对象的实例了
print() // Uncaught TypeError: this is not a Date object.
// 使用 bind
let print = d.getTime.bind(d);
print() // 1561974996108
空元素(null)与 undefined 的差别在于,数组的 forEach 方法会跳过空元素,但是不会跳过 undefined。
对象的继承
大部分面向对象的编程语言,都是通过“类(class)”实现对象的继承。传统上,JavaSCript 语言的继承不通过 class,而是通过“原型对象(prototype)”实现。
构造函数的缺点:同一个构造函数的多个实例之间,无法共享属性,从而造成对系统资源的浪费。
function Cat(name, color) {
this.name = name;
this.color = color;
this.meow = function () {console.log('喵喵');
};
}
let cat1 = new Cat('大毛', '白色');
let cat2 = new Cat('二毛', '黑色');
cat1.meow === cat2.meow // false cat1 和 cat2 是同一个构造函数的两个实例,它们都具有 meow 方法。由于 meow 方法是生成在每个实例对象上面的,所以两个实例就生成了两次,没有必要,也浪费了系统资源,需要共享,也就是 JavaScript 的原型对象(prototype)。
prototype 属性的作用
JavaScript 继承机制的涉及思想就是,原型对象的所有属性和方法,都能被实例对象共享。也就是说,如果属性和方法定义在原型上,那么所有实例对象就能共享,不仅节省了内存,还体现了实例对象之间的联系。(感觉像是 Java 类中的公共属性和公共方法一样。。。)
JavaScript 规定,每个函数都有一个 prototype 属性,指向一个对象。对于普通函数来说,该属性基本无用,但是,对于构造函数来说,生成实例的时候,该属性会自动成为实例对象的原型。
function f() {}
typeof f.prototype // "object"
function Animal(name) {this.name = name;}
Animal.prototype.color = 'white'; // 原型对象上添加一个 color 属性,下面的实例对象都共享了该属性。let cat1 = new Animal('大毛');
let cat2 = new Animal('二毛');
cat1.color // 'white'
cat2.color // 'white'
// 原型对象的属性不是实例对象自身的属性。只要修改原型对象,变动就立刻会体现在所有实例对象上
Animal.prototype.color = 'yellow';
cat1.color // "yellow"
cat2.color // "yellow"
// 如果实例对象自身就有某个属性或方法,它就不会再去原型对象寻找这个属性或方法。cat1.color = 'black';
cat1.color // 'black'
cat2.color // 'yellow'
综上,原型对象的作用,就是定义所有实例对象共享的属性和方法,而实例对象可以视作从原型对象衍生出来的子对象。
原型链
JavaScript 规定,所有对象都有自己的原型对象(prototype)。一方面,任何我一个对象,都可以充当其他对象的原型;另一方面,由于原型对象也是对象,所以它也有自己的原型。因此,就会形成一个“原型链”(prototype chain):对象到原型,再到原型的原型……如果一层层地上溯,所有对象的原型最终都可以上溯到 Object.prototype,即 Object 构造函数的 prototype 属性。也就是说,所有对象都继承了 Object.prototype 的属性。这就是所有对象都有 valueOf 和 toString 方法的原因,因为这是从 Object.prototype 继承的。
其实,Object.prototype 对象也有他的原型,Object.prototype 的原型是 null。null 没有任何属性和方法,也米有自己的原型。因此,原型链的尽头就是 null。
Object.getPrototypeOf(Object.prototype) // null
如果对象自身和它的原型,都定义了一个同名属性,那么优先读取对象自身的属性,这叫做“覆盖”(overriding)。举例来说,如果让构造函数的 prototype 属性指向一个数组,就意味着实例对象可以调用数组方法。
constructor 属性
prototype 对象有一个 constructor 属性,默认指向 prototype 对象所在的构造函数。
constructor 属性的作用是,可以得知某个实例对象,到底是哪一个构造函数产生的。
function P() {}
P.prototype.constructor === P // true constructor 属性定义在 prototype 对象上,意味着可以被所有实例对象继承。let p = new P();
p.constructor === P // true p 是构造函数 P 的实例对象
p.constructor === P.prototype.constructor // true
p.hasOwnPrototype('constructor') // false p 自身没有 constructor 属性,该属性是读取原型链上面的 P.prototype.constructor 属性
instanceof 运算符
instanceof 运算符返回一个布尔值,表示对象是否为某个构造函数的实例。(左边是实例对象,右边是构造函数。)
let v = new Vehicle();
v instanceof Vehicle // true 实际检查右边构建函数的原型对象(prototype),是否在左边对象的原型链上。// 等同于
Vehicle.prototype.isPrototypeOf(v)
// instanceOf 检查的是整个原型链,因此同一个实例对象,可能会对多个构造函数都返回 true
let d = new Date();
d instanceof Date // true
d instanceof Object // true d 同时是 Date 和 Object 的实例
// 任意对象(除了 null)都是 Object 的实例,所以 instanceof 运算符可以判断一个值是否为非 null 的对象
typeof null // Object 是为了防止这种情况的发生
null instanceof Object // false
// 但是需要注意的是,instanceof 运算符只能用于对象,不适用原始类型的值
let s = 'hello';
's' instanceof String // false
new String('s') instanceof String // true
// 对于 undefined 和 null,instanceof 运算符总是返回 false
undefined instanceof Object // false
null instanceof Object // false
emm,对象的继承中的模块内容还需要下狠功夫
Object 对象的相关方法
Object.getPrototypeOf() 方法返回参数对象的原型,这是获取原型对象的标准方法。
let F = function () {};
let f = new F();
Object.getPrototypeOf(f) === F.prototype // true
// 特殊对象的原型
// 空对象的原型是 Object.prototype
Object.getPrototypeOf({}) === Object.prototype // true
// Object.prototype 的原型是 null
Object.getPrototypeOf(Objectprototype) === null // true
// 函数的原型是 Function.prototype
function f() {}
Object.getPrototypeOf(f) === Function.prototype // true
Object.create()可以从一个实例对象,生成另一个实例对象。该方法接受一个对象作为参数,然后以它为原型,返回一个实例对象,该实例完全继承原型对象的属性。
// 原型对象 没有 this,所以不是构造函数!!!不能用 new 来创建,这是一个实例对象。let A = {print: function () {console.log('hello');
}
}
// 实例对象
let B = Object.create(A); // 相当于创建一个空的构造函数,将其.prototype 属性指向参数对象 A,从而实现让该实例继承 A 的属性。Object.getPrototypeOf(B) === A // true 以 A 对象为原型,生成了 B 对象,B 继承了 A 的所有属性和方法
B.print() // hello
B.print === A.print // true
// 以下三种方式生成的新对象是等价的
let obj1 = Object.create({});
let obj2 = Object.create(Object.prototype);
let obj3 = new Object();
//Object.create 方法生成的新对象,动态继承了原型。在原型上添加或修改任何方法,会立刻反映在新对象之上。let obj1 = {p: 1};
let obj2 = Object.create(obj1);
obj1.p = 2;
obj2.p // 2 修改对象原型 obj1 会影响到实例对象 obj2
obj2.p = 3;
obj1.p // 1 修实例对象 obj2 并不会影响到原型对象 obj1
obj2.p // 3
length 用来截断长度,只对数组有效,对字符串无效。
emm 面向对象的编程也是云里雾里的,fighting!
异步操作概述
JavaScript 只在一个线程上运行,也就是说,JavaScript 同时只能执行一个任务,其他任务都必须在后面排队等待。
程序里面所有的任务,可以分成两类:同步任务(synchronous)和异步任务(asynchronous)。同步任务是那些没有被引擎挂起、在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。异步任务是那些被引擎放在一边,不进入主线程、而进入任务队列的任务,只有引擎认为某个异步任务可以执行了(比如 Ajax 操作从服务器得到了结果),该任务(采用回调函数的形式)才会进入主线程执行。也就是说,异步任务不具有“堵塞”效应。
JavaScript 运行时,除了一个正在运行的主线程,引擎还提供了一个任务队列(task queue),里面是各种需要当前程序处理的异步任务。(实际上,根据异步任务的类型,存在多个任务队列。为了方便理解,这里假设只存在一个队列。)
引擎如何确定异步任务有没有结果,能不能进入主线程呢?答案就是引擎在不停地检查,一遍又一遍,只要同步任务执行完了,引擎就会去检查那些挂起来的异步任务,是不是可以进入主线程了。这种循环检查的机制,就叫做时间循环(Event Loop)。维基百科的定义是:“事件循环是一个程序结构,用于等待和发送消息和事件”(a programming construct that waits for and dispatches events or messages in a program)。
异步操作的模式:
1、回调函数 回调函数是异步操作最基本的方法。(包括 Promise)
function f1() {// ...}
function f2() {// ...}
f1();
f2(); // 这样编程的意图是 f2 必须等到 f1 执行完成,才能执行。// 但是如果 f1 是异步操作,f2 会立即执行,不会等到 f1 结束再执行。这种情况下,可以考虑改写 f1,把 f2 写成 f1 的回调函数。function f1(callback) {
// ...
callback();}
function f2() {// ...}
f1(f2); // 回调函数的优点是简单、容易理解和实现,缺点是不利于代码的阅读和维护,各个部分之间高度耦合(coupling),使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况),而且每个任务只能指定一个回调函数。
2、事件监听
以前只记着是定时器,现在才知道,应该是事件监听!!!还是需要多看!!!
另一种思路是采用事件驱动模式。异步任务的执行不取决于代码的顺序,而取决于某个事件是否发生。
f1.on('done', f2); // 当 f1 发生 done 事件,就执行 f2。接着,对 f1 进行改写
function f1() {setTimeout(function () {
// ...
f1.trigger('done'); // 表示执行完成后,立即触发 done 事件,从而开始执行 f2
}, 1000);
}
这种方法比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,并且可以“去耦合”(decoupling),有利于实现模块化。缺点是整个程序都要编程事件驱动型,运行流程会变得很不清晰。阅读代码的时候,很难看出主流程。
3、发布 / 订阅
事件完全可以理解成“信号”,如果存在一个“信号中心”,某个任务执行完成,就向信号中心“发布”(publish)一个信号,其他任务可以向信号中心“订阅”(subscribe)整个信号,从而知道什么时候自己可以开始执行。这就叫做“发布 / 订阅模式”(publish-subscribe parttern),又称“观察者模式”(observer pattern)。
观察者模式还不是了解,还需要学习!!
定时器
JavaScript 提供定时执行代码的能力,叫做定时器(timer),主要由 setTimeout() 和 setInterval()这两个函数来完成。
1、setTimeout()
该函数用来指定某个函数或某段代码,再多少毫秒之后执行。它返回一个整数,表示定时器的编号,以后可以用来取消这个定时器。
setTimeout 函数接受两个参数,第一个参数 func|code 是将要推迟执行的函数名或者一段代码,第二个参数是推迟执行的毫秒数。
let timerId = setTimeout(func|code, delay);
console.log(1);
setTimeout('console.log(2)', 1000); // 需要注意的是,console.log(2)必须以字符串的形式,作为 setTimeout 的参数
console.log(3);
// 1
// 3
// 2
function f() {console.log(2);
}
setTimeout(f, 1000); // 如果推迟执行的是函数,就直接将函数名,作为 setTimeout 的参数。
2、setInterval()
该函数的用法与 setTimeout 完全一样,区别仅仅在于 setInterval 指定某个任务每隔一段时间就执行一次,也就是无限次的定时执行。
// 清除定时器
var id1 = setTimeout(f, 1000);
var id2 = setInterval(f, 1000);
clearTimeout(id1);
clearInterval(id2);
3、实例:debounce 函数
debounce 防抖动, 这是个好东西,以后要好好看看!!感觉可以用到小程序以及 h5 的输入框输入!!
4、setTimeout(f, 0)
setTimeout 的作用是将代码推迟到指定时间执行,如果指定时间为 0,即 setTimeout(f, 0), 那么会立刻执行吗?答案是不会,因为它必须要等到当前脚本的同步任务,全部处理完以后,才会执行 setTimeout 指定的回调函数 f。也就是说,setTimeout(f, 0)会在下一轮事件循环一开始就执行。
setTimeout(function () {console.log(1);
}, 0);
console.log(2);
// 2
// 1
Promise 对象
Promise 对象是 JavaScript 的异步操作解决方案,为异步操作提供统一接口。它起到代理作用(proxy),充当异步操作与回调函数之间的中介,使得异步操作具备同步操作的接口。
Promise 对象的状态:Promise 对象通过自身的状态,来控制异步操作。
Promise 实例具有三种状态:
1)异步操作未成功(pending)
2)异步操作成功(fulfilled)
3)异步操作失败(rejected)
三种状态里面,fulfilled 和 rejected 合在一起称为 resolved(已定型)。
这三种状态的变化途径只有两种:从“未完成”到“成功”;从“未完成”到“失败”。
一旦状态发生变化,就凝固了,不会再有新的状态变化。这也是 Promise 这个名字的又来,它的英文意思是“承若”,一旦承诺成效,就不得再改变了。这也意味着,Promise 实例的状态变化只可能发生一次。因此,Promise 的最终结果只有两种:
1)异步操作成功,Promise 实例传回一个值(value),状态变为 fulfilled;
2)异步操作失败,Promise 实例抛出一个错误(error),状态变为 rejected。
JavaScript 提供原生的 Promise 构造函数,用来生成 Promise 实例。
Promise 构造函数接受一个函数作为参数,该函数的两个参数分别是 resolve 和 reject,它们是两个函数,由 JavaScript 引擎提供,不用自己实现。
resolve 函数的作用是,将 Promise 实例的状态从“未完成”变为“成功”(即从 pending 变为 fulfilled),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去。
reject 函数的作用是,将 Promise 实例的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
DOM 概述
DOM 是 JavaScript 操作网页的接口,全称为“文档对象模型”(Document Object Model)。它的作用是将网页转为一个 JavaScript 对象,从而可以用脚本进行各种操作(比如增删内容)。
DOM 的最小组成单位叫做节点(node)。文档的属性结构(DOM 树),就是由各种不同类型的节点组成,每个节点可以看作是文档树的一片叶子。
节点的类型有七种:
Document:整个文档树的顶层节点
DocumentType:doctype 标签(比如 <!DOCTYPE html>)
Element:网页的各种 HTML 标签(比如 <body>、<a> 等)
Attribute:网页元素的属性(比如 class=”right”)
Text:标签之间或标签包含的文本
Comment:注释
DocumentFragment:文档的片段
浏览器提供一个原生的节点对象 Node,上面这七种节点都继承了 Node,因此具有一些共同的属性和方法。
emm 后面的看不下去了,再见!!!