你不知道的JavaScript读书笔记(this与对象原型)

111 阅读22分钟

第一章 关于this

function identify(){
  return this.name.toUpperCase(); //对输入的对象的name属性进行大写操作
}
function speak(){
    console.log(this)
  var greeting = "Hello,I'm " + identify.call(this);
  console.log(greeting);
}

var me = {
  name : "Kyle"
}

var you = {
  name: "Jack"
}

identify.call(me); //表示输入参数me KEYLE
identify.call(you);//表示输入参数you JACK
speak.call(me);//"Hello,I'm KYLE"
speak.call(you);//"Hello,I'm JACK"

如果不使用this,需要显式传入一个上下文对象context,但是这种的话,会让代码变得越来越乱

function identify(context){
  return context.name.toUpperCase(); //对输入的对象的name属性进行大写操作
}
function speak(context){
  var greeting = "Hello,I'm " + identify(context);
  console.log(greeting);
}

var me = {
  name : "Kyle"
}

var you = {
  name: "Jack"
}

identify(you)
speak(me)  //"Hello,I'm KYLE"

误解

不是指向自身

function foo(num){
  console.log("foo:" + num)
  console.log(this.count); //undefined,表示这里的count没有被定义到
  //记录foo被调用的次数
  this.count++
}
foo.count = 0
var i
for(i = 0;i< 10;i++){
  if(i > 5){
    //foo(i);
    foo.call(foo,i);//使用call(..)可以确保this指向函数对象foo本省
  }
}
console.log(foo.count)

精华错误

function foo(){
  var a = 2;
  this.bar();
}
function bar(){
  console.log(this.a);
}
foo();//undefined

this到底是什么

当一个函数被调用时,会创建一个活动记录。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方式、传入的参数等信息。this就是这个记录的一个属性,会在函数执行的过程中使用。

小结

this实际上就是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。

第二章 this全面解析

调用位置

绑定规则

默认绑定

function foo(){
  console.log(this.a)
}
var a = 2;
foo(); //2

默认绑定到全局变量中,所以这里的foo()函数中的this.a,指向的上一层(全局的变量)

在代码中,foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应用其他规则。

function foo(){
  "use strict";
  console.log(this.a);//"TypeError: Cannot read properties of undefined (reading 'a')
}
var a = 2;
foo(); 

使用严格模式,就不能将全局变量用于绑定;在严格模式下调用foo()则不会影响默认绑定

隐式绑定

function foo(){
  console.log(this.a);
}
var obj = {
  a:2,
  foo:foo
}
obj.foo(); //2

调用对象的时候,会使用obj上下文来引用函数,因此可以说函数被调用时obj对象“拥有”或者“包含”函数引用

function foo(){
  console.log(this.a);
}
var obj2 = {
  a:2,
  foo:foo
}

var obj1 = {
  a:1,
  obj2:obj2
}

obj1.obj2.foo();//2

对象属性引用链中只有上一层或者说最后一层在调用位置起作用

隐式丢失

function foo(){
  console.log(this.a);
}
var obj = {
  a:2,
  foo:foo
}


var bar = obj.foo;//函数别名
var a = "oops,global";//a是全局变量的属性
bar();//"oops,global"

这里的barobj.foo的一个引用,它引用的是foo函数本身,因此此时的bar其实是一个不带任何修饰的函数调用,因此使用了默认绑定。

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); //'oops,global'

显式绑定

function foo(){
  console.log(this.a);
}


var obj = {
  a:2
}

foo.call(obj); //可以在调用foo时强制把它的this绑定到obj上

在执行函数的时候,会将foo的this绑定到obj上也就是对于foo函数中的this.a表示的是obj中的a

1.硬绑定

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

无论之后如何调用函数bar,他总是会手动在obj上调用foo.这种绑定是一种显式的强制绑定,因此我们称之为硬绑定。

也就是用函数来封装已经绑定好的函数。

应用场景:创建一个可以重复使用的辅助函数:

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);//这里的参数是3 打印的结果是 2 3
console.log(b);//5

创建一个可以重复使用的辅助函数:

function foo(something){
  console.log(this.a,something)
  return this.a + something
}

var obj = {
  a:2
}

function bind(fn,obj){
  return function(){
     return fn.apply(obj,arguments);
  }
}

var bar = bind(foo,obj); //将函数和对象绑定在一起
var b = bar(3);//2 3
console.log(b);//5

ES5提供了内置的方法Function.prototype.bind,fn.bind(obj),将函数fn和对象obj绑定在一起。

2.API调用的“上下文”

function foo(el){
  console.log(el,this.id)
}

var obj = {
  id:"awesome"
};

//调用foo(...)把this绑定到obj
[1,2,3].forEach(foo,obj);
//1 awesome 2 awesome 3 awesome

比如call(...)或者apply(...)实现了显式绑定

new绑定

