关于Object.defineProperty()的知识点

1一般javascript(简称js)中原型链以及原型的知识

所有引用类型(函数,数组,对象)都拥有__proto__属性(隐式原型)
所有函数拥有prototype属性(显式原型)(仅限函数)
原型对象:拥有prototype属性的对象,在定义函数时就被创建

属性搜索原则

所谓的属性搜索原则, 就是对象在访问属性与方法的时候, 首先在当前对象中查找

如果当前对象中存储在属性或方法, 停止查找, 直接使用该属性与方法

如果对象没有改成员, 那么再其原型对象中查找

如果原型对象含有该成员, 那么停止查找, 直接使用

如果原型还没有, 就到原型的原型中查找

如此往复, 直到直到 Object.prototype 还没有, 那么就返回 undefind.

如果是调用方法就包错, 该 xxxx 不是一个函数

arguments 对象

arguments 是一个伪数组对象. 它表示在函数调用的过程中传入的所有参数的集合.

在函数调用过程中没有规定参数的个数与类型, 因此函数调用就具有灵活的特性, 那么为了方便使用,

在 每一个函数调用的过程中, 函数代码体内有一个默认的对象 arguments, 它存储着实际传入的所有参数.

js 中函数并没有规定必须如何传参

定义函数的时候不写参数, 一样可以调用时传递参数

定义的时候写了参数, 调用的时候可以不传参

在代码设计中, 如果需要函数带有任意个参数的时候, 一般就不带任何参数, 所有的 参数利用 arguments 来获取.

一般的函数定义语法, 可以写成:
在这里插入图片描述
利用 Function 创建一个函数, 要求允许函数调用时传入任意个数参数, 并且函数返回这些数字中最大的数字.
关于Object.defineProperty()的知识点_第1张图片

一.要理解原型和原型链首先要知道几个概念:

1.在js里,继承机制是原型继承。继承的起点是 对象的原型(Object prototype)。

2.一切皆为对象,只要是对象,就会有 proto 属性,该属性存储了指向其构造的指针。
Object prototype也是对象,其 proto 指向null。

3.对象分为两种:函数对象和普通对象,只有函数对象拥有『原型』对象(prototype)。

prototype的本质是普通对象。

Function prototype比较特殊,是没有prototype的函数对象。

new操作得到的对象是普通对象。

var o1 = {}; 
var o2 =new Object();
var o3 = new f1();

function f1(){}; 
var f2 = function(){};
var f3 = new Function('str','console.log(str)');

console.log(typeof Object); //function 
console.log(typeof Function); //function  

console.log(typeof f1); //function 
console.log(typeof f2); //function 
console.log(typeof f3); //function   

console.log(typeof o1); //object 
console.log(typeof o2); //object 
console.log(typeof o3); //object

在上面的例子中 o1 o2 o3 为普通对象,f1 f2 f3 为函数对象。怎么区分,其实很简单,凡是通过 new Function() 创建的对象都是函数对象,其他的都是普通对象。f1,f2,归根结底都是通过 new Function()的方式进行创建的。Function Object 也都是通过 New Function()创建的。

4.当调取一个对象的属性时,会先在本身查找,若无,就根据 proto 找到构造原型,若无,继续往上找。最后会到达顶层Object prototype,它的 proto 指向null,均无结果则返回undefined,结束。

5.由 proto 串起的路径就是『原型链』。

函数的原型链结构
任意的一个函数, 都是相当于 Function 的实例. 类似于 {} 与 new Object() 的关系

function foo () {};
    // 告诉解释器, 有一个对象叫 foo, 它是一个函数
    // 相当于 new Function() 得到一个 函数对象

函数有 proto 属性
加粗样式
函数的构造函数是 Function

函数应该继承自 Function.prototype

Fucntion.prototype 继承自 Object.protoype

构造函数有prototype, 实例对象才有__proto__指向原型, 构造函数的原型才有 constructor 指向构造函数

6.一个具体对象(所有实例的祖先)最原始的原型,都是来源于最顶层的Object对象,所有的原型对象上最根本的是Object.proto

二.创建一个对象的方式

{}、new Object()

构造函数

Object.create()

此方法将新对象的proto更改并指向create的入参对象。

1.引用类型:是一种数据结构,用于将数据和功能组织在一起

2.ECMAScript是一门面向对象的语言,但它不具备传统的面向对象语言所支持的类和接口等基本结构

3.引用类型也称为对象定义,因为它描述的是一类对象所具有的属性和方法

4.对象是某个特定引用类型的实例

5.新对象是使用new操作符后跟一个构造函数来创建的

6.构造函数本身就是一个函数,只不过该函数是出于创建新对象的目的而定义的

