目录
(2)对象是一个容器,封装了属性(property)和方法(method)。
3.clearTimeout(),clearInterval()
一、js下new命令的用法
1.对象
面向对象编程将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。
而类是一类事物,比如学生就是一个类,如果学生这个类通过new这个关键词生成一个实例对象,这个实例对象就是一个具体的人。通常在类中都含有一个构造函数,构造函数可以传递相应的属性,类在实例化的瞬间构造函数会自动执行把属性赋值给实例对象。假设在学生类中有一个学生,在实例化时构造函数可以给他赋值姓名为张三年龄为18,即就是将抽象的一类事物具体到某一个对象上。
在js中不存在类的概念,可以将构造函数视为一个类。
(1)对象是单个实物的抽象。
一本书、一辆汽车、一个人都可以是对象,一个数据库、一张网页、一个远程服务器连接也可以是对象。
(2)对象是一个容器,封装了属性(property)和方法(method)。
属性是对象的状态,方法是对象的行为(完成某种任务)。比如,我们可以把动物抽象为animal
对象,使用“属性”记录具体是哪一种动物,使用“方法”表示动物的某种行为(奔跑、捕猎、休息等等)。
2.构造函数
面向对象编程的第一步,就是要生成对象。对象是单个实物的抽象。通常需要一个模板,表示某一类实物的共同特征,然后对象根据这个模板生成。
JavaScript 语言使用构造函数(constructor)作为对象的模板。所谓”构造函数”,就是专门用来生成实例对象的函数。它就是对象的模板,描述实例对象的基本结构。一个构造函数,可以生成多个实例对象,这些实例对象都有相同的结构。
var Vehicle = function () {
this.price = 1000;
};上面代码中,
Vehicle
就是构造函数。为了与普通函数区别,构造函数名字的第一个字母通常大写。
构造函数的特点有两个:
-
函数体内部使用了
this
关键字,代表了所要生成的对象实例。 -
生成对象的时候,必须使用
new
命令。
3.new命令
(1)基本用法
new
命令的作用,就是执行构造函数,返回一个实例对象。
var Vehicle = function () {
this.price = 1000;
};var v = new Vehicle();
v.price // 1000上面代码中,new命令让构造函数Vehicle生成了一个实例对象保存在变量v中,这个实例对象从Vehicle中得到了price属性。new命令执行时,构造函数内部this代表新生成的实例对象,this.price代表实例对象具有price属性,值为1000。
(2)new命令重要性
如果忘记使用new命令,构造函数则变成普通函数,也不会生成实例对象,this此时也代表全局对象。
var Student = function(name) {
this.name = name
}
var Stu = Student('zhangsan')
console.info(Stu)
console.info(name)
控制台会返回:
undefined
zhangsna
原因:
由于忘记使用new命令,构造函数Student变成了一个普通函数,普通函数的调用是在全局调用,此时的this也指向全局,而全局中的顶层变量为window,window.name为'zhangsan';而出现undefined是由于构造函数被当成普通函数执行,但却没有普通函数应该具有的return返回值,所以zhangsan赋值过来时undefined。
4.Object.create() 创建实例对象
构造函数作为模板,可以生成实例对象。但是,有时拿不到构造函数,只能拿到一个现有的对象。我们希望以这个现有的对象作为模板,生成新的实例对象,这时就可以使用Object.create()
方法。
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 张三.上面代码中,对象
person1
是person2
的模板,后者继承了前者的属性和方法。
二、this关键字
1.含义
this
就是属性或方法“当前”所在的对象。
var person = {
name: '张三',
describe: function () {
return '姓名:'+ this.name;
}
};person.describe()
// "姓名:张三"
上面代码中,this.name
在describe
方法中调用,describe
方法所在的当前对象是person
,因此this
指向person
,this.name
就是person.name
。 相当于person调用了describe,describe调用了this.name,this.name的大环境在person这个对象里,所以指向了person中的属性name。
2.实质
由于函数是一个单独的值,所以它可以在不同的环境(上下文)执行。
var f = function () {};
var obj = { f: f };// 单独执行 this指向全局
f()// obj 环境执行 this指向obj
obj.f()
实例:
var f = function(){
console.log(x);
};
f()
此时结果为:
x is not defined
修改为:
var f = function(){
console.log(x);
};
var x = ‘aaa’
f()
此时结果为:
aaa
原因:f()运行在全局环境下,x指向全局里的window,而window中x未定义,所以第一次为undefined,第二次全局定义了x,即可以输出aaa。
3.使用场合
(1)全局环境
全局环境使用this
,它指的就是顶层对象window
。
this === window // true function f() { console.log(this === window); } f() // true
上面代码说明,不管是不是在函数内部,只要是在全局环境下运行,this
就是指顶层对象window
。
(2)构造函数
构造函数中的this
,指的是实例对象。
var Obj = function (p) { this.p = p; };
上面代码定义了一个构造函数Obj
。由于this
指向实例对象,所以在构造函数内部定义this.p
,就相当于定义实例对象有一个p
属性。this指的是o。
var o = new Obj('Hello World!'); o.p // "Hello World!"
(3)对象的方法
如果对象的方法里面包含this
,this
的指向就是方法运行时所在的对象。该方法赋值给另一个对象,就会改变this
的指向。
var obj ={ foo: function () { console.log(this); } }; obj.foo() // obj
上面代码中,obj.foo
方法执行时,它内部的this
指向obj
。
但是,下面这几种用法,都会改变this
的指向。
// 情况一 (obj.foo = obj.foo)() // window // 情况二 (false || obj.foo)() // window // 情况三 (1, obj.foo)() // window
上面代码中,obj.foo
就是一个值。这个值真正调用的时候,运行环境已经不是obj
了,而是全局环境,所以this
不再指向obj
。
可以这样理解,JavaScript 引擎内部,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
只是指向当前一层的对象,而不会继承更上面的层。
4.使用注意点
(1)避免多层 this
由于this
的指向是不确定的,所以切勿在函数中包含多层的this
。
var o = { f1: function () { console.log(this); var f2 = function () { console.log(this); }(); } } o.f1() // Object // Window
上面代码包含两层this
,结果运行后,第一层指向对象o
,第二层指向全局对象,因为实际执行的是下面的代码。
var temp = function () { console.log(this); }; var o = { f1: function () { console.log(this); var f2 = temp(); } }
一个解决方法是在第二层改用一个指向外层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
指向的改变。
事实上,使用一个变量固定this
的值,然后内层函数调用这个变量,是非常常见的做法,请务必掌握。
JavaScript 提供了严格模式,也可以硬性避免这种问题。严格模式下,如果函数内部的this
指向顶层对象,就会报错。
var counter = { count: 0 }; counter.inc = function () { 'use strict'; this.count++ }; var f = counter.inc; f() // TypeError: Cannot read property 'count' of undefined
上面代码中,inc
方法通过'use strict'
声明采用严格模式,这时内部的this
一旦指向顶层对象,就会报错。
(2)避免数组处理方法中的 this
数组的map
和foreach
方法,允许提供一个函数作为参数。这个函数内部不应该使用this
。
var o = { v: 'hello', p: [ 'a1', 'a2' ], f: function f() { this.p.forEach(function (item) { console.log(this.v + ' ' + item); }); } } o.f() // undefined a1 // undefined a2
上面代码中,foreach
方法的回调函数中的this
,其实是指向window
对象,因此取不到o.v
的值。原因跟上一段的多层this
是一样的,就是内层的this
不指向外部,而指向顶层对象。
解决这个问题的一种方法,就是前面提到的,使用中间变量固定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
另一种方法是将this
当作foreach
方法的第二个参数,固定它的运行环境。
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
(3)避免回调函数中的 this
回调函数中的this
往往会改变指向,最好避免使用。
var o = new Object(); o.f = function () { console.log(this === o); } // jQuery 的写法 $('#button').on('click', o.f);
上面代码中,点击按钮以后,控制台会显示false
。原因是此时this
不再指向o
对象,而是指向按钮的 DOM 对象,因为f
方法是在按钮对象的环境中被调用的。这种细微的差别,很容易在编程中忽视,导致难以察觉的错误。
为了解决这个问题,可以采用下面的一些方法对this
进行绑定,也就是使得this
固定指向某个对象,减少不确定性。
5.绑定this的方法
(1)Function.prototype.call()
函数实例的call
方法,可以指定函数内部this
的指向(即函数执行时所在的作用域),然后在所指定的作用域中,调用该函数。
var obj = {}; var f = function () { return this; }; f() === window // true f.call(obj) === obj // true
上面代码中,全局环境运行函数f
时,this
指向全局环境(浏览器为window
对象);call
方法可以改变this
的指向,指定this
指向对象obj
,然后在对象obj
的作用域中运行函数f
。
(2)Function.prototype.apply()
apply
方法的作用与call
方法类似,也是改变this
指向,然后再调用该函数。唯一的区别就是,它接收一个数组作为函数执行时的参数,使用格式如下。
func.apply(thisValue, [arg1, arg2, ...])
apply
方法的第一个参数也是this
所要指向的那个对象,如果设为null
或undefined
,则等同于指定全局对象。第二个参数则是一个数组,该数组的所有成员依次作为参数,传入原函数。原函数的参数,在call
方法中必须一个个添加,但是在apply
方法中,必须以数组形式添加。
function f(x, y){ console.log(x + y); } f.call(null, 1, 1) // 2 f.apply(null, [1, 1]) // 2
上面代码中,f
函数本来接受两个参数,使用apply
方法以后,就变成可以接受一个数组作为参数。
(3)Function.prototype.bind()
bind()
方法用于将函数体内的this
绑定到某个对象,然后返回一个新函数。
var d = new Date(); d.getTime() // 1481869925657 var print = d.getTime; print() // Uncaught TypeError: this is not a Date object.
上面代码中,我们将d.getTime()
方法赋给变量print
,然后调用print()
就报错了。这是因为getTime()
方法内部的this
,绑定Date
对象的实例,赋给变量print
以后,内部的this
已经不指向Date
对象的实例了。
bind()
方法可以解决这个问题。
var print = d.getTime.bind(d); print() // 1481869925657
上面代码中,bind()
方法将getTime()
方法内部的this
绑定到d
对象,这时就可以安全地将这个方法赋值给其他变量了。
三、JavaScript闭包和this
1.闭包
在JS中,通俗来讲,闭包就是能够读取外层函数内部变量的函数。
(1)读取函数内部的局部变量
①在函数内部再定义一个函数
function f1() { let code = 200; function f2() { console.log(code); } }
函数f1内部的函数f2可以读取f1中所有的局部变量。因此,若想在外部访问函数f1中的局部变量code
,可通过函数f2间接访问。
②为外部程序提供访问函数局部变量的入口
function f1() { let code = 200; function f2() { console.log(code); } return f2; } f1()(); // 200
(2)闭包概念
(1)中的函数f2,就是闭包,其作用就是将函数内部与函数外部进行连接。
-
闭包访问的变量,是每次运行上层函数时重新创建的,是相互独立的。
-
不同的闭包,可以共享上层函数中的局部变量
2.this
(1)四类调用方法
①作为对象方法的调用
function f() { console.log( this.code ); } let obj = { code: 200, f: f }; obj.f(); // 200
②纯粹的函数调用
function f() { console.log( this.code ); } // 此处,通过var(函数作用域)声明的变量code会绑定到window上;如果使用let(块作用域)声明变量code,则不会绑定到window上,因此下面的2次函数调用f(),会输出undefined // let code = 200; var code = 200; f(); // 200 code = 404; f(); // 404
③作为[构造函数]调用
code = 404 function A() { this.code = 200 this.callA = function() { console.log(this.code) } } var a = new A() a.callA() // 200, callA在new A返回的对象里
④使用apply、call、bind调用
apply
var code = 404; let obj = { code: 200, f: function() { console.log(this.code); } } obj.f(); // 200, 实际上是作为对象的方法调用 obj.f.apply(); // 404,参数为空时,默认使用全局对象global,在此处为对象window obj.f.apply(obj); //200,this指向参数中设置的对象
call
function f() { console.log( this.code ); } var obj = { code: 200 }; f.call( obj ); // 200
bind
// bind返回一个新的函数 function f(b) { console.log(this.a, b); return this.a + b; } var obj = { a: 2 }; var newF = f.bind(obj); var result = newF(3); // 2 3 console.log(result); // 5
(2) 箭头函数中的this
箭头函数中的this是定义函数时绑定的,而不是在执行函数时绑定。若箭头函数在简单对象中,由于简单对象没有执行上下文,所以this指向上层的执行上下文;若箭头函数在函数、类等有执行上下文的环境中,则this指向当前函数、类。
(3)举例
function f() {
//宏任务
setTimeout(() => {
console.log(">>>" + this); // >>>[object object],语句5
this.code = 401;
}, 0) 定时器为0表示不会延迟,详见(3.定时器)
//同步
console.log( this.code );
}let obj = {
">>>" + this
code: 200,
foo: f
};var code = 500;
同步和异步 关键词说的很好(详见4.宏任务微任务 同步异步)
宏任务 微任务
1.宏任务 微任务 同步异步
2.箭头函数this指向问题
3.字符串+ this 改写为[object object]的形式
obj.foo(); // 200,语句1
console.log("--" + obj.code); // --200,语句3
//宏任务
setTimeout(()=>{console.log("---" + obj.code);}, 0); // ---401,语句4obj.foo(); (语句1):调用 obj 对象的 foo 方法。
输出:200
解释:在 foo 方法内部的 console.log(this.code) 打印出 obj 对象的 code 属性,其值为 200。console.log("--" + obj.code); (语句3):打印 obj 对象的 code 属性。
输出:--200
解释:在全局作用域中,code 被赋值为 500,但这里的 obj.code 指向的是 obj 对象的 code 属性,其值仍然是 200。setTimeout(() => { console.log("---" + obj.code); }, 0); (语句4):设置一个零延迟的定时器,其中的箭头函数在调用。
输出:---401
解释:在调用 obj.foo() 的过程中,foo 方法中的 setTimeout 在当前宏任务结束后执行。由于是箭头函数,this 的值保持与父作用域一致(也就是 obj 对象)。所以在箭头函数内部,this.code 被设置为 401。setTimeout(() => { console.log(">>>" + this); this.code = 401; }, 0) (语句5):设置一个零延迟的定时器,其中的箭头函数在调用。
输出:>>>[object Object]
解释:在调用 obj.foo() 的过程中,foo 方法中的 setTimeout 在当前宏任务结束后执行。箭头函数的 this 始终指向它被创建时的外部作用域,所以 this 指向了 obj 对象,而在控制台中打印 this 时会将其转换为字符串。所以输出为 >>>[object Object]。
四、定时器
1.setTimeout()
setTimeout`函数用来指定某个函数或某段代码,在多少毫秒之后执行。它返回一个整数,表示定时器的编号,以后可以用来取消这个定时器。
var timerId = setTimeout(func|code, delay);
上面代码中,setTimeout
函数接受两个参数,第一个参数func|code
是将要推迟执行的函数名或者一段代码,第二个参数delay
是推迟执行的毫秒数。
console.log(1); setTimeout('console.log(2)',1000); console.log(3); // 1 // 3 // 2
上面代码会先输出1和3,然后等待1000毫秒再输出2。注意,console.log(2)
必须以字符串的形式,作为setTimeout
的参数。
如果推迟执行的是函数,就直接将函数名,作为setTimeout
的参数。
2.setInterval()
setInterval
函数的用法与setTimeout
完全一致,区别仅仅在于setInterval
指定某个任务每隔一段时间就执行一次,也就是无限次的定时执行。
var i = 1 var timer = setInterval(function() { console.log(2); }, 1000)
上面代码中,每隔1000毫秒就输出一个2,会无限运行下去,直到关闭当前窗口。
3.clearTimeout(),clearInterval()
setTimeout
和setInterval
函数,都返回一个整数值,表示计数器编号。将该整数传入clearTimeout
和clearInterval
函数,就可以取消对应的定时器。
var id1 = setTimeout(f, 1000); var id2 = setInterval(f, 1000); clearTimeout(id1); clearInterval(id2);
上面代码中,回调函数f
不会再执行了,因为两个定时器都被取消了。
五、宏任务微任务 同步异步
1.执行顺序
执行顺序:先执行同步代码,遇到异步宏任务则将异步宏任务放入宏任务队列中,遇到异步微任务则将异步微任务放入微任务队列中,当所有同步代码执行完毕后,再将异步微任务从队列中调入主线程执行,微任务执行完毕后再将异步宏任务从队列中调入主线程执行,一直循环直至所有任务执行完毕。
2.案例
//宏任务 放进队列
setTimeout(function(){ setTimeout是宏任务
console.log(1);
});
//微任务
new Promise(function(resolve){ promise是微任务
console.log(2);
resolve();
}).then(function(){(promise是一个有回调函数的值,回调函数是resolve,当promise异步函数执行后会调用回调函数,结果会接过来到resolve(),console.log(3)。)
console.log(3);
}).then(function(){
console.log(4)
});
//同步代码
console.log(5);
// 2 5 3 4 1上述代码中,遇到setTimout异步宏任务,先放入宏任务队列中
然后遇到new Promise,new Promise在实例化的过程中所执行的代码都是同步进行的,所以输出2
Promise.then,异步微任务,将其放入微任务队列中
遇到同步任务console.log(5);输出5;主线程中同步任务执行完
从微任务队列中取出任务到主线程中,输出3、 4,微任务队列为空
从宏任务队列中取出任务到主线程中,输出1,宏任
务队列为空
六、prototype
1.原型对象概述
(1)构造函数的缺点
function Cat (name, color) {
this.name = name;
this.color = color;
}var cat1 = new Cat('大毛', '白色');
cat1.name // '大毛'
cat1.color // '白色'
上面代码中,Cat
函数是一个构造函数,函数内部定义了name
属性和color
属性,所有实例对象(上例是cat1
)都会生成这两个属性,即这两个属性会定义在实例对象上面。
通过构造函数为实例对象定义属性,虽然很方便,但是有一个缺点。同一个构造函数的多个实例之间,无法共享属性,从而造成对系统资源的浪费。
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
上面代码中,cat1
和cat2
是同一个构造函数的两个实例,它们都具有meow
方法。由于meow
方法是生成在每个实例对象上面,所以两个实例就生成了两次。也就是说,每新建一个实例,就会新建一个meow
方法。这既没有必要,又浪费系统资源,因为所有meow
方法都是同样的行为,完全应该共享。
这个问题的解决方法,就是 JavaScript 的原型对象(prototype)。
(2)prototype属性作用
构造函数的一个方法,或者一个属性
function Animal(name) {
this.name = name;
}构造函数理解为一个类,类中有个属性prototype,被称为子类的原型对象,相当于在父类中增加了一个属性,子类可以共享。
Animal.prototype.color = 'white';
var cat1 = new Animal('大毛');
var cat2 = new Animal('二毛');cat1.color // 'white'
cat2.color // 'white'定义在同一个原型对象上,cat1.color === cat2.color。
(3)原型链
JavaScript 规定,所有对象都有自己的原型对象(prototype)。一方面,任何一个对象,都可以充当其他对象的原型;另一方面,由于原型对象也是对象,所以它也有自己的原型。因此,就会形成一个“原型链”(prototype chain):对象到原型,再到原型的原型……
例如:cat1的原型对象是→Animal.prototype→×××.prototype→object.prototype→null
python最上层的父类是type(type一个类)
每个方法都有valueOf和toString,这两个方法是定义在object.prototype上的,object.prototype.valueOf = function()。