使用new来调用函数,

  1. 创建一个全新的对象
  2. 这个新对象会执行[[Prototype]]连接
  1. 这个新对象会绑定到函数调用的this
  2. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象
function foo(a){
  this.a = a
}
//自己的this
foo(3)
var bar = new foo(2)
console.log(bar.a);//2
console.log(a);//3

如果使用new来调用foo(...)时,我们会构造一个新对象并把它绑定到foo(...),调用中的this

比如上面的代码片段,如果没有使用new的话,输出的结果是3,也就是默认绑定,绑定到全局变量上

优先级

function foo(){
  console.log(this.a)
}
var obj1 = {
  a:2,
  foo:foo
}
var obj2 = {
  a:3,
  foo:foo
}
//隐式绑定
obj1.foo();//2
obj2.foo();//3
//显示绑定
obj1.foo.call(obj2);//3
obj2.foo.call(obj1);//2

可以看出显式绑定优先级更高,也就是判断的时候,可以优先考虑显式绑定

function foo(something){
  this.a = something;
}
var obj1 = {
  foo:foo
}

var obj2 = {}

//隐式绑定
obj1.foo(2)
console.log(obj1.a); //2
//显示绑定
obj1.foo.call(obj2,3);
console.log(obj2.a);//3
//new 绑定
var bar = new obj1.foo(4) //隐式绑定和new绑定进行比较
console.log(obj1.a);//2
console.log(bar.a);//4

这里的话可以看到new绑定比隐式绑定优先级高

function foo(something){
  this.a = something;
}
var obj1 = {}

//硬绑定,就是显式绑定
var bar = foo.bind(obj1);
bar(2);
console.log(obj1.a);   //2
//new绑定+显式绑定的比较
var baz = new bar(3);
console.log(obj1.a);   //2
console.log(baz.a);    //3

new绑定优先级大于显式绑定

判断this

  1. 函数是否在new中调用?如果是的话,this绑定的是新创建的对象

var bar = new foo();

  1. 函数是否通过callapply(显式绑定)或者硬绑定调用,如果是的话,this绑定的是指定的对象

var bar = foo.call(obj2);

  1. 函数是否在某上下文对象中调用(隐式绑定)?如果的是,this绑定的是那个上下文对象

var bar = obj1.foo();

  1. 如果都不是的话,使用默认绑定,如果在严格模式下,绑定到undefined,否则绑定到全局对象。

var bar = foo();

\

绑定例外

被忽视的this

function foo(){
  console.log(this.a)
}
var a = 2;
foo.call(null);//2

null或者undefined作为this的绑定对象传入callapply或者bind,这些值在调用时会被忽视,实际应用的是默认绑定规则

function foo(a,b){
  console.log("a:"+a+",b:"+b)
}
//把数组“展开”成参数
foo.apply(null,[2,3]);//a:2,b:3
//使用bind(...)进行柯里化
var bar = foo.bind(null,2)
bar(3);//a:2,b:3

间接引用

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(),这里应用默认绑定。

\

软绑定

function foo(){
  console.log("name: "+this.name)
}
var obj ={name:"obj"},
    obj2 = {name:"obj2"},
    obj3 = {name:"obj3"};

var fooOBJ = foo.softBind(obj);

fooObJ();//name:obj

obj2.foo = foo.softBind(obj);
obj2.foo();//name: obj2 《-----看!

fooOBJ.call(obj3);//name:obj3

setTimeout(obj2.foo,10);
//name:obj

软绑定版本的foo()可以手动将this绑定到obj2.或者obj3

this词法

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,不是3!

foo()内部创建的箭头函数会捕获调用时foo()的this,由于foo()的this绑定到obj1,bar(引用箭头函数)的this也会绑定到obj1,箭头函数的绑定无法被修改。(new也不行)

箭头函数常常用在回调函数中,例如事件处理器或者定时器;

function foo(){
  setTimeout(()=>{
    //这里的this在词法上继承自foo()
    console.log(this.a)
  },100)
}
var obj = {
  a:2
}
foo.call(obj); //2

等价于

function foo(){
  var self = this
  setTimeout(function(){
    //这里的this在词法上继承自foo()
    console.log(self.a)
  },100)
}
var obj = {
  a:2
}
foo.call(obj); //2

ES6中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定this,具体来说,箭头函数会继承外城函数调用的this来绑定

第三章 对象

语法

声明(文字)形式

var myObj = {
  key:value
}

构造形式

var myObj = new Object();
myObj.key = value;

文字声明特点可以添加多个属性

类型

JavaScript有六种类型

  • number
  • string
  • boolean
  • null
  • undefined
  • object

内置对象

  • String
  • Number
  • Boolean
  • Object
  • Function
  • Array
  • Date
  • RegExp
  • Error
var strPrimitive = "I am a string";
console.log(typeof strPrimitive);  //"string"
console.log(strPrimitive instanceof String);//false