1.使用new操作符后跟Object构造函数
在这里插入图片描述
2.使用字面量表示法

对象字面量是对象定义的一种简写形式,目的在于简化创建包含大量属性的对象的过程

在通过对象字面量定义对象时,实际上不会调用Object构造函数

var person={
	name:"Nicholas",
	age:29
};
//在对象字面量中,使用逗号来分隔不同不同属性
//但是29后面不能添加逗号,因为它是最后一个属性。在最后一个属性后面添加逗号,会在IE7及更早的版本和Opera中,导致错误

var person={
	"name":"Nicholas",
	"age":29,
	5:true
};
//在使用对象字面量语法时,属性名也可以使用字符串
//数值属性名会自动转换为字符串

var person={};//与new Object()相同
person.name="Nicholas";
person.age=29;
//在使用对象字面量语法时,如果留空其花括号,则可以定义只包含默认属性和方法的对象

function displayInfo(args){
	var output="";
	if(typeof args.name=="string"){
		output += "Name" + args.name + "\n";
	}
	if(typeof args.name=="number"){
		output += "Age: " + args.age + "\n";
	}
	alert(output);
}
displayInfo({
	name:"Nicholas",
	age:29
});
displayInfo({
	name:"Greg",
});
//推荐使用,能够给人封装数据的感觉
//实际上,对象字面量也是向函数传递大量可选参数的首选方式

2.instance of VS constructor**

instance of 原理:检查左边对象与右边对象是否在同一条原型链上。

constructor原理:取对象的proto属性指向的prototype对象上的constructor字段。

关于Object.defineProperty()的知识点_第2张图片

new运算符的原理

创建一个空对象,它的proto等于构造函数的原型对象(可以用Object.create()完成)

构造函数以第1步创建的对象做为上下文,是否会返回一个对象

若第2步返回了对象,则使用该对象作为新实例,否则用第1步创建的对象作为新实例

关于Object.defineProperty()的知识点_第3张图片

我们调用constructor属性,p._proto.proto.constructor得到拥有多个参数的Object()函数,Person.prototype的隐式原型的constructor指向Object(),即Person.prototype.proto.constructor == Object()

从p.proto.constructor返回的结果为构造函数本身得到Person.prototype.constructor == Person()所以p._proto.proto== Object.prototype

总结:

1.查找属性,如果本身没有,则会去__proto__中查找,也就是构造函数的显式原型中查找,如果构造函数中也没有该属性,因为构造函数也是对象,也有__proto__,那么会去它的显式原型中查找,一直到null,如果没有则返回undefined

2.p.proto.constructor == function Person(){}

3.p._proto.proto== Object.prototype

4.p._proto.proto.proto== Object.prototype.proto == null

5.通过__proto__形成原型链而非protrotype

最后附上一张图,大家阅读完之后,看图应该可以很容易理解
关于Object.defineProperty()的知识点_第4张图片

2.Object()的方法**

Object.assign()

Object.assign(target,source) 中将sources对象中所有可枚举的属性的值复制到目标的对象中,其会返回目标对象。该方法的兼容性不是很好,IE全面沦陷,在移动端方面,仅有少数的浏览器才兼容该方法。幸好的是在MDN上提供了兼容的方法。

