第一章:关于this
为什么要用this
function identify(){
return this.name.toUpperCase()
}
function speak(){
var greeting = "hello " + identify.call(this)
console.log(greeting)
}
var me = {
name:"Kyle"
}
var you = {
name:"Reader"
}
console.log(identify.call(me)) //KYLE
console.log(identify.call(you)) //READER
speak.call(me) //hello KYLE
speak.call(you) //hello READER
隐式“传递”一个对象引用
误解
1.误解一:指向自身
问题:**
function foo(num) {
console.log("foo" + num)
//记录foo被调用的次数
this.count++
}
foo.count = 0
var i;
for (i = 0; i < 10; i++) {
if (i > 5) {
foo(i)
}
}
console.log(foo.count) //0
解决 => 创建另一个带有count属性的对象**
function foo(num){
console.log("foo" + num)
//记录foo被调用的次数
// this.count++
data.count++
}
// foo.count = 0
var data = {
count:0
}
var i;
for(i=0;i<10;i++){
if(i>5){
foo(i)
}
}
console.log(data.count) //4
使用foo标识符替代this来引用函数对象,是 依赖于变量的foo的词法作用域
function foo(num) {
console.log("foo" + num)
//记录foo被调用的次数
foo.count++
}
foo.count = 0
var i;
for (i = 0; i < 10; i++) {
if (i > 5) {
foo(i)
}
}
console.log(foo.count) //4
=> 真正的解决问题(发现问题而不是换一种方法)
强制this指向foo函数对象**
(?疑问 那么那个this到底指向谁 0 是哪里来的)
function foo(num) {
console.log("foo" + num)
//记录foo被调用的次数
//注意 在当前的调用方式下,this确实指向foo
this.count++
}
foo.count = 0
var i;
for (i = 0; i < 10; i++) {
if (i > 5) {
//使用call(..)可以确保this指向函数对象foo本身
foo.call(foo,i)
}
}
console.log(foo.count)
**
如果从函数对象内部引用它自身,那只使用this是不够的,一般来说你需要通过一个指向函数对象的词法标识符(变量)来引用它.
这是具名函数,在它的内部可以使用foo来引用自身
function foo(){
foo.count = 4 //foo 指向它自身
}
传入setTimeout(...)回调函数没有名称标识符(这种函数被称为匿名函数),因此无法从函数内部引用自身
setTimeout(function(){
//匿名函数无法指向自身
},10)
**误解二:它的作用域**
明确:this在任何情况下都不指向函数的词法作用域
错误实例:this不可以隐式用用函数的词法作用域
function foo(){
var a = 2
this.bar()
}
function bar(){
console.log(this.a)
}
foo()
this到底是什么
this实际上是在函数被调用时发生的绑定,它指向完全取决于函数在哪里被调用。
this是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
第二章 this全面解析
调用位置
调用位置:就是函数在代码中被调用的位置(而不是声明的位置)。
分析调用栈(就是为了到达当前执行位置所调用的所有函数)
绑定规则
1.默认绑定
函数调用类型:独立函数调用。
**
在代码中,foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定。因此this指向全局对象。
function foo(){
console.log(this.a)
}
var a =2;
foo()
2.隐式绑定
当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象
需要考虑调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含。
调用位置会使用obj上下文来引用函数,因此可以说函数被调用时obj对象“拥有”或“包含”它
function foo(){
console.log(this.a)
}
var obj = {
a:2,
foo:foo
}
obj.foo() //2
对象属性引用链只有最后一层会影响调用位置
function foo(){
console.log(this.a)
}
var obj2 = {
a:42,
foo:foo
}
var obj1 = {
a:2,
obj2:obj2
}
obj1.obj2.foo() //42
隐式丢失
被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把this绑定到全局对象或者undefined上,取决于是否是严格模式。
原理解析:这里他指向全局是因为引用类型的问题
虽然bar是obj.foo一个引用,但是实际上,它引用的是foo函数本身,因此此时的bar()其实是一个不带任何修饰的函数调用,因此应用了默认绑定
function foo(){
console.log(this.a)
}
var obj = {
a:2,
foo:foo
}
var bar = obj.foo //函数别名
var a = "oops,global" //a是全局对象的属性
bar() //oops,global
- 另一种情况:传入回调函数时
参数传递其实就是一种隐式赋值。
function foo(){
console.log(this.a)
}
function doFoo(fn){
//fn其实引用的是foo
fn() //调用位置
}
var obj = {
a:2,
foo:foo
}
var a = "oops,global"
doFoo(obj.foo) //oops,global
- 如果把函数传入语言内置的函数而不是传入你自己声明的函数
function foo(){
console.log(this.a)
}
var obj = {
a:2,
foo:foo
}
var a = "oops,global"
setTimeout(obj.foo,100)
延伸:setTimeout()函数实现伪代码
function setTimeout(fn,delay){
//等待 delay 毫秒
fn() // 调用位置
}
3.显示绑定
call(..) 和 apply(..),他们的第一个参数是一个对象,它们会把这个对象绑定到this,接着在调试函数时指定这个this,因为你可以直接指定this的绑定对象,因此我们称之为显示绑定
function foo(){
console.log(this.a)
}
var obj = {
a:2
}
foo.call(obj) //2
1.硬绑定
我们创建了函数bar()并在它的内部手动调用了foo.call(obj),因此强制把foo的this绑定到了obj,无论之后如何调用函数bar,它总会手动在obj上调用foo,这种绑定是一种显示的强制绑定,因此我们称之为硬绑定。
function foo(){
console.log(this.a)
}
var obj = {
a:2
}
var bar = function(){
foo.call(obj)
}
bar() //2
setTimeout(bar,100) //2
//硬绑定的bar不可能再修改它的this
bar.call(window) //2
应用场景一:创建一个包裹函数,传入所有的参数并返回接收到的所有值
function foo(something){
console.log(this.a,something)
return this.a + something
}
var obj = {
a:2
}
var bar = function(){
return foo.apply(obj,arguments)
}
var b = bar(3) //2 3
console.log(b) //5
应用场景二:创建一个i可以重复使用的辅助函数
function foo(something){
console.log(this.a,something)
return this.a + something
}
//简单的辅助绑定函数
function bind(fn,obj){
return function(){
return fn.apply(obj,arguments)
}
}
var obj = {
a:2
}
var bar = bind(foo,obj)
var b = bar(3)
console.log(b)
ES5中提供了内置的方法 Function.prototype.bind
bind(..)会返回一个硬编码的新函数,它会把参数设置为this的上下文并调用原始函数
function foo(something){
console.log(this.a,something)
return this.a + something
}
var obj = {
a:2
}
var bar = foo.bind(obj)
var b = bar(3)
console.log(b)
2.API调用的“上下文”
上下文(context)其作用和bind(..)一样,确保你的回调函数使用指定的this
比如 forEach()
4.new绑定
function foo(a){
this.a = a
}
var bar = new foo(2)
console.log(bar.a) //2
**
关于js中函数和对象的误解
在js中,构造函数只是一些使用new操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。
实际上它们甚至都不能说是一种特殊的函数类型,它们只是被new操作符调用的普通函数而已
举例:
所有的函数都可以用new来调用,这种函数调用被称为构造函数调用。
实际上并不存在所有的“构造函数”,只有对于函数的“构造调用”
使用new来调用函数:
1.创建(或者说构造)一个全新的对象
2.这个新对象会被执行[[原型]]连接
3.这个新对象会绑定到函数调用的this
4.如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象
优先级
暂时没有理解全部 先不写
判断this
绑定例外
1.被忽略的this
如果你把null或者undefined作为this的绑定对象传入call apply 或者bind,这些值在调用时会被忽略,实际应用的是默认绑定规则、
function foo(){
console.log(this.a)
}
var a = 2;
foo.call(null) //2
2.间接引用
在赋值时发生
function foo() {
console.log(this.a)
}
var a = 2;
var o ={
a:3,
foo:foo
};
var p = {
a:4
};
o.foo(); //3 这里必须写
(p.foo=o.foo)() //2
赋值表达式p.foo=o.foo的返回值是目标函数的引用,因此调用位置是foo()而不是p.foo()或者o.foo(),所以这里会应用默认绑定。
注意:对于默认绑定来说,决定this绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式,如果函数体处于严格模式,this会被绑定到undefined,否则this会被绑定到全局对象。
3.软绑定
硬绑定这种方式可以把this强制绑定到指定的对象(除了使用new时)防止函数调用应该默认绑定规则,但是会降低函数的灵活性,使用硬绑定之后就无法使用隐式绑定或显示绑定来修改this
4.this词法
箭头函数不使用this的四种标准规则,而是根据外层(函数或者全局)作用域来决定this
foo()内部创建的箭头函数会捕获调用时foo()的this,由于foo()的this绑定到obj1,bar(引用箭头函数)的this也会绑定到obj1,箭头函数的绑定无法被修改(new也不行)
function foo(){
//返回一个箭头函数
return (a)=>{
//this继承来自 foo()
console.log(this.a)
}
}
var obj1 = {
a:2
}
var obj2 = {
a:3
}
var bar = foo.call(obj1)
bar.call(obj2) //2
箭头函数最长用于回调函数中,例如事件处理器或定时器
function foo(){
setTimeout(()=>{
//这里的this在词法上继承自foo()
console.log(this.a)
},100)
}
var obj = {
a:2
}
foo.call(obj) //2
function foo(){
var self = this
setTimeout(()=>{
console.log(self.a)
},100)
}
var obj = {
a:2
}
foo.call(obj) //2
疑问解答:this使用 var self = this 的原因
function foo(){
var self = this
setTimeout(function(){
console.log(self.a)
},100)
}
var obj = {
a:2
}
foo.call(obj) //2
首先看一个例子
在一个对象里面定义了一个普通函数(不是该对象的属性函数),为了能够在该普通函数里面访问到对象的属性,可以先把对象的this赋值给一个变量self,然后在该普通函数里通过self来获取到对象的属性。
这里理解: 函数也是一个对象,写在里面的b函数不是这个对象的属性函数,所以他需要访问a函数里面的作用域,这时候就需要将a的作用域保存然后给他,因为此时的this指向是不一样的。
写这个var self = this 就是为了在其他函数中访问到对象属性,
function a(){
var self = this;
this.name = "hello"
function b(){ //b只是一个普通函数,不是a的属性方法
console.log(self.name)
}
b()
}
new a() //hello
这里模拟一下 setTimeout 实现的伪代码
function setTimeout(fn,delay){
//等待 delay 毫秒
fn() // 调用位置
}
所以上面的代码就可以看成是对象中含有一个普通函数,不是foo的一个属性方法。
第三章 对象
语法
两种形式定义:声明和构造
推荐:文字语法
var myObj = {
key:value
}
构造形式
var myObj = new Object()
myObj.key = value
类型
简单基本类型 : string number boolean null undefined object
复杂基本类型:函数 数组
注明:null本身是基本类型。原理:不同的对象在底层表示为二进制,在js中二进制前三位都为0的话会被判断为object类型,null的二进制表示是全0,自然前三位也是0,所以执行typeof时会返回”object”
内置对象
String Number Boolean Object Function Array Date RegExp Error
这些内置函数可以当作构造函数(由new产生的函数调用)来使用,从而可以构造一个对应子类型的新对象。
instanceof 用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上
var strPrimitive = "i am a string"
typeof strPrimitive //"string"
strPrimitive instanceof String //false
var strObject = new String("i am a string")
typeof strObject //"object"
strObject instanceof String //true
下面的两种方法我们都可以直接在字符串字面量上访问属性或方法,之所以可以这样做,是因为引擎自动把字面量转换成String对象,所以可以访问属性和方法
var strPrimitive = "i am a string"
console.log(strPrimitive.length) //13
console.log(strPrimitive.charAt(3)) //m
理解:直接在字符串上面访问属性或者方法,其实是经过了一层转换,转换为其对应的构造形式。其他类似数值,布尔字面量也是如此。
对于Object Array Function RegExp 来说,无论使用文字形式还是构造形式,它们都是对象,不是字面量。
内容
对象属性的两种访问方式
- .a 属性访问
- [a] 键访问
区别所在:
- . 只可以访问规范的标识符
- [] 任意字符串
访问对象属性的那个值(属性名)一定是字符串。哪怕不是也会进行转换。
可计算属性名
ES6增加了可计算属性名,可以在文字形式中使用 [] 包裹一个表达式来当作属性名
var prefix = "foo"
var myObject = {
[prefix + "bar"]:"hello",
[prefix + "baz"]:"world"
}
myObject["foobar"] //hello
属性与方法
对象中的函数究竟是属性访问还是方法访问?
有些函数具有this引用,有时候这些this确实会指向调用位置的对象引用,但是这种用法从本质上并没有把一个函数变成一个方法,因为this始在运行时根据调用位置动态绑定的,所以函数和对象的关系只能说是间接关系。
即便在对象的文字形式中声明一个函数表达式,这个函数也不会属于这个对象,它们只是对于相同函数对象的多个引用。
var myObject = {
foo:function(){
console.log("foo")
}
}
var someFoo = myObject.foo
someFoo; //function foo(){...}
myObject.foo; //function foo(){...}
数组
数组期望的是数值下标,也就是说值存储的位置,通常被称为索引。
数组也是一个对象。所以就可以给数组添加属性。
注:虽然添加了命名属性,但是数组的length并没有发生变化
var myArray = ["foo",42,"bar"]
myArray.baz = "baz"
myArray.length //3
myArray.baz //baz
向一个数组添加一个属性,但是属性名“看起来”像一个数字,那么它会变成一个数值下标。=>(因此会修改数组的内容而不是添加一个属性)
var myArray = ["foo",42,"bar"]
myArray["3"] = "baz"
myArray.length //4
myArray[3] //baz
复制对象
- 浅拷贝
- ES6 Object.assign()
定义:Object.assign() 方法第一个参数是目标对象,之后还可以跟一个或多个源对象。
它会遍历一个或多个源对象的所有可枚举(enumerable)的自有键(owendkey)并把它们复制(使用=操作符赋值)到目标对象,最后返回目标对象。
- 深拷贝
只适用部分情况
var newObj = JSON.parse(JSON.stringify(someObj))
属性描述符
**
var myObject = {
a:2
}
Object.getOwnPropertyDescriptor(myObject,"a")
//configurable: true
//enumerable: true
//value: 2
//writable: true
分析:这个普通的对象属性对应的属性描述符不仅仅是一个2
writeable(可写) enumerable(可枚举) configurable(可配置)
在创建普通属性时属性描述符会使用默认值,我们也可以使用 Object.defineProperty()来添加一个新属性或者修改一个已有属性(如果他是configurable)并对特性进行设置。
var myObject = {}
Object.defineProperty(myObject,"a",{
value:2,
writable:true,
configurable:true,
enumerable:true
})
myObject.a //2
如果单单只设置一个value,其他的值默认会是false
1.writable
writable 决定是否可以修改属性的值
var myObject = {}
Object.defineProperty(myObject,"a",{
value:2,
writable:false,
configurable:true,
enumerable:true
})
myObject.a = 3
myObject.a //2
2.Configurable
只要属性是可配置的,就可以使用defineProperty() 方法来修改属性描述符
把configurable 修改成false是单向操作,无法撤销。
除了无法修改,configurable:false 还会禁止删除这个属性
注意:delete方法:delete只用来直接删除对象的(可删除)属性,如果对象的某个属性是某个对象/函数的最后一个引用者,对这个属性执行delete操作之后,这个未引用的对象/函数就可以被垃圾回收。但是他不是释放内存的工具,只是一个删除对象属性的操作而已。
3.Enumerable
这个描述符控制的是属性是否会出现在对象的属性枚举中,比如说for..in循环,如果把enumerable设置成false,这个属性就不会出现在枚举中,虽然仍然可以正常访问。相对的,设置成true就会让他出现在枚举中
不变性
希望属性或者对象是不可变的。
1.对象常量
结合writable:false 和 configurable:false 就可以创建一个真正的常量属性(不可修改)
var myObject = {}
Object.defineProperty(myObject,"favorite",{
value:42,
writable:false,
configurable:false
})
2.禁止拓展
如果你想禁止一个对象添加新属性并且保留已有属性,可以使用Object.preventExtensions
var myObject = {
a:2
}
Object.preventExtensions(myObject)
myObject.b = 3
myObject.b //undefiend
(重要)[[Get]]
var myObject = {
a:2
}
myObject.a //2
**
分析:在语言规范中,myObject.a在myObject上实际上是实现了[[Get]]操作,(有点像函数调用:[Get])
对象默认的内置[[Get]]操作首先在对象中查找是否有名称相同的属性,如果找到就会返回这个属性的值。
如果没有找到名称相同的属性,按照[[Get]]算法的定义会执行另外一种非常重要的行为,就是遍历可能存在的
[[Prototype]]链,也就是原型链。
如果无论如何都没有找到名称相同的属性,那么[[Get]]操作会返回值undefined
var myObject = {
a:2
}
myObject.b //2
/?不是很理解
注意:这种方法和访问变量时时不一样的,如果你引用了一个当前词法作用域中不存在的变量,并不会像对象属性一样返回undefined,而是会抛出一个ReferenceError异常
var myObject = {
a:undefined
}
myObject.a //undefined
myObject.b //undefined
从返回值的角度来说,这两个引用没有区别,然而实际上底层的[[Get]]操作对myObject.b进行了更复杂的处理
由于仅根据返回值无法判断出到底变量的值为undefined还是变量不存在,所以[[Get]]操作返回了undefined.
[[Put]]
Getter 和 Setter
只能应用在单个属性上,无法应用在整个对象上,getter是一个隐藏函数,会在获取属性值时调用,setter也是一个隐藏函数,会在设置属性值时调用。
**
当你给一个属性定义getter setter或者两者都有时,这个属性会被定义为“访问描述属性”,对于访问属性来说,js会忽略它们的value和writable特性,取而代之的是关心set和get特性
var myObject = {
get a() {
return 2
}
}
Object.defineProperty(
myObject, "b", {
get: function () {
return this.a * 2
},
//确保b会出现在对象的属性列表中
enumerable: true
}
)
myObject.a //2
myObject.b //4
上述代码:不管是对象语法中的get a(){},还是defineProperty()中的显示定义,二者都会在对象中创建一个不包含值的属性,对于这个属性的访问会自动调用一个隐藏函数,它的返回值会被当做属性访问的返回值。
var myObject ={
get a(){
return 2
}
}
myObject.a = 3
myObject.a //2
由于我们只定义了a的getter,所以对a的值进行设置时set操作会忽略赋值操作,不会抛出错误。
即便有合理的setter,由于我们定义的getter只会返回2,所以set操作是没有意义的。
理解:这儿的意思是返回的是一个固定值。所以你设置了也没用,下面的return的是一个变量
为了让属性更合理,还应当定义setter,setter会覆盖单个属性默认的[Put]操作。
var myObject = {
get a(){
return this._a_
},
set a(val){
this._a_ = val*2
}
}
myObject.a = 2
myObject.a //4
存在性
var myObject = {
a:2
}
// ("a" in myObject) //true
// ("b" in myObject) //false
myObject.hasOwnProperty("a") //true
myObject.hasOwnProperty("b") //false
Object.prototype.hasOwnProperty.call(myObject,”a”)
借用基础的 hasOwnProperty(…)方法并把它显示绑定在myObject上
注意:
实际上in 检查的是某个属性名是否存在,对于数组来说这个区别十分重要。
console.log(4 in [2,4,6]) //false
1.枚举
var myObject = {}
Object.defineProperty(
myObject,
"a",
//让a像普通属性一样可以枚举
{enumerable:true,value:2}
)
Object.defineProperty(
myObject,
"b",
//让b不可枚举
{enumerable:false,value:3}
)
console.log(myObject.b) //3
console.log("b" in myObject) //true
myObject.hasOwnProperty("b") //true
for(var k in myObject){
console.log(k,myObject[k])
}
// "a" 2
“可枚举”就相当于”可以出现在对象属性的遍历中”
提示:最好在对象上应用for…in 循环。
另一种方法
遍历
for…in 循环可以用来遍历对象的可枚举属性列表(包括[[Prototype]]链)
for…in 遍历对象是无法直接获取属性值的,因为他实际上遍历的是对象中的所有可枚举属性,你需要手动获取属性值。
for…of 可以直接遍历值而不是数组下标(或者对象属性),如果对象本身定义了迭代器的话也可以遍历对象。
for…of 循环首先会向被访问对象请求一个迭代对象,然后通过调用迭代器对象的next()方法来遍历所有值。
第四章:混合对象“类”
类理论
类、继承描述了一种代码的组织结构。
类继承和实例化 。比如说汽车工厂就是一个类,可以生产汽车,所以在定义car时,只要声明它继承了汽车这个类。
而实例就是生产出来的汽车,每台汽车都有自己的编号,这是不一样的。
类理论强烈建议父类和子类使用相同的方法名来表示特定的行为,从而让子类重写父类。
“类”设计模式
javascript中的类
js中有一些近似类的语法元素(new,instanceof)不过在后来的ES6中新增了一些元素,比如class关键字、
但这不是类
类的机制
类的继承
第五章:原型
prototype
prototype 引用有什么用?
当试图引用对象的属性时会触发[[Get]]操作,比如myObject.a。对于默认的[[Get]]操作来说,
- 第一步检查对象本身是否有这个属性,如果有的话就使用它。
- 如果a不在myObject中,就需要使用对象的[[prototype]]链
var anotherObject = {a:2}
//创建一个关联到 anotherObject 的对象
var myObject = Object.create(anotherObject)
//它会创建一个对象并把这个对象的[[prototype]]关联到指定的对象
Object.prototype
所有普通的[[prototype]]链最终都会指向内置的Object.prototype
比如 .toString() .valueOf()
Object.prototype.valueOf() valueOf() 方法返回指定对象的原始值。
var arr = [1,2,3]
arr.valueOf() === arr //true 注意返回的是原始值
属性的设置和屏蔽
myObject.foo = 'bar'
1.对象中含有名为foo的属性:只会修改已有的属性值
2.不是直接存在于对象中,[[prototype]]链会被遍历,类似[[Get]]操作,如果原型链上找不到foo,就会被直接添加到myObject上。
3.如果属性名既存在myObject中也出现在myObject的[[prototype]]链上层,那么就会发生屏蔽,屏蔽的是原型链的属性。
如果foo不直接存在于myObject中而是存在于原型链上层时,会出现的三种情况:
“类”
javascript 和面向类的语言不同,它并没有类来作为对象的抽象模式,javascript中只有对象。
“类函数”
“类似类”:所有的函数默认都会拥有一个名为prototype的公有并且不可枚举的属性,它会指向另一个对象
function Foo(){
//...
}
Foo.prototype; //{}
这个对象通常被称为Foo的原型,因为我们通过名为Foo.prototype的属性引用来访问它
解释:这个对象是在调用new Foo()时创建的,最后会被关联到这个“Foo.prototype”对象上
function Foo(){
}
var a = new Foo()
Object.getPrototypeOf(a) === Foo.prototype; //true