var strObject = new String("I am a string");
console.log(typeof strObject);//"object"
console.log(strObject instanceof String);//true

//检查sub-type对象
var a = Object.prototype.toString.call(strObject) //"[object String]"
console.log(a)

在上面的代码中var strPrimitive = "I am a string";不是一个对象,而是一个字面量,并且是不可以变的值。转化成String对象,才能够执行操作:获取长度、访问某个字符串

语言会自动把字符串字面量自动转化成String对象。

var strPrimitive = "I am a string";

console.log(strPrimitive.length);//13
console.log(strPrimitive[2]);//"a"
console.log(42.359.toFixed(2));//"42.36"

系统会自动将42转换成new Number(42),布尔值字面量来说也是如此

nullundefined没有对应的构造形式,只有文字形式。

Data只有构造没有文字形式

内容

对象的内容是由一些存储在特定命名位置的(任意类型的)值组成的,我们称之为属性。

var myObject= {
  a:2
};

console.log(myObject.a);//2
console.log(myObject[a]);//2

.a语法称为“属性访问”(通常使用)

[a]语法称为“键访问”,是通过字符串来进行访问的,语言会自动进行转化

var myObject = {
  a:2
};
var idx = "a";
console.log(myObject[idx]); //2
var myObject = {};

myObject[true] = "foo";	//布尔值也会进行自动转化
myObject[3] = "3oo";	//数字会自动进行转化
myObject[myObject] = "myoo"; //这里的话是对自身的对象进行再一次赋值
myObject[asdf] = "myoo"; //出错,因为无法识别到变量asdf
myObject["asdf"] = "myoo";//这样子的话,就可以


console.log(myObject["true"])
console.log(myObject["3"])
console.log(myObject[3])
console.log(myObject["[object Object]"])
console.log(myObject)


[object Object] {
  3: "3oo",
  [object Object]: "myoo",
  true: "foo"
}

可计算属性名

var prefix = "foo";

var myObject = {
  [prefix + "bar"]:"hello", //语言会自动进行转化,转化成"foobar"
  [prefix + "baz"]:"hello1"
}

console.log(myObject["foobar"])
console.log(myObject["foobaz"])

使用构造形式的话,会自动进行转化

属性与方法

属于对象(称为类)的函数通常被称为“方法”,因此把“属性访问”说成“方法访问”

function foo(){
  console.log("foo")
}

var someFoo = foo; //对foo的变量引用

var myObject = {
  someFoo:foo
}

console.log(foo);//function foo(){..}
console.log(someFoo);//function foo(){..}
console.log(myObject.someFoo);//function foo(){..}

函数和方法在JavaScript中可以互换

数组

赋值对象

function anotherFuncion(){}

var anotherObject = {
  c:true
}

var anotherArray = [];

var myObject = {
  a:2,
  b:anotherObject,//引用,不是复本
  c:anotherArray,//另外一个引用
  d:anotherFuncion
}
anotherArray.push(anotherObject,myObject)
console.log(myObject.b.c)
function anotherFuncion(){}

var anotherObject = {
  c:true
}

var anotherArray = [];


var myObject = {
  a:2,
  b:anotherObject2,//引用,不是复本
  c:anotherArray,//另外一个引用
  d:anotherFuncion
}

var anotherObject1 = myObject;//引用
var anotherObject2 = JSON.parse(JSON.stringify(myObject)); //深复制
var newObject = Object.assign({},myObject);//浅复制

console.log(newObject == myObject)

myObject.a = 3
console.log(anotherObject1); //里面的值改变了,a为3   引用
console.log(anotherObject2); //里面的值不变          深复制
console.log(newObject);      //里面的值不变          浅复制

需要使用JavaScript中的Object.assign(..)来进行浅复制,或者使用