if(typeof Object.assign!='function'){
  Object.assign = function(target){
    'use strict';
    if(target = null){
      throw new TypeError('Cannot convert undefined or null to object');
    }
    target = Object(target);
    for(var index=0;index

其实如果我们仔细观看源码的时候就会发现一个事实,那就是Object.assign()只是对一级属性进行复制,而不会对对象里面的对象进行深度拷贝,如果出现同名属性的key值,那么后者会覆盖前者,并不能做到完整的融合,如果要进行融合的话,可以前往depp-assign中阅读。

Object.create()

Object.create(proto,[properties]) 该方法将__proto__作为原型对象,并将[properties]作为新对象的属性。

Object.defineProperties()

Object.defineProperties(obj,props) 方法直接在一个对象上修改或创建属性,并返回修改后的对象,其与上面那个方法的区别在于前者可以修改或定义多个属性,但是后者可以定义或修改多个,同时二者的兼容性一样。对于前者而言,有相应的polyfill函数。如下显示:

function defineProperties(obj, properties)
{
  function convertToDescriptor(desc){
    function hasProperty(obj, prop){
      return Object.prototype.hasOwnProperty.call(obj, prop);
    }

    function isCallable(v){
      // 如果除函数以外,还有其他类型的值也可以被调用,则可以修改下面的语句
      return typeof v === "function";
    }

    if (typeof desc !== "object" || desc === null)
      throw new TypeError("不是正规的对象");

    var d = {};
    if (hasProperty(desc, "enumerable"))
      d.enumerable = !!desc.enumerable;
    if (hasProperty(desc, "configurable"))
      d.configurable = !!desc.configurable;
    if (hasProperty(desc, "value"))
      d.value = desc.value;
    if (hasProperty(desc, "writable"))
      d.writable = !!desc.writable;
    if (hasProperty(desc, "get")){
      var g = desc.get;
      if (!isCallable(g) && g !== "undefined")
        throw new TypeError("bad get");
      d.get = g;
    }
    if (hasProperty(desc, "set")){
      var s = desc.set;
      if (!isCallable(s) && s !== "undefined")
        throw new TypeError("bad set");
      d.set = s;
    }

    if (("get" in d || "set" in d) && ("value" in d || "writable" in d))
      throw new TypeError("identity-confused descriptor");

    return d;
  }

  if (typeof obj !== "object" || obj === null)
    throw new TypeError("不是正规的对象");

  properties = Object(properties);
  var keys = Object.keys(properties);
  var descs = [];
  for (var i = 0; i < keys.length; i++)
    descs.push([keys[i], convertToDescriptor(properties[keys[i]])]);
  for (var i = 0; i < descs.length; i++)
    Object.defineProperty(obj, descs[i][0], descs[i][1]);

  return obj;
}

Object.getOwnPropertyDescriptor(obj,prop)

该方法是用于如果prop属性存在对象obj上,则返回其属性描述符,如果不存在就返回undefined。该属性描述符由下面的属性所组成。

【value】表示属性的值,仅针对数据属性描述符有效
【writable】当且仅当属性的值可以被改变时为true。(仅针对数据属性描述有效)
【configurable】 当且仅当指定对象的属性描述可以被改变或者属性可被删除时,为true。
【enumerable】当且仅当指定对象的属性可以被枚举出时,为 true。
【get】获取该属性的访问器函数(getter)。如果没有访问器, 该值为undefined。
【set】获取该属性的设置器函数(setter)。如果没有设置器,该值为undefined。(仅针对包含访问器或设置器的属性描述有效)
该方法能够在IE8及以上的浏览器上运行。

Object.getOwnPropertyNames(obj)

该方法返回obj上所有自身可枚举和不可枚举的属性(不包括原型链上的属性),如果传入的obj不是数组,则会报错。该方法的兼容IE9及以上的浏览器。

Object.getPrototypeOf(obj)

该方法返回对象的原型对象,如果没有的话,则返回null。需要指出的是,对于函数对象,其返回的并不是显式原型(prototype),而是隐式原型(proto),该方法兼容IE9及以上的浏览器。

Object.is(val1,val2)

该方法是确定两个值是否是相同的值,这个方法与===相比,其会将-0和+0看成不等,并且对于两个NaN的比较,Object.is()也会看成是不等的。以下是Object.is()的示例:

Object.is(0,-0)//false
Object.is(-0,-0);//true
Object.is(NaN,0/0); //true
Object.is(5,5/1); //true

该方法在微软公司出的浏览器上只支持EDGE浏览器。不过,万幸的是MDN提供了相应的解决方案。

if (!Object.is) {
  Object.is = function(x, y) {
    if (x === y) { 
      return x !== 0 || 1 / x === 1 / y;
    } else {
      return x !== x && y !== y;
    }
  };
}

Object.preventExtensions()

该方法可以让一个对象永远不能添加新的属性,在严格模式下,如果强行为对象添加属性,会报错,以下是Object.isExtensible()的注意事项:

"use strict";
var obj = {name:"zhang"};
obj.name = "li"//可以进行修改
Object.preventExtensions(obj);
//obj.age = 14;严格模式下会报错
obj.__proto__.age = 13;
console.log(obj);//能够在原型对象上添加属性
obj.__proto__ = {}//不能直接重定义原型,会报错。

Object.seal(obj)

其对一个对象进行密封,并返回被密封的对象,这些对象都是不能够添加属性,不能删除已有属性,以及不能够修改已有属性的可枚举型、可配置型、可写性。

Object.freeze(obj)

该方法将obj对象冻结,其任何属性都是不可以被修改的。现在我们演示下这个用法。

var obj = {name:"zhangsan",prop:{age:23,sex:"man"}};
Object.freeze(obj);
obj.name = "lisi";
console.log(obj.name);//"zhangsan
//我们使用Object.defineProperty()方法来修改属性
Object.defineProperty(obj,'prop',{"age":32,sex:"female"});
console.log(obj.prop);
//{age: 23, sex: "man"}貌似还是不行,我们换种方式看看
Object.prop.age = 25;
console.log(Object.prop);
//{age: 25, sex: "man"}
//这个对象居然改变了,明明已经冻结了,为什么起属性还是可以发生变化

这就要说到Object.freeze(obj) 的特性了,其只是一个浅冻结。何为浅冻结?浅冻结仅仅是对对象的一级属性进行冻结,像上面代码中所演示的那样,如果直接修改其name和prop属性是不能被修改的。如果属性也是一个对象的话,那将不一样了,直接对属性中的属性就行修改,如Object.prop.age = 25;一样,是可以修改的。既然有浅冻结,就一定有深冻结了,那怎么才能实现深冻结呢?

//我们可以配合递归实现
Object.prototype.deepFreeze = Object.prototype.deepFreeze || function (o){
    var prop, propKey;
    Object.freeze(o); // 首先冻结第一层对象
    for (propKey in o){
        prop = o[propKey];
        if(!o.hasOwnProperty(propKey) || !(typeof prop === "object") || Object.isFrozen(prop)){
            continue;
        }
        deepFreeze(prop); // 递归
    }
}

可以有人会对preventExtensions,seal,freeze这三个方法产生疑问,这三个方法对从扩展、密封和冻结三个方面对对象进行读写状态的控制,防止对象被改变。其中最弱的一层是preventExtensions,只能让对象无法添加新的属性,其次是seal,该方法无法添加属性,也无法删除属性,最后是freeze。当然,上面这三种方法还是可以通过改变对象的原型对象,来增加原型链上的属性,并且都使浅冻结。有对对象进行控制的方法,就肯定有判断其是否被控制的方法,Object.isExtensible()、Object.isSealed()和Object.isfreeze(obj)。这三个是判断是否被控制,在此就不再赘述。

Object.keys(obj)

该方法会返回obj上所有可以进行枚举的属性的字符串数组,如下所示:

//数组对象
var arr  =[3,4,5];
console.log(Object.keys(obj))
//[0,1,2]
var obj = {}
console.log(Object.keys(obj))
//[],其不会遍历原型链上的属性。

该方法兼容IE9及以上的浏览器,但是有相应的解决方法。

if (!Object.keys) {
  Object.keys = (function () {
    var hasOwnProperty = Object.prototype.hasOwnProperty,
        hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'),
        dontEnums = [
          'toString',
          'toLocaleString',
          'valueOf',
          'hasOwnProperty',
          'isPrototypeOf',
          'propertyIsEnumerable',
          'constructor'
        ],
        dontEnumsLength = dontEnums.length;
    return function (obj) {
      if (typeof obj !== 'object' && typeof obj !== 'function' || obj === null) throw new TypeError('Object.keys called on non-object');
      var result = [];
      for (var prop in obj) {
        if (hasOwnProperty.call(obj, prop)) result.push(prop);
      }
      if (hasDontEnumBug) {
        for (var i=0; i < dontEnumsLength; i++) {
          if (hasOwnProperty.call(obj, dontEnums[i])) result.push(dontEnums[i]);
        }
      }
      return result;
    }
  })()
};

getOwnPropertySymbols(obj)

该方法返回obj对象上自身的(非继承的)所有Symbol属性键。

Object.defineProperty()

Object.defineProperty(obj,prop,descriptor) 方法在obj对象上对prop属性进行定义或修改,其中descriptor为被定义或修改的属性符。其中对于descriptor属性符可以设置的值如下显示:

【value】表示属性的值,默认为undefined
【writable】该属性是否为可写,如果直接在对象上定义属性,则默认为true。如果设置为false,则属性仅为可读。
【configurable】 如果为false的话,则不能修改(writabel,configurable,enumerable),如果直接在对象上定义属性,则默认为true
【enumerable】是否能够被枚举,如果直接在对象上定义属性,则默认为true。
【get】当对象访问prop属性的实话,会调用这个方法,并返回结果。默认为undefined
【set】当对象设置该属性的时候,会调用这个方法,默认为undefined。
关于Object.defineProperty()的知识点_第5张图片
对象的定义与赋值

经常使用的定义与赋值方法obj.prop =value或者obj['prop']=value

关于Object.defineProperty()的知识点_第6张图片
Object.defineProperty()语法说明

Object.defineProperty()的作用就是直接在一个对象上定义一个新属性,或者修改一个已经存在的属性

Object.defineProperty(obj, prop, desc)

obj 需要定义属性的当前对象

prop 当前需要定义的属性名

desc/属性描述符**descriptor**

一般通过为对象的属性赋值的情况下,对象的属性可以修改也可以删除,但是通过Object.defineProperty()定义属性,通过描述符的设置可以进行更精准的控制对象属性。

属性的特性以及内部属性

javacript 有三种类型的属性

命名数据属性:拥有一个确定的值的属性。这也是最常见的属性

命名访问器属性:通过getter和setter进行读取和赋值的属性

内部属性:由JavaScript引擎内部使用的属性,不能通过JavaScript代码直接访问到,不过可以通过一些方法间接的读取和设置。比如,每个对象都有一个内部属性[[Prototype]],你不能直接访问这个属性,但可以通过Object.getPrototypeOf()方法间接的读取到它的值。虽然内部属性通常用一个双吕括号包围的名称来表示,但实际上这并不是它们的名字,它们是一种抽象操作,是不可见的,根本没有上面两种属性有的那种字符串类型的属性

属性描述符

通过Object.defineProperty()为对象定义属性,有两种形式,且不能混合使用,分别为数据描述符,存取描述符,下面分别描述下两者的区别:

数据描述符 --特有的两个属性(value,writable)

let Person = {}
Object.defineProperty(Person, 'name', {
   value: 'jack',
   writable: true // 是否可以改变
})

关于Object.defineProperty()的知识点_第7张图片
关于Object.defineProperty()的知识点_第8张图片
注意,如果描述符中的某些属性被省略,会使用以下默认规则:
关于Object.defineProperty()的知识点_第9张图片
存取描述符 --是由一对 getter、setter 函数功能来描述的属性

get:一个给属性提供getter的方法,如果没有getter则为undefined。该方法返回值被用作属性值。默认为undefined。

set:一个给属性提供setter的方法,如果没有setter则为undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认值为undefined。

let Person = {}
let temp = null
Object.defineProperty(Person, 'name', {
  get: function () {
    return temp
  },
  set: function (val) {
    temp = val
  }
})

关于Object.defineProperty()的知识点_第10张图片
数据描述符和存取描述均具有以下描述符

configrable 描述属性是否配置,以及可否删除

enumerable 描述属性是否会出现在for in 或者 Object.keys()的遍历中

configrable 代码片段分析

关于Object.defineProperty()的知识点_第11张图片
关于Object.defineProperty()的知识点_第12张图片
关于Object.defineProperty()的知识点_第13张图片
关于Object.defineProperty()的知识点_第14张图片
关于Object.defineProperty()的知识点_第15张图片
关于Object.defineProperty()的知识点_第16张图片
关于Object.defineProperty()的知识点_第17张图片
从以上代码运行结果分析总结可知:

configurable: false 时,不能删除当前属性,且不能重新配置当前属性的描述符(有一个小小的意外:可以把writable的状态由true改为false,但是无法由false改为true),但是在writable: true的情况下,可以改变value的值
configurable: true时,可以删除当前属性,可以配置当前属性所有描述符

enumerable 代码片段分析

关于Object.defineProperty()的知识点_第18张图片
关于Object.defineProperty()的知识点_第19张图片
关于Object.defineProperty()的知识点_第20张图片
不变性

对象常量

结合writable: false 和 configurable: false 就可以创建一个真正的常量属性(不可修改,不可重新定义或者删除)
关于Object.defineProperty()的知识点_第21张图片
禁止扩展

如果你想禁止一个对象添加新属性并且保留已有属性,就可以使用Object.preventExtensions(…)

关于Object.defineProperty()的知识点_第22张图片
关于Object.defineProperty()的知识点_第23张图片
关于Object.defineProperty()的知识点_第24张图片
关于Object.defineProperty()的知识点_第25张图片

这个方法是你可以应用在对象上级别最高的不可变性,它会禁止对于对象本身及其任意直接属性的修改(但是这个对象引用的其他对象是不受影响的)
你可以深度冻结一个对象,具体方法为,首先这个对象上调用Object.freeze()然后遍历它引用的所有对象,并在这些对象上调用Object.freeze()。但是一定要小心,因为这么做有可能会无意中冻结其他共享对象。

属性定义和属性赋值

属性定义,通过Object.defineProperty()形式

如果Obj没有名为Prop的自身属性的话:如果Obj是可扩展的话,则创建Prop这个自身属性,否则拒绝
如果Obj已经有了名为Prop的自身属性:则按照下面的步骤重新配置这个属性
如果这个已有的属性是不可配置的,则进行下面的操作会被拒绝

关于Object.defineProperty()的知识点_第26张图片

否则这个已有的属性可以被重新配置

属性赋值,通过obj.prop = ''prop"形式

如果在原型链上存在一个名为P的只读属性(只读的数据属性或者没有setter的访问器属性),则拒绝
如果在原型链上存在一个名为P的且拥有setter的访问器属性,则调用这个setter

如果没有名为P的自身属性,则如果这个对象是可扩展的,就创建一个新属性,否则,如果这个对象是不可扩展的,则拒绝
如果已经存在一个可写的名为P的自身属性,则调用Object.defineProperty(),该操作只会更改P属性的值,其他的特性(比如可枚举性)都不会改变

作用以及影响

属性的定义操作和赋值操作各自有自己的作用和影响。

赋值可能会调用原型上的setter,定义会创建一个自身属性。

原型链中的同名只读属性可能会阻止赋值操作,但不会阻止定义操作。如果原型链中存在一个同名的只读属性,则无法通过赋值的方式在原对象上添加这个自身属性,必须使用定义操作才可以。这项限制是在ECMAScript 5.1中引入的
关于Object.defineProperty()的知识点_第27张图片
关于Object.defineProperty()的知识点_第28张图片
关于Object.defineProperty()的知识点_第29张图片
关于Object.defineProperty()的知识点_第30张图片
关于Object.defineProperty()的知识点_第31张图片
关于Object.defineProperty()的知识点_第32张图片
关于Object.defineProperty()的知识点_第33张图片
关于Object.defineProperty()的知识点_第34张图片
3.剖析Vue原理&实现双向绑定MVVM

几种实现双向绑定的做法

目前几种主流的mvc(vm)框架都实现了单向数据绑定,而我所理解的双向数据绑定无非就是在单向绑定的基础上给可输入元素(input、textare等)添加了change(input)事件,来动态修改model和 view,并没有多高深。所以无需太过介怀是实现的单向或双向绑定。

发布者-订阅者模式(backbone.js)

脏值检查(angular.js) 

数据劫持(vue.js)

发布者-订阅者模式: 一般通过sub, pub的方式实现数据和视图的绑定监听,更新数据方式通常做法是 vm.set(‘property’, value),这里有篇文章讲的比较详细,有兴趣可点这里

这种方式现在毕竟太low了,我们更希望通过 vm.property = value 这种方式更新数据,同时自动更新视图,于是有了下面两种方式

脏值检查: angular.js 是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图,最简单的方式就是通过 setInterval() 定时轮询检测数据变动,当然Google不会这么low,angular只有在指定的事件触发时进入脏值检测,大致如下:

DOM事件,譬如用户输入文本,点击按钮等。( ng-click )
XHR响应事件 ( $http )
浏览器Location变更事件 ( $location )
Timer事件( $timeout , $interval )
执行 $digest() 或 $apply()

数据劫持: vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

已经了解到vue是通过数据劫持的方式来做数据绑定的,其中最核心的方法便是通过Object.defineProperty()来实现对属性的劫持,达到监听数据变动的目的

整理了一下,要实现mvvm的双向绑定,就必须要实现以下几点:

1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者

2、实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数

3、实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图

4、mvvm入口函数,整合以上三者

关于Object.defineProperty()的知识点_第35张图片

1、实现Observer

ok, 思路已经整理完毕,也已经比较明确相关逻辑和模块功能了,

我们知道可以利用Obeject.defineProperty()来监听属性变动

那么将需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter和getter

这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化。。相关代码可以是这样

var data = {name: 'kindeng'};
observe(data);
data.name = 'dmq'; // 哈哈哈,监听到值变化了 kindeng --> dmq

function observe(data) {
    if (!data || typeof data !== 'object') {
        return;
    }
    // 取出所有属性遍历
    Object.keys(data).forEach(function(key) {
        defineReactive(data, key, data[key]);
    });
};

function defineReactive(data, key, val) {
    observe(val); // 监听子属性
    Object.defineProperty(data, key, {
        enumerable: true, // 可枚举
        configurable: false, // 不能再define
        get: function() {
            return val;
        },
        set: function(newVal) {
            console.log('哈哈哈,监听到值变化了 ', val, ' --> ', newVal);
            val = newVal;
        }
    });
}

这样我们已经可以监听每个数据的变化了,那么监听到变化之后就是怎么通知订阅者了,所以接下来我们需要实现一个消息订阅器,很简单,维护一个数组,用来收集订阅者,数据变动触发notify,再调用订阅者的update方法,代码改善之后是这样:

// ... 省略
function defineReactive(data, key, val) {
    var dep = new Dep();
    observe(val); // 监听子属性

    Object.defineProperty(data, key, {
        // ... 省略
        set: function(newVal) {
            if (val === newVal) return;
            console.log('哈哈哈,监听到值变化了 ', val, ' --> ', newVal);
            val = newVal;
            dep.notify(); // 通知所有订阅者
        }
    });
}

function Dep() {
    this.subs = [];
}
Dep.prototype = {
    addSub: function(sub) {
        this.subs.push(sub);
    },
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update();
        });
    }
};

