你不知道的JS(上卷)第二部分
1.this的作用是什么?
this提供了一种更优雅的方式来隐试传递对象的引用,因此可以将API设计的更加简洁并且易于复用。随着你的使用模式越来越复杂,显式传递上下文会让代码变得越来越混乱,使用this就不会这样。
2.错误的理解
一般情况下,我们理解的this是指向这个函数自身的,但是这事一种错误的理解,其实this大多数的情况下不会指向函数自身,如果强制this指向函数自身可以使用call()或者apply()这样的方法,将自身函数传递进去。还有一种错误的理解,就是this是指向这个函数的词法作用域的。要明确是是this在任何情况下都不指向函数的词法作用域,因为作用域“对象”是无法通过JS代码来访问的,它存在于JS引擎内部。下面我们看看这样的一段代码,可以帮助我们理解JS:
function foo(num){
console.log("foo: "+num)
this.count++
}
foo.count = 0
for(var i=0;i<10;i++){
if(i>5){
foo(i)
}
}
console.log(foo.count)//0
这段代码的输出结果是0,为啥呢,难道不应该是4吗?下面我们就要解释一下,首先在foo函数内部有一个this.count ++这样的语句,但是这个this到底是谁呢?如果我们理解this是这个函数本身,那么我们就会认为输出结果为4。其实this是指向调用这个foo函数的对象的,谁调用了foo,this就指向谁。然后我们看到在for循环中有foo函数的调用,但是并没有显示是谁调用的,没有显示我们就理解为是window调用的,所以this是指向window的,但是我们输出的是foo.count这个count是foo的属性count(因为foo是函数,函数也是对象,对象就可以有属性),所以还是初始化之后的值0,而不是4.
然后看下面这段代码:
function foo(num){
console.log("foo: "+num)
this.count++
}
foo.count = 0
count = 0
for(var i=0;i<10;i++){
if(i>5){
foo(i)
}
}
console.log(foo.count,count)
这段代码的输出结果是0,4 第一个0前面已经解释过了,这个4是怎么回事呢?其实这个4,是window中的4,也就是在foo函数中递增的那个count的值。这个count是我们预期的结果!
或者你还可以这样做,让this指向这个函数自身,那么这个foo函数的this的count自增就相当于给这个函数的count自增。代码如下:
function foo(num){
console.log("foo: "+num)
this.count++
}
foo.count = 0
for(var i=0;i<10;i++){
if(i>5){
foo.call(foo,i)
}
}
console.log(foo.count)
3.this的绑定规则
3.1默认绑定
即没有任何对象调用的时候,就可以理解为window在调用。
3.2隐式绑定
就是一个obj.foo()这样的形式,this是指向obj的。对象属性引用链中只由上一层或者说最后一层在调用位置中起作用。举例来说:
function foo(){
console.log(this.a)
}
var obj2 = {
a:42,
foo:foo
}
var obj1 = {
a:2,
obj2:obj2
}
obj1.obj2.foo()//42
另外还存在一个隐式丢失的问题,举例来说:
//此代码需要在浏览器中运行
function foo(){
console.log(this.a)
}
var obj = {
a:42,
foo:foo
}
var bar = obj.foo
var a = "oops global"
bar()//oops global
虽然bar是obj.foo的一个引用(栈中),但是实际上,它引用的是foo函数本身(堆中),因此此时的bar()其实是一个不带任何修饰的函数调用,因此应用了默认规则。
3.3显示绑定
显示绑定是通过apply和call来实现的。对于上面绑定丢失的问题可以使用bind来处理,示例代码如下:
function foo(){
console.log(this.a)
}
var obj = {
a:42,
foo:foo
}
var bar = foo.bind(obj)
var a = "oops global"
bar()//42
bind函数会返回一个硬编码的新函数,它会把你指定的参数设置为this的上下文。
3.4new绑定
在讲解new绑定之前我们需要澄清一非常重要的关于JS的函数和对象的误解。在传统的面向对象语言中,构造函数是一种特殊的函数,在使用new初始化类时会调用类中的构造函数。JS也有一个构造函数,使用方法看起来一样,其实完全不同。JS的构造函数只是一些使用new操作符时被调用的函数。它不是属于某一个类,也不会实例化一个类,实际上它都不能说是一种特殊的函数,只是带上了new操作符的普通函数。使用new来调用函数的时候,会创建一个新的对象,这个新对象会被绑定到函数调用的this,如果函数没有返回其他对象,那么会自动返回这个新的对象。
function Foo(arg){
this.a = arg
}
var bar = new Foo(2)
console.log(bar.a)//2
当你把null或者undefined作为this的绑定对象传入call,apply或者bind,这些值在调用的时候会被忽略,实际应用的是默认绑定规则。
4 绑定例外
4.1被忽略的this
//几个实用的小方法
function foo(a,b){
console.log('a: '+a,'b: '+b)
}
foo.apply(null,[2,4])//a: 2 b: 4 apply解构数组
foo(...[7,8])//a: 7 b: 8 ES6解构赋值
var bar = foo.bind(null,2,5)//a: 2 b: 5 bind初始化参数
bar()
使用bind和apply来初始化参数的时候,其实我们并不关心函数里面this的指向,但是必须传入一个this,这时候传一个null是不错的选择。如果这个函数确实使用了this,那么传null,就会使this绑定到全局对象,这是不安全的。但是可以传入这个Ø来解决。示例代码如下:
console.log(Ø)
var Ø = Object.create(null)
console.log(Ø)
5.深入理解对象
基本类型并不是对象,null有时会被当做对象类型,但是这其实只是语言本身的一个bug,即对null执行typeof操作会返回object,但实际上null只是一个基本类型。数组和函数都是对象的一种子类型。
5.1内置对象
JS中还有一些对象子类型,通常被称为内置对象。有些内置对象的名字看起来和简单基础类型一样,不过实际上他们的关系更为复杂。有String,Number,Boolean,Array,Object,Function,Date,RegExp,Error。这些内置函数可以当做构造函数来使用,创建一个此种类型的对象。
5.2对象属性的访问
对象属性的访问有两种形式,一种是.操作符,一种是[]操作符。.操作符要求属性名必须满足标识符的命名规范,而[]操作符就没有这种限制。此外,由于[]操作符使用字符串来访问属性,所以可以在程序中构造这个字符串。
var obj = {
a:123
}
var idx = 'a'
console.log(obj[idx])//123
属性名永远是字符串,如果使用其他基本类型的值作为属性名,这些值都会被转为字符串。示例代码如下:
var obj = {
a:123
}
obj[true] = true
console.log(obj.true,obj['true'])
obj[520] = 'I Love You'
console.log(obj['520'])
5.3属性描述符
在ES5之前JS语言本身并没有提供可以直接检测属性特征的方法,比如判断属性是否是只读。但是从ES5开始所有的属性都有了属性描述符。
var obj = {
a:2
}
console.log(Object.getOwnPropertyDescriptor(obj,'a'))
//{ value: 2, writable: true, enumerable: true, configurable: true }
Object.defineProperty(obj,'a',{
writable:false,//是否是只读类型
enumerable:true,//是否可以枚举
configurable:true//是否可以再次调用defineProperty进行配置
})
obj.a = 1
console.log(obj.a)//2
Object.defineProperty(obj,'a',{
enumerable:false
})
console.log(Object.getOwnPropertyDescriptor(obj,'a'))
console.log(obj)//{} enumerable设置为false之后a这个属性不再出现
Object.defineProperty(obj,'a',{
enumerable:true
})
console.log(obj)//{ a: 2 }
Object.defineProperty(obj,'a',{
configurable:false
})
Object.defineProperty(obj,'a',{
configurable:true
})//TypeError: Cannot redefine property: a
console.log(Object.getOwnPropertyDescriptor(obj,'a'))
了解了这些,我们就可以更加自由的控制自己的对象,比如为对象创建常量属性,或者禁止再次配置这个对象。
5.4Getter和Setter
访问描述符:当你给一个属性定义getter和setter或者两者都有时,这个属性会被定义为‘访问描述符’。当我们调用obj.a的时候,其实是调用了obj对象的默认的[[Get]]操作。当设置一个属性的值的时候首先调用[[Set]]。
var obj1 = {
_a_:10,
get a(){
return this._a_
},
set a(val){
this._a_ = val*2
}
}
obj1.a = 12
console.log(obj1.a)//24
5.5遍历
对于数组的遍历,使用for···in循环的时候遍历的是可枚举的属性,并不是属性的值。我们可以用forEach来遍历数组直接拿到数组的值,在ES6中新增了一种可以直接拿到数组属性值的方法,就是for···of。
示例代码如下:
var arr=[7,99,0,12,78]
arr.forEach(function (val,key){
console.log(val,key)
})
console.log('-------')
for(var i of arr){
console.log(i)
}
console.log('-------')
for(var i in arr){
console.log(i)
}
6.原型
6.1Object.create()时什么?怎么将一个对象作为另一个对象的原型
var obj1 = {
a:10
}
var obj2 = Object.create(obj1)
console.log(obj2)//{}
console.log(obj2.a)//10
这段代码将obj1当做了obj2的原型,虽然obj2是空的对象,但是访问a属性的时候,依旧可以沿着原型链在obj1中找到a。
使用for···in循环遍历对象的原理和查找原型链类似,任何可以通过原型链访问到的属性都会被枚举。使用in操作符来检查属性时,同样会查找属性的整条原型链。
//虽然a不是obj2本身的属性,但是for···in遍历的时候依旧可以访问到a
var obj1 = {
a:10
}
var obj2 = Object.create(obj1)
obj2.name = 'zhaohe'
obj2.age = 19
for(var i in obj2){
console.log(i)
}
6.2原型的顶层是什么,为啥输出的时候是空的?
原型的顶层是Object.prototype,这是一个对象,但是不管我们是console.log还是for···in都会拿到一个空的结果,这是为什么呢,难道这个东西真的是空的吗?
答案是否定的,是因为Object.prototype里面的属性都是不可枚举的,所以我们无法通过遍历得到,但是下面的代码可以让你恍然大悟!
var res = Object.getOwnPropertyNames(Object.prototype)//拿到所有的属性名
var res2 = Object.getOwnPropertyDescriptor(Object.prototype,'valueOf')//拿到valueOf这个属性的访问描述符
Object.defineProperty(Object.prototype,'valueOf',{
enumerable:true
})//修改这个属性的可枚举类型
console.log(Object.prototype)//可以枚举了
6.3obj.a是怎么工作的?
当我们要拿到一个对象的属性并把这个属性输出的时候,大致的过程是怎样的呢?比如obj.a我们会调用[[Get]]在当前的对象中查找,如果找不多就在这个对象的原型链中查找,直到找到为止,如果最终也没有找到就返回undefined。当我们在使用obj.a = 10的时候由发生了什么。首先在当前的对象中查找是否有这个变量a,如果有这个变量,而且这个变量有set,那么就调用这个变量的set,如果这个变量没有set,并且是writable的,就把等号右边的值,设置为这个属性的值。如果当前对象就没有这个变量a,我们就会沿着原型链查找,如果原型链中有一个属性为a,并且是writable的,就在obj中创建一个属性a,把它的值设为10,如果原型链中的属性是非writable的,那么这个obj.a = 10赋值是无效的,也不会在obj中创建a。如果原型链中也没有一个名字为a的属性,如果有名字为a的set就调用这个set,也不会在obj中创建a,也不会修改这个set,如果原型链中没有set,就在obj中创建一个a,把它的值设为10。
6.4“类”函数
函数有一种特殊的特性:所有的函数默认都会有一个名为prototype的公有的不可枚举的属性,它会指向一个对象。这个对象就被称为此函数的原型。
“继承”以为着复制操作,JS并默认并不会复制对象属性。相反,JS会在两个函数之间创建一个关联。
function Foo(){
console.log('123')
}
var a = new Foo()
console.log(a.constructor === Foo)//true a并没有constructor,constructor只是Foo原型的属性
console.log(Foo.prototype.constructor === Foo)//true
上面的代码new Foo()看起来像是C++中的构造函数,但是JS中没有所谓的构造函数,Foo()函数就是一个普通的函数,在这里只是通过new来调用了,注意这里是调用了Foo函数,然后返回一个对象,仅此而已。
6.4JS中的继承是什么?
JS中的继承就是讲两个函数关联起来。不是C++中的继承。
6.5隐式原型和显示原型
function Fun(a,b){
console.log(a+b)
}
function Fun2(){
}
var fn = new Fun(1,2)
console.log(Fun.prototype.isPrototypeOf(Fun))//false
console.log(Fun.prototype.isPrototypeOf(fn))//true
console.log(fn.__proto__ === Fun.prototype)
console.log(Object.getPrototypeOf(fn)===Fun.prototype)//true
console.log(Object.getPrototypeOf(Fun)===Fun.__proto__)//true
console.log(Fun.__proto__===Fun2.__proto__)//true
console.log(Fun.__proto__===Function.prototype)//true
7.行为委托
JS中的原型链的这个机制本质就是对象之间的关联关系。为了更好地学习如何直观地使用原型,我们必须认识到它代表的是一种不同于类的设计模式。