JSOM.parse(JSON.stringify(someObj);

属性描述符

var myObject = {
  a:2
}

var res = Object.getOwnPropertyDescriptor(myObject,"a")
console.log(res)
// [object Object] {
//   configurable: true,
//   enumerable: true,
//   value: 2,
//   writable: true
// }

对于这个普通的对象属性的话,不仅仅只有一个数据值2,还有writable(可写)、enumberable(可枚举)、configurable(可配置)

可以使用Object.defineProperty(...)来添加一个

var myObject = {}

Object.defineProperty(myObject,"a",{
  value:2,
  writable:true,
  configurable:true,
  enumerable:true
})

console.log(myObject.a);//2

Writbale

决定是否可以修改属性的值

var myObject = {};
Object.defineProperty(myObject,"a",{
  value:2,
  writable:false, //不可写
  configurable:true,
  enumerable:true
})
myObject.a = 3;
print(myObject.a); //2




function print(a){
  console.log(a)
}

Configurable

var myObject = {
  a:2
}

myObject.a = 3;
console.log(myObject.a); //3

Object.defineProperty(myObject,"a",{
  value:4,
  writable:true,
  configurable: false, //不可配置!
  enumerable:   true
})

console.log(myObject.a); //4
myObject.a = 5;
console.log(myObject.a); //5

Object.defineProperty(myObject,"a",{
  value:6,
  writable:true,
  configurable:false,
  enumerable:true
});//"TypeError: Cannot redefine property: a

可以修改value,但是不能够修改其他的配置,也不能够进行删除,可以把writable的状态由true改为false,但是无法从false改为true.

delete 仅仅只能够删除对象属性

\

Enumerable

是否会出现在对象的属性枚举中

不变性

对象常量

var myObject = {};
Object.defineProperty(myObject,"FAVORITE_NUMBER",{
  value:42,
  writable:false,
  consifurable:false
}); //这个就是一个常量,不可以修改、重定义或者修改

禁止扩展

var myObject = {
  a:2
}
//禁止拓展
Object.preventExtensions(myObject);
myObject.b = 3;
console.log(myObject.b);//undefined

密封

Object.seal(..)会创建一个“密封”的对象,这个方法实际上会在一个现有对象上调用Object.preventExtensions(..),并把所有现有属性标记为configurable:false

冻结

Object.freeze(..)会创建一个冻结对象。无法修改他们的值

[[Get]]

var myObject = {
  a:2
};
myObject.a;//2

myObject.amyObject上实际上是实现了[[Get]]操作,首先在对象中查找是否有名称相同的属性,如果找就会返回这个属性的值。

[[Put]]

\

Getter和Setter

var myObject = {
  //给a定义一个getter
  get a(){
    return 2;
  }
}

Object.defineProperty(
  myObject, //目标对象
  "b",//属性名
  {
    //描述符
    //给b设置一个getter
    get:function(){return this.a*2},
    //确保b会出现在对象的属性列表中
    enumerable:true
  })

console.log(myObject.a);//2 对a进行get查询
console.log(myObject.b);//4 对b进行get查询

\

var myObject = {
  //给a定义一个getter
  get a(){
    return this._a_;
  },
  //给a定义一个setter
  set a(val){
    this._a_ = val*2
  }
}
myObject.a = 2;
console.log(myObject.a); //4 进行setter的时候令a翻倍

名称_a_只是一种惯例,没有任何特殊的行为——和其他普通属性一样。

存在性

var myObject = {
  a :2
};

console.log("a" in myObject); //true
console.log("b" in myObject); //false

console.log(myObject.hasOwnProperty("a")); //true
console.log(myObject.hasOwnProperty("b")); //false

in操作符会检查属性是否在对象机器[[Prototype]]原型链中。相比之下hasOwnProperty(..)只会检查属性是否在myObject对象中,不会检查[[Prototype]]原型链。

枚举

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
console.log(myObject.hasOwnProperty("b"));//true

//.....

for(var k in myObject){
  console.log(k,myObject[k]);//k表示属性,myObject[k]表示对应的值
}
//"a" 2

可以观察到b没有出现在 for .. in循环中,原因是“可枚举”相当于“可以出现在对象属性的遍历中”

var myObject = {};
Object.defineProperty(
  myObject,
  "a",
  //让a像普通属性一样可以枚举
  {enumerable:true,value:2}
);

Object.defineProperty(
  myObject,
  "b",
  //让b不可以枚举
  {enumerable:false,value:3}
)

console.log(myObject.propertyIsEnumerable("a"));//true
console.log(myObject.propertyIsEnumerable("b"));//false

console.log(Object.keys(myObject)); //["a"]
console.log(Object.getOwnPropertyNames(myObject)); // ["a","b"] 包含所有属性

propertyIsEnumerable(..)会检查给定的属性名是否直接存在于对象中,并且满足enumerable:true

遍历

var myArray = [1,2,3];

for (var v of myArray){
  console.log(v)
}
// 1
// 2
// 3

for..of循环首先会向被访问对象请求一个迭代器对象,然后通过调用迭代器对象的next()方法来遍历所有返回值.

var myArray = [1,2,3];
var it = myArray[Symbol.iterator]()

console.log(it.next());// {done:false,value:1}
console.log(it.next());// {done:false,value:2}
console.log(it.next());// {done:false,value:3}
console.log(it.next());// {done:true}  表示已经结束

使用ES6中的符号Symbol.iterator来获取对象的@@iterator内部属性.

var myObject = {
  a:2,
  b:3
}

Object.defineProperty(myObject,Symbol.iterator,{
  enumerable:false,
  writable:false,
  configurable:true,
  value:function(){
    var o = this;
    var idx = 0;
    var ks = Object.keys(o);//表示键值
    return {
      next:function(){
        return {
          value:o[ks[idx++]],
          done:(idx > ks.length)
        }
      }
    }
  }
});

//手动遍历myObject
var it = myObject[Symbol.iterator]();
console.log(it.next());//{value:2,done:false}
console.log(it.next());//{value:3,done:false}
console.log(it.next());//{value:undefined,done:true}

//用for..of遍历myObject
for(var v of myObject){
  console.log(v);
}
//2
//3

第四章 混合对象"类"

类理论

类包括数据和对数据的操作行为

"类"实际模式

JavaScript中的类

类的机制

建造

一个类就是一个蓝图,为了获得真正可以交互的对象,我们必须按照类来建造(也可以说实例化)一个东西,这个东西通常称为实例,有需要的话,我们可以直接在实例上调用方法并访问所有公有数据属性.

构造函数

类的继承

多态

多重继承

混入

显示混入

//非常简单的mixin(..)例子:
function mixin(sourceObj,targetObj){
  for(var key in sourceObj){
    //只会在不存在的情况下复制
    if(!(key in targeObj)){
      targeObj[key] = sourceObj[key]
    }
  }
}
var Vehicle = {
  engines:1,
  
  ignition:function(){
    console.log("Turning on my engine.")
  },
  
  drive:function(){
    this.ignition();
    console.log("Steering and moving forward!");
  }
}
​
var Car = mixin(Vehicle,{
  wheels:4,
  
  drive:function(){
    Vehicle.drive.call(this);
    console.log(
      "Rolling on all" + this.wheels + ' wheels!'
    )
  }
})
​
//Vehicle和Car都是对象

多态

Vehicle.drive.call(this)`这就是显式多台,`inherited:drive()

如果直接使用Wehicle.drive(),函数调用中的this会绑定到Vehicle对象而不是Car对象.

混合复制

//非常简单的mixin(..)例子:
function mixin(sourceObj,targetObj){
  for(var key in sourceObj){
    //只会在不存在的情况下复制
    if(!(key in targeObj)){
      targeObj[key] = sourceObj[key]
    }
  }
}

复制后进行使用不会相互影响

寄生继承

//传统的JavaScript类Vehicle
function Vehicle(){
  this.engines = 1;
}
​
Vehicle.prototype.ignition = function(){
  console.log("Turning on my engine")
}
​
Vehicle.prototype.drive = function(){
  this.ignition();
  console.log("Steering and moving forward!")
}
​
//寄生类Car
function Car(){
  //首先,car是一个Vehicle
  var car = new Vehicle();
  
  //接着我们对car进行定制
  car.wheels = 4;
  
  //保存到Vehicle::drive()的特殊引用
  var vehDrive = car.drive;
  
  //重写Vehicle::drive()
  car.drive = function(){
    vehDrive.call(this);
    console.log(
      "Rolling on all " + this.wheels + " Wheels!"
    )
  }
  return car
}
​
var myCar = new Car();
​
myCar.drive();

首先我们复制一份Cehicle弗雷的定义,然后混入子类的定义,然后用这个符合对象构建实例

隐式混入

var Something = {
  cool:function(){
    this.greeting = "Hello World";
    this.count = this.count?this.count + 1 : 1;
  }
}
Something.cool();
console.log(Something.greeting);//"Hello World"
console.log(Something.count);//1var Another = {
  cool:function(){
    //隐式把Something混入Another
    Something.cool.call(this);
  }
}
​
Another.cool();
console.log(Another.greeting);//"Hello World"
console.log(Another.count); //1 (count不是共享状态)