那么问题来了,谁是订阅者?怎么往订阅器添加订阅者?

没错,上面的思路整理中我们已经明确订阅者应该是Watcher, 而且var dep = new Dep();是在 defineReactive方法内部定义的,所以想通过dep添加订阅者,就必须要在闭包内操作,所以我们可以在 getter里面动手脚:

// Observer.js
// ...省略
Object.defineProperty(data, key, {
    get: function() {
        // 由于需要在闭包内添加watcher,所以通过Dep定义一个全局target属性,暂存watcher, 添加完移除
        Dep.target && dep.addDep(Dep.target);
        return val;
    }
    // ... 省略
});

// Watcher.js
Watcher.prototype = {
    get: function(key) {
        Dep.target = this;
        this.value = data[key];    // 这里会触发属性的getter,从而添加订阅者
        Dep.target = null;
    }
}

这里已经实现了一个Observer了,已经具备了监听数据和数据变化通知订阅者的功能,完整代码。那么接下来就是实现Compile了

2、实现Compile

compile主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图,如图所示:
关于Object.defineProperty()的知识点_第36张图片
因为遍历解析的过程有多次操作dom节点,为提高性能和效率,会先将跟节点el转换成文档碎片fragment进行解析编译操作,解析完成,再将fragment添加回原来的真实dom节点中

function Compile(el) {
    this.$el = this.isElementNode(el) ? el : document.querySelector(el);
    if (this.$el) {
        this.$fragment = this.node2Fragment(this.$el);
        this.init();
        this.$el.appendChild(this.$fragment);
    }
}
Compile.prototype = {
    init: function() { this.compileElement(this.$fragment); },
    node2Fragment: function(el) {
        var fragment = document.createDocumentFragment(), child;
        // 将原生节点拷贝到fragment
        while (child = el.firstChild) {
            fragment.appendChild(child);
        }
        return fragment;
    }
};

compileElement方法将遍历所有节点及其子节点,进行扫描解析编译,调用对应的指令渲染函数进行数据渲染,并调用对应的指令更新函数进行绑定,详看代码及注释说明:

Compile.prototype = {
    // ... 省略
    compileElement: function(el) {
        var childNodes = el.childNodes, me = this;
        [].slice.call(childNodes).forEach(function(node) {
            var text = node.textContent;
            var reg = /\{\{(.*)\}\}/;    // 表达式文本
            // 按元素节点方式编译
            if (me.isElementNode(node)) {
                me.compile(node);
            } else if (me.isTextNode(node) && reg.test(text)) {
                me.compileText(node, RegExp.$1);
            }
            // 遍历编译子节点
            if (node.childNodes && node.childNodes.length) {
                me.compileElement(node);
            }
        });
    },

    compile: function(node) {
        var nodeAttrs = node.attributes, me = this;
        [].slice.call(nodeAttrs).forEach(function(attr) {
            // 规定:指令以 v-xxx 命名
            // 如  中指令为 v-text
            var attrName = attr.name;    // v-text
            if (me.isDirective(attrName)) {
                var exp = attr.value; // content
                var dir = attrName.substring(2);    // text
                if (me.isEventDirective(dir)) {
                    // 事件指令, 如 v-on:click
                    compileUtil.eventHandler(node, me.$vm, exp, dir);
                } else {
                    // 普通指令
                    compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
                }
            }
        });
    }
};