通过在构造函数调用或者方法调用中使用Something.cool.call(this),我们实际上借用了函数Something.cool()并在Another的上下文呢中调用了他,最终的记过是Something.cool()中的复制操作都会应用在Another对象上而不是Something对象上

小结

类意味着复制

类被继承时,行为也会被复制到子类中.

第五章 原型

[[Prototype]]

var anotherObject = {a : 2};
//创建一个关联到anotherObject的对象
var myObject = Object.create(anotherObject);
console.log(myObject.a);//2

现在myObject对象的[[Prototype]]关联到anotherObject,显然myObject.a并不存在,的那hi是属性访问仍能够找到2

var anotherObject = {a : 2};
//创建一个关联到anotherObject的对象
var myObject = Object.create(anotherObject);
​
for (var k in myObject){
  console.log("found: " + k);
}
​
//found:a
console.log("a" in myObject);//true

使用for..in遍历对象时原理和查找[[Prototype]]链类似,任何可以通过原型链访问到的属性都会被枚举,使用in操作符检查属性在对象中是否存在,同样会查找对象的整条原型链.

Object.prototype

所有普通的[[Prototype]]链最终都会指向内置的Object.prototype.

属性设置和屏蔽

myObject.foo = "bar

如果foo不是直接存在于myObject中,[[Prototype]]链就会被遍历,类似[[Get]]操作,如果原型链上找不到foo,就会被添加到myObject上

var anotherObject = {a : 2};
//创建一个关联到anotherObject的对象
var myObject = Object.create(anotherObject);
​
console.log(anotherObject.a);//2
console.log(myObject.a);//2console.log(anotherObject.hasOwnProperty("a"));//true
console.log(myObject.hasOwnProperty("a"));//false
​
myObject.a++;//隐式屏蔽console.log(anotherObject.a);//2
console.log(myObject.a);//3console.log(myObject.hasOwnProperty("a"));//true

myObject.a++是通过委托查找并增加anotherObject.a属性,++的操作首先会通过[[Prototype]]查找属性a,并从anotherObject.a获取当前的属性值2

我们是一个对象需要关联到另外一个对象

在JavaScript中,类无法描述对象的行为,因为根本就不存在类,对象直接定义自己的行为,JavaScript中只有对象.

类函数

在JavaScript中,没有类似的复制机制,不能创建一个类的多个实例,只能够创建多个对象,他们[[Prototype]]关联的是同一个对象

关于名称

“构造函数”

function Foo(){
  //...
}
var a = new Foo();

Foo是一个“类”

function Foo(){
  //...
}
​
Foo.prototype.constructor === Foo; //true
​
var a = new Foo();
a.constructor === Foo; //true

构造函数调用new Foo()创建的对象也有一个.constructor属性,指向“创建这个对象的函数”

实际上a本身并没有.constructor属性

类的名首字符要大些

1.构造函数还是调用

function NothingSpecial(){
  console.log("Don't mind me!")
}
var a = new NothingSpecial();//"Don't mind me!"console.log(a);//[object Object] { ... }

NothingSpecial只是一个普通的函数,但是使用new调用时候,他就会构造一个对象并赋值给a,这看起来像是new,这个调用是构造函数调用,但是NothingSpecial本身不是一个构造函数。

函数不是构造函数,使用new时,函数调用会变成“构造函数调用”

技术

function Foo(name){
  this.name = name
}
​
Foo.prototype.myName = function(){
  return this.name
}
​
var a = new Foo("a")
var b = new Foo("b")
​
console.log(a.myName());
console.log(b.myName());
// "a"
// "b"
  1. this.name = name 给每一个对象都添加了.name属性
  2. Foo.prototype.myName 是个有趣的技巧,它会给Foo.prototype 对象添加一个属性。

在创建的过程中,a和b的内部[[Prototype]]都会关联到Foo.prototype上,当a和b无法找到myname时,他会通过委托在Foo.prototype上找到

function Foo(){/*...*/}
Foo.prototype = {/*...*/}; //创建一个新原型对象
var a1 = new Foo();
console.log(a1.constructor === Foo); //constructor 表示由......构造,说明是由对象构造的
console.log(a1.constructor === Object);
// false
// true

a1并没有.constructor属性,所以他会委托[[Prototype]]链上的Foo.prototype,但是这个对象没有.constructor属性,所以他会继续委托,这次会委托给韦陀怜顶端的Object.prototype。这个对象由.constructor属性,指向内置的Object(...)函数

function Foo(){/*...*/}
Foo.prototype = {/*...*/}; //创建一个新原型对象//需要咋Foo.prototype上“修复”丢失的。.constructor属性
//新对象属性起到Foo,prototype的作用
Object.defineProperty(Foo.prototype,"constructor",{
    enumerable:false,
  wriable:true,
  configurable:true,
  value:Foo //让constructor指向Foo
}

可以给Foo.prototype添加一个.constructor属性,不过这需要手动添加一个符号正常行为的不可枚举属性

(原型)继承

function Foo(name){
  this.name = name
}
​
Foo.prototype.myName = function (){
  return this.name
}
​
function Bar(name,label){
  Foo.call(this,name)
  this.label = label
}
​
//我们创建了一个新的Bar.prototype对象并关联到Foo.prototype
Bar.prototype = Object.create(Foo.prototype);
​
//注意!现在没有Bar.prototype.constructort了
//如果你需要这个属性的话可能需要手动修复一下它Bar.prototype.myLabel = function(){
  return this.label
}
​
var a = new Bar("a","obj a");
​
console.log(a.myName());
console.log(a.myLabel());
// "a"
// "obj a"
//ES6之前需要抛弃默认的Bar.prototype
Bar.prototype = Object.create(Foo.prototype);
​
//ES6开始之后可以直接修改现有的Bar.prototype
Object.setPrototypeOf(Bar.prototype,Foo.prototype);

如果忽略掉Object.create方法带来的轻微性能损失(抛弃的对象需要进行垃圾回收),但是它可读性强。两种是不同的语法

检查“类”关系

检查一个实例的继承祖先通常被称为内省(或者反射)

function Foo(){
  //...
}
Foo.prototype.blah = ...;
var a = new Foo();

如何通过内省找出a的“祖先”,第一种方法是站在“类”的角度来判断:

a instanceof Foo;//true

做操作数是一个普通的对象,右操作数是一个函数。instanceof回答的问题是,在a的整条链中是否右Foo.prototype指向的对象