// 指令处理集合
var compileUtil = {
    text: function(node, vm, exp) {
        this.bind(node, vm, exp, 'text');
    },
    // ...省略
    bind: function(node, vm, exp, dir) {
        var updaterFn = updater[dir + 'Updater'];
        // 第一次初始化视图
        updaterFn && updaterFn(node, vm[exp]);
        // 实例化订阅者,此操作会在对应的属性消息订阅器中添加了该订阅者watcher
        new Watcher(vm, exp, function(value, oldValue) {
            // 一旦属性值有变化,会收到通知执行此更新函数,更新视图
            updaterFn && updaterFn(node, value, oldValue);
        });
    }
};

// 更新函数
var updater = {
    textUpdater: function(node, value) {
        node.textContent = typeof value == 'undefined' ? '' : value;
    }
    // ...省略
};

这里通过递归遍历保证了每个节点及子节点都会解析编译到,包括了{{}}表达式声明的文本节点。指令的声明规定是通过特定前缀的节点属性来标记,如 监听数据、绑定更新函数的处理是在compileUtil.bind()这个方法中,通过new Watcher()添加回调来接收数据变化的通知

至此,一个简单的Compile就完成了,完整代码。接下来要看看Watcher这个订阅者的具体实现了

3、实现Watcher

Watcher订阅者作为Observer和Compile之间通信的桥梁,主要做的事情是:

1、在自身实例化时往属性订阅器(dep)里面添加自己

2、自身必须有一个update()方法

3、待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。

如果有点乱,可以回顾下前面的思路整理

function Watcher(vm, exp, cb) {
    this.cb = cb;
    this.vm = vm;
    this.exp = exp;
    // 此处为了触发属性的getter,从而在dep添加自己,结合Observer更易理解
    this.value = this.get(); 
}
Watcher.prototype = {
    update: function() {
        this.run();    // 属性值变化收到通知
    },
    run: function() {
        var value = this.get(); // 取到最新值
        var oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal); // 执行Compile中绑定的回调,更新视图
        }
    },
    get: function() {
        Dep.target = this;    // 将当前订阅者指向自己
        var value = this.vm[exp];    // 触发getter,添加自己到属性订阅器中
        Dep.target = null;    // 添加完毕,重置
        return value;
    }
};
// 这里再次列出Observer和Dep,方便理解
Object.defineProperty(data, key, {
    get: function() {
        // 由于需要在闭包内添加watcher,所以可以在Dep定义一个全局target属性,暂存watcher, 添加完移除
        Dep.target && dep.addDep(Dep.target);
        return val;
    }
    // ... 省略
});
Dep.prototype = {
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update(); // 调用订阅者的update方法,通知变化
        });
    }
};