这种无法判断两个对象(a和b)之间是否通过[[Prototype]]链相关联,只用instanceof无法实现。

Foo.prototype.isPrototypeOf(a);//true

b是否处出现在c的[[Prototype]]链中

b.isPrototypeOf(c);

对象关联

创建关联

var foo = {
  something:function(){
    console.log("Tell me somthing good...");
  }
}
​
var bar = Object.create(foo);
​
bar.something();//"Tell me somthing good..."

Object.create(..)是一个大英雄。

比如上面的励志,Object.create会创建一个新对象(bar)并把它关联到我们的指定对象(foo),这样我们就可以充分发挥[[Prototype]]机制的为例(委托)并且可以避免不必要的麻烦(因为使用new的构造函数调用会生成.prototype .constructor引用)

Object.create()的polyfill代码

if(!Object.create){
  Object.create = function(o){
    function F(){}
    F.prototype = o;
    return new F()
  }
}

这段polyfill代码使用一个一次性的函数F,我们通过改写它的.prototype属性使得其指向我们想要关联的对象的,最后我们使用new F()来构造一个新对象进行关联。

关联关系是备用

var anotherObject = {
  cool:function(){
    console.log("cool!")
  }
}
​
var myObject = Object.create(anotherObject);
​
myObject.doCool = function(){
  this.cool();//内部委托
}
​
myObject.doCool();// "cool!"

这里我们调用的myObject.doCool()是实际存在于myObject中的,这可以让我们的API设计更加清晰。从内部来说,我们的实现遵循的是委托设计模式,通过[[Prototype]]委托到anotherObject.cool()

内部委托比起直接委托可以让API接口更加清晰

对象之间的关系不是复制而是委托

第六章 行为委托

面向委托的设计

类理论

//伪代码
class Task {
  id;
  
  //构造函数Task()
  Task(ID){id = ID;}
  outputTask(){output(id);}
}
​
class XYZ inherits Task{
  label;
  //构造函数XYZ()
  XYZ(ID,Label){super(ID);label = Label}
  outputTask(){super();output(label);}
}
class ABC inherits Task{
  //...
}

委托理论

执行任务“XYZ”需要两个兄弟对象(XYZ和Task)协作完成。但是我们并不需要把这些行为放在一起,通过类的复制,我们可以把他们呢分别放在各自独立的对象中,需要时可以允许XYZ对象委托给Task。

Task = {
  setId:function(ID){this.id = ID;},
  outputID:function(){console.log(this.id);}
};
​
//让XYZ委托Task
XYZ = Object.create(Task);XYZ.prepareTask = function(ID,Label){
  this.setId(id);
  THIS.LABEL = Label;
};XYZ.ouputTaskDetails = function(){
  this.outputID();
  console.log(this.label);
};
​
//ABC = Object.create(Task)
//ABC ... = ...

在这段代码中,Task和XYZ并不是类或者对象,他们呢时对象

对象关联,XYZ对象委托了Task对象。

在Javascript中,[[Prototype]]机制会把对象关联到其他对象。

相互委托(禁止)

无法在两个或两个以上互相(双向)委托的对象之间创建循环委托

调试

function Foo(){}
var a1 = new Foo();
console.log(a1.constructor)
console.log(a1.constructor.name)
// function Foo(){}
// "Foo"

比较思维模型

典型的(原型)面向对象风格

function Foo(who){
  this.me = who
}
​
Foo.prototype.identify = function(){
  return "I am " + this.me
}
​
function Bar(who){
  Foo.call(this,who)
}
//子类Bar继承了父类Foo
Bar.prototype = Object.create(Foo.prototype);
​
Bar.prototype.speak = function(){
  alert("Hello, " + this.identify() + ".");
}
//生成了b1和b2两个实例,b1委托了Bar.prototype,Bar.prototype委托了Foo.prototype
var b1 = new Bar("b1");
var b2 = new Bar("b2");
​
b1.speak();
b2.speak();

使用对象关联编写功能完全相同的代码

Foo = {
  init:function(who){
    this.me = who;
  },
  //在上面的例子中的话,这里是通过原型下面进行定义的
  identify:function(){
    return "I am " + this.me
  }
}
​
Bar = Object.create(Foo);
​
Bar.speak = function(){
  alert("Hello, " + this.identify() + ".")  ;
}
​
var b1 = Object.create(Bar)
b1.init("b1")
var b2 = Object.create(Bar)
b2.init("b2")
​
b1.speak();
b2.spear();

这段代码中我们同样利用[[Prototype]]把b1委托给Bar并把Bar委托给Foo,跟上一段代码一摸一样,实现了三个对象之间的关联。

类与对象

控件“类”

这个好像是jQuery