实例化Watcher的时候,调用get()方法,通过Dep.target = watcherInstance标记订阅者是当前watcher实例,强行触发属性定义的getter方法,getter方法执行的时候,就会在属性的订阅器dep添加当前watcher实例,从而在属性值有变化的时候,watcherInstance就能收到更新通知。

ok, Watcher也已经实现了,完整代码。
基本上vue中数据绑定相关比较核心的几个模块也是这几个,猛戳这里 , 在src 目录可找到vue源码。

最后来讲讲MVVM入口文件的相关逻辑和实现吧,相对就比较简单了~

4、实现MVVM

MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。

一个简单的MVVM构造器是这样子:

function MVVM(options) {
    this.$options = options;
    var data = this._data = this.$options.data;
    observe(data, this);
    this.$compile = new Compile(options.el || document.body, this)
}

但是这里有个问题,从代码中可看出监听的数据对象是options.data,每次需要更新视图,则必须通过var vm = new MVVM({data:{name: ‘kindeng’}}); vm._data.name = ‘dmq’; 这样的方式来改变数据。

显然不符合我们一开始的期望,我们所期望的调用方式应该是这样的:
var vm = new MVVM({data: {name: ‘kindeng’}}); vm.name = ‘dmq’;

所以这里需要给MVVM实例添加一个属性代理的方法,使访问vm的属性代理为访问vm._data的属性,改造后的代码如下:

function MVVM(options) {
    this.$options = options;
    var data = this._data = this.$options.data, me = this;
    // 属性代理,实现 vm.xxx -> vm._data.xxx
    Object.keys(data).forEach(function(key) {
        me._proxy(key);
    });
    observe(data, this);
    this.$compile = new Compile(options.el || document.body, this)
}

MVVM.prototype = {
    _proxy: function(key) {
        var me = this;
        Object.defineProperty(me, key, {
            configurable: false,
            enumerable: true,
            get: function proxyGetter() {
                return me._data[key];
            },
            set: function proxySetter(newVal) {
                me._data[key] = newVal;
            }
        });
    }
};

这里主要还是利用了Object.defineProperty()这个方法来劫持了vm实例对象的属性的读写权,使读写vm实例的属性转成读写了vm._data的属性值,达到鱼目混珠的效果,哈哈

至此,全部模块和功能已经完成了,如本文开头所承诺的两点。一个简单的MVVM模块已经实现,其思想和原理大部分来自经过简化改造的vue源码,猛戳这里可以看到本文的所有相关代码。
由于本文内容偏实践,所以代码量较多,且不宜列出大篇幅代码,所以建议想深入了解的童鞋可以再次结合本文源代码来进行阅读,这样会更加容易理解和掌握。

总结

本文主要围绕“几种实现双向绑定的做法”、“实现Observer”、“实现Compile”、“实现Watcher”、“实现MVVM”这几个模块来阐述了双向绑定的原理和实现,以及关于Object原型,原型链的知识点,vuejs数据双向绑定的原理。
并根据思路流程渐进梳理讲解了一些细节思路和比较关键的内容点,以及通过展示部分关键代码讲述了怎样一步步实现一个双向绑定MVVM。文中肯定会有一些不够严谨的思考和错误,欢迎大家指正,有兴趣欢迎一起探讨和改进~

你可能感兴趣的:(关于Object.defineProperty()的知识点)