//父类
function Widget(width,height){
  this.width = width||50;
  this.height = height || 50;
  this.$elem = null;
}
​
Widget.prototype.render = function($where){
  if(this.$elem){
    this.$elem.css({
      width:this.width+"px",
      height:this.height+"px"
    }).appendTo($where)
  }
};
​
//子类
function Button(width,height,label){
  //调用"super"构造函数
  Widget.call(this,width,height)
  this.label = label || "Default"
  this.$elem = $("<button>").text(this.label)
}
​
//让Button"继承"Widget
Button.prototype = Object.create(Widget.prototype);
​
//重写render(..)
Button.prototype.render = function($where){
  //“super"调用
  Widget.prtotype.render.call(this.$where)
  this.$elem.click(this.onClick.bind(this));
}
​
Button.prototype.onClick = function(evt){
  console.log("Button '" + this.label + "' clicked!");
}
​
$(document).ready(function(){
  var $body = $(document.body);
  var btn1 = new Button(125,30,"Hello");
  var btn2 = new Button(150,40,"World");
  
  btn1.render($body)
  btn2.render($body)
})

在面向对象设计模式中我们需要现在父类中定义基础的render(...),然后在子列中重写它,子类并不会替换基础的render(...)只是添加一些按钮特有的行为

代码中出现了很多从“子类”方法中引用“父类”中的基础方法

ES6的class语法糖

class Widget{
  constructor(width,height){
    this.width = width || 50;
    this.height = height || 50;
    this.$elem = null
  }
  render($where){
    if(this.$elem){
      this.$elem.css({
        width:this.width+"px",
        height:this.height+"px"
      }).appendTo($where)
    }
  }
}
​
class Button extends Widget{
  constructor(width,height,label){
    super(width,height);
    this.label = label || "Default";
    this.$elem.click(this.onClick.bind(this));
  }
  render($where){
    super.render($where)
    this.$elem.click(this.onClick.bind(this));
  }
  onClick(evt){
    console.log("Button '" + this.label + "' clicked!");
  }
}
​
$(document).ready(function(){
  var $body = $(document.body);
  var btn1 = new Button(125,30,"Hello");
  var btn2 = new Button(150,40,"World");
  
  btn1.render($body)
  btn2.render($body)
})

委托控件对象

使用对象关联风格委托实现Widget/Button

//自身还有一个函数
var Widget = {
  init:function(width,height){
    this.width = width||50;
    this.height = height || 50;
    this.$elem = null;
  },
  insert:function($where){
    if(this.$elem){
      this.$elem.css({
        width:this.width+"px",
        height:this.height+"px"
      }).appendTo($where)
    }
  }
}
//建立一个Button和Widget之间的委托关系
var Button = Object.create(Widget);
​
Button.setup = function(width,height,label){
  //委托调用
  this.init(width,height)
  this.label = label || "Default"
  this.$elem = $("<button>").text(this.label)
};
Button.build = function($where){
  //委托调用
  this.insert($where)
  this.$elem.click(this.onClick.bind(this));
}
Button.onclick = function(evt){
  console.log("Button '" + this.label + "' clicked!");
};
$(document).ready(function(){
  var $body = $(document.body);
  
  var btn1 = Object.create(Button)
  btn1.setup(125,30,"Hello");
  
  var btn2 = Object.create(Button)
  btn2.setup(150,40,"World");
  
  btn1.build($body)
  btn2.build($body)
})

使用对象关联风格来编写代码时候就不需要将Wdget和Button当作父类和子类。相反的Widget只是一个对象,包含一组通用的函数。任何类型的空间都可以委托,Button同样只是一个对象。

对象关联可以更好地支持关注分离原则,创建和初始化并不需要合并为一个步骤

更简洁的设计

更好的语法

class的语法

class Foo{
  mythodName()
}

ES6中可以在任何对象中的字面形式中使用更加简洁的声明

var LoginController = {
  errors:[],
  getUser(){ //妈妈再也不用当心代码中有function了!
    
  }
}
​
//现在把AuthController关联到LoginController
Object.setPrototypeOf(AuthController,LoginController)

反词法

简洁模式会导致

  • 自我引用(递归、事件(解除)绑定、等等)更难;
var Foo = {
  bar: function(x){
    if(x < 10){
      return Foo.bar(x*2)
    }
    return x;
  },
  baz:function baz(x){
    if(x<10){
      return baz(x*2);
    }
    return x;
  }
}

多个对象通过代理共享、使用this绑定,等等,这种情况下最好的办法就是使用函数对象的name标识符来进行真正的自我引用。

内省

内省就是检查实例的类型,目的是通过创建方式来判断对象的结构和功能。

function Foo(){
  //...
}
​
Foo.prototype.something = fucntion(){
  
}
​
var a1 = new Foo();
​
//之后
if(a1 instanceof Foo){
  a1.something()
}

小结

对象关联是一种编码风格,它倡导的时直接创建和关联对象,不把他们抽象成类。对象关联可以用基于[[Prototype]]的行为委托非常自然地实现。