js中dog-drag源码js解析json的问题

2461人阅读
神经网络(21)
最近有文章评论Karpathy的基于JavaScript的卷积神经网络在Github下载量名列前茅。其,,以及正在。原因有两个,一个是JavaScript,这是一种脚本语言,其好处在于用户除了安装IE等浏览器,而不需要安装其他编译的package,就可以直接运行程序,其也更利于可视化。另外就是卷积神经网络成为处理图片的一个流行且有效的算法,。
以前,都是Python写,最近一个星期看了JavaScript.现在尝试以一个初学JavaScript的角度解析ConvNetJS.此文主要涉及JavaScript的代码结构,不太涉及太多的原理。比较适合有刚学JavaScript的读者。
另外,我看了一个星期的《JavaScript高级程序》这本书,本书是一个朋友推荐的,可以在网上搜到免费的pdf文件。在JavaScripts编程时,建议阅读书中的第七章的对象,以及第八章的函数表达式。如果是完全小白,那么最好阅读之前的六章。
二.JavaScript代码解析
下载,解压,打开文件夹src.其主要的类在这里实现。至于其官网上的compile编译,可做可不做,因为其编译过程大概意思就是讲src中的代码文件按照一定的顺序合并成build文件夹中convnet.js文件。至于其官网上面的花哨的例子只能在编译之后并且下载完需要数据(Github代码中并没有数据)以后才可以运行。因为这些例子都是基于convnet的API,因此要想自己搞清楚那些例子,必须从基础的convnet搞起。我会在后续的更新中说明这些例子。接下俩几篇文章的分析是src文件夹中的js文件。
我们按照,
convnet_init.convnet_util.convnet_vol.convnet_vol_util.
Layer(convnet_layers_dotproducts.convnet_layers_dropout.convnet_layers_input.convnet_layers_loss.
convnet_layers_nonlinearities.convnet_layers_normalization.convnet_layers_pool.js),convnet_net.
convnet_trainers.js。第一篇文章将分析前面四个,主要说明其代码的主要结构和使用的数据结构Vol。
2.1&convnet_init.js
只有一行代码:& & & &
var convnetjs = convnetjs || { REVISION: 'ALPHA' };
这一行代码的意思就是创建名为convnetjs的对象,并定义了一个名为REVISION的属性,值为‘ALPHA’。后面的代码格式基于都是围绕convnetjs类的继续定义。
2.2&convnet_util.js
该代码继续定义了convnet类的一些公共属性。文件是一个闭包语句。闭包是一个函数,其中传递的参数就是我们在convnet_init.js中定义的convnetjs的类对象。使用闭包的形式是为了屏蔽在这个文件中定义一些局部变量。具体关于闭包的用法可以参见《JavaScript高级程序》中的第八章中闭包这一节。另外除了闭包,其中另外一个是函数表达式,比如定义randf()时,其作用是返回输入参数number类型变量a,b,并随机返回之间的数字。其中定义的函数为匿名函数,并将其赋值给randf,其中randf是一个函数类型的变量。这一点与C中函数指针类似。但是在C中需要事先声明变量的类型,但是在JS中,不需要事先声明。最后在完成一些的函数定义之后,建立convnetjs的属性并赋值。
(function(global) {
&use strict&;
var randf = function(a, b) { return Math.random()*(b-a)+a; }
global.randf =
global.randi =
global.randn =
global.zeros =
global.maxmin =
global.randperm =
global.weightedSample = weightedS
global.arrUnique = arrU
global.arrContains = arrC
global.getopt =
global.assert =
})(convnetjs);以上代码是部分代码的截断。
2.3convnetjs_vol.js
Vol是全局类convnetjs的一个属性,其算法逻辑是基本的数据单元,里面定义了一些输入数据的信息,如输入数据的长度,宽度和深度。比如对于卷积神经网络大多处理的是图片,如25*25*RBG图片,因此必须使用三维矩阵存储该数据点。而对于一般的神经网络,可以把数据存储在一个维度上,因此其宽度和深度为1。因此Vol可以认为是整个代码体系的自定义的数据结构,就像C中定义的结构体。在Vol类的定义中分为两个文件完成,一个是convnetjs_vol.js,完成了类Vol定义的主体。另一个是convnet_vol_util.js则是对于上面定义类补充了两个公共函数方法。【下面代码中的中文注释为我自加的注释。】
(function(global) {
&use strict&;
// Vol is the basic building block of all data in a net.
// it is essentially just a 3D volume of numbers, with a
// width (sx), height (sy), and depth (depth).
// it is used to hold data for all filters, all volumes,
// all weights, and also stores all gradients w.r.t.
// the data. c is optionally a value to initialize the volume
// with. If c is missing, fills the Vol with random numbers.
var Vol = function(sx, sy, depth, c) {
// this is how you check if a variable is an array. Oh, Javascript :)
if(Object.prototype.toString.call(sx) === '[object Array]') {
// 这个长长的函数完成了对输入数据类型的判断,如果使用typeof,只能返回一个object
// we were given a list in sx, assume 1D volume and fill it up
this.sx = 1;
this.sy = 1;
this.depth = sx.
// we have to do the following copy because we want to use
// fast typed arrays, not an ordinary javascript array
this.w = global.zeros(this.depth);
this.dw = global.zeros(this.depth);
for(var i=0;i&this.i++) {
this.w[i] = sx[i];
// we were given dimensions of the vol
this.depth =
var n = sx*sy*
this.w = global.zeros(n);
this.dw = global.zeros(n);
if(typeof c === 'undefined') {
// weight normalization is done to equalize the output
// variance of every neuron, otherwise neurons with a lot
// of incoming connections have outputs of larger variance
var scale = Math.sqrt(1.0/(sx*sy*depth));
for(var i=0;i&n;i++) {
this.w[i] = global.randn(0.0, scale);
for(var i=0;i&n;i++) {
this.w[i] =
Vol.prototype = {
get: function(x, y, d) {
var ix=((this.sx * y)+x)*this.depth+d;
return this.w[ix];
set: function(x, y, d, v) {
var ix=((this.sx * y)+x)*this.depth+d;
this.w[ix] =
add: function(x, y, d, v) {
var ix=((this.sx * y)+x)*this.depth+d;
this.w[ix] +=
get_grad: function(x, y, d) {
var ix = ((this.sx * y)+x)*this.depth+d;
return this.dw[ix];
set_grad: function(x, y, d, v) {
var ix = ((this.sx * y)+x)*this.depth+d;
this.dw[ix] =
add_grad: function(x, y, d, v) {
var ix = ((this.sx * y)+x)*this.depth+d;
this.dw[ix] +=
cloneAndZero: function() { return new Vol(this.sx, this.sy, this.depth, 0.0)},
clone: function() {
var V = new Vol(this.sx, this.sy, this.depth, 0.0);
var n = this.w.
for(var i=0;i&n;i++) { V.w[i] = this.w[i]; }
addFrom: function(V) { for(var k=0;k&this.w.k++) { this.w[k] += V.w[k]; }},
addFromScaled: function(V, a) { for(var k=0;k&this.w.k++) { this.w[k] += a*V.w[k]; }},
setConst: function(a) { for(var k=0;k&this.w.k++) { this.w[k] = }},
toJSON: function() {
// todo: we may want to only save d most significant digits to save space
var json = {}
json.sx = this.
json.sy = this.
json.depth = this.
json.w = this.w;
// we wont back up gradients to save space
fromJSON: function(json) {
this.sx = json.
this.sy = json.
this.depth = json.
var n = this.sx*this.sy*this.
this.w = global.zeros(n);
this.dw = global.zeros(n);
// copy over the elements.
for(var i=0;i&n;i++) {
this.w[i] = json.w[i];
global.Vol = V
})(convnetjs);
同上面的代码结构类似,定义Vol的类,并将其赋值给convnetjs的属性Vol.与上面不同的是这里定义的是类Vol。关于类Vol的定义,我只能说是一个完美学习JS类定义的程序。建议对于一个JS的入门者,先看书中的第七章类的定义。在返回看这里的Vol的定义。废话不说了,转入正题:
Vol的定义就是两句话,第一句话的格式是&var Vol = function(sx, sy, depth, c) {this.....}这样的形式,查看书中p145中关于构造函数的模式。第二句话的格式是Vol.prototype = {
& & get: function(x, y, d) {&
& & & var ix=((this.sx * y)+x)*this.depth+d;
& & & return this.w[ix];
& & set: function(x, y, d, v) {&
& & & var ix=((this.sx * y)+x)*this.depth+d;
& & & this.w[ix] =&
}这样的格式是定义类的一些公共属性。这些公共属性是指对于类的每一个实例,都会共享一个函数指针。这是必要的,因为对于每一个实例,如果都定义相应的函数,那将会浪费大量的内存。
其官方的关于Vol的说明如下:
The entire library is based around transforming 3-dimensional volumes of numbers. These volumes are stored in the, which is at the heart of the library. The Vol class is a wrapper around:
a 1-dimensional list of numbers (the activations, in field .w)their gradients (field .dw)and lastly contains three dimensions (fields .sx, .sy, .depth).
// create a Vol of size 32x32x3, and filled with random numbers
var v = new convnetjs.Vol(32, 32, 3);
var v = new convnetjs.Vol(32, 32, 3, 0.0); // same volume but init with zeros
var v = new convnetjs.Vol(1, 1, 3); // a 1x1x3 Vol with random numbers
// you can also initialize with a specific list. E.g. create a 1x1x3 Vol:
var v = new convnetjs.Vol([1.2, 3.5, 3.6]);
// the Vol is a wrapper around two lists: .w and .dw, which both have
// sx * sy * depth number of elements. E.g:
v.w[0] // contains 1.2
v.dw[0] // contains 0, because gradients are initialized with zeros
// you can also access the 3-D Vols with getters and setters
// but these are subject to function call overhead
var vol3d = new convnetjs.Vol(10, 10, 5);
vol3d.set(2,0,1,5.0); // set coordinate (2,0,1) to 5.0
vol3d.get(2,0,1) // returns 5.0
通过上面的代码分析,我们可以清楚的明白上面的例子。
2.4 .&convnet_vol_util.js
(function(global) {
&use strict&;
var Vol = global.V // convenience
// Volume utilities
// intended for use with data augmentation
// crop is the size of output
// dx,dy are offset wrt incoming volume, of the shift
// fliplr is boolean on whether we also want to flip left&-&right
var augment = function(V, crop, dx, dy, fliplr) {
// note assumes square outputs of size crop x crop
if(typeof(fliplr)==='undefined') var fliplr =
if(typeof(dx)==='undefined') var dx = global.randi(0, V.sx - crop);
if(typeof(dy)==='undefined') var dy = global.randi(0, V.sy - crop);
// randomly sample a crop in the input volume
if(crop !== V.sx || dx!==0 || dy!==0) {
W = new Vol(crop, crop, V.depth, 0.0);
for(var x=0;x&x++) {
for(var y=0;y&y++) {
if(x+dx&0 || x+dx&=V.sx || y+dy&0 || y+dy&=V.sy) // oob
for(var d=0;d&V.d++) {
W.set(x,y,d,V.get(x+dx,y+dy,d)); // copy data over
if(fliplr) {
// flip volume horziontally
var W2 = W.cloneAndZero();
for(var x=0;x&W.x++) {
for(var y=0;y&W.y++) {
for(var d=0;d&W.d++) {
W2.set(x,y,d,W.get(W.sx - x - 1,y,d)); // copy data over
W = W2; //swap
// img is a DOM element that contains a loaded image
// returns a Vol of size (W, H, 4). 4 is for RGBA
var img_to_vol = function(img, convert_grayscale) {
if(typeof(convert_grayscale)==='undefined') var convert_grayscale =
var canvas = document.createElement('canvas');
canvas.width = img.
canvas.height = img.
var ctx = canvas.getContext(&2d&);
// due to a Firefox bug
ctx.drawImage(img, 0, 0);
} catch (e) {
if (e.name === &NS_ERROR_NOT_AVAILABLE&) {
// sometimes happens, lets just abort
var img_data = ctx.getImageData(0, 0, canvas.width, canvas.height);
} catch (e) {
if(e.name === 'IndexSizeError') {
// not sure what causes this sometimes but okay abort
// prepare the input: get pixels and normalize them
var p = img_data.
var W = img.
var H = img.
var pv = []
for(var i=0;i&p.i++) {
pv.push(p[i]/255.0-0.5); // normalize image pixels to [-0.5, 0.5]
var x = new Vol(W, H, 4, 0.0); //input volume (image)
if(convert_grayscale) {
// flatten into depth=1 array
var x1 = new Vol(W, H, 1, 0.0);
for(var i=0;i&W;i++) {
for(var j=0;j&H;j++) {
x1.set(i,j,0,x.get(i,j,0));
global.augment =
global.img_to_vol = img_to_
})(convnetjs);
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:48529次
排名:千里之外
原创:31篇
转载:30篇
评论:22条
阅读:5922
阅读:2200
(1)(10)(11)(10)(7)(1)(1)(6)(2)(17)介绍了script中的compose函数的实现,我是用了递归的思想去让函数依次执行,lodash中是用了迭代的思想依次执行函数,但实现了以后我还是觉得有些别扭,仔细想想,我们实现的是一个函数式用到的函数,但是实现的方法还是太命令式了,函数还是命令式的执行,通俗点说,还是太把函数当成函数了,在我的理解中,函数和普通变量没什么区别,只是执行的方法不一样,一旦赋予了函数这个执行的属性,我们就可以完全将函数当成普通变量去对待。
函数和普通变量没什么区别,只是需要偶尔执行一下
1.函数世界的加号
'a' + 'b' = 'ab'
func1 '+' func2 -& func3
前两个例子就是普通变量的操作,最后一个例子是函数的操作,本质上看来,没有任何区别,两个函数作用的结果就是生成一个函数,只不过在函数的世界里,这个加号的意义就是如何变换生成一个新的函数,回到compose来,在compose中,加号的意义就是把一个函数的执行结果当成下一个函数的输入,最后在生成一个函数,就像下面这样
var fn = (func1, func2) =& (...args) =& func2.call(this, func1.apply(this, args))
在这个例子里面,func1的执行结果就是func2的参数,并且生成了一个新的函数fn,我们给这个fn传递参数,它就会作为func1的参数来启动执行,最后得到了函数依次执行的效果,这就是最简单的compose,这个函数就是ramda.js实现compsoe需要的第一个函数_pipe
var _pipe = (f, g) =& (...args) =& g.call(this, f.apply(this, args))
_pipe就定义了compose中所谓加号的意义了。
2.'不一样的'reduce
在这里提到了reduce,是不是有一点感觉,reduce的作用就是让一个数组不断的执行下去,所以肯定能和咱们这个compose有点联系,先举个reduce最常用的例子,求数组的和
var a = [1,2,3,4,5]
a.reduce((x, y) =& x + y, 0)
这个就是不断的将两个数求和,生成一个新的数,再去和下一个数求和,最后得到15,下面想一下,如果把数字换成函数会怎么样,两个函数结合生成一个新的函数,这个结合法则就使用上面的_pipe,这个新的函数再去结合下一个函数,直到最后一个函数执行完,我们得到的还是函数,我们前面说了,函数偶尔需要执行一下,这个函数的生成和执行过程是反向递归的过程。利用这个思想,就可以寥寥几行(甚至只需要一行)就写出来这个非常函数式的compose了
var reverse = arr =& arr.reverse()
var _pipe = (f, g) =& (...args) =& g.call(this, f.apply(this, args));
var compose = (...args) =& reverse(args).reduce(_pipe, args.shift())
举个例子验证一下,我们把首个函数做多元处理,再upperCase,再repeat
var classyGreeting = (firstName, lastName) =& "The name's " + lastName + ", " + firstName + " " + lastName
var toUpper = str =& str.toUpperCase()
var repeat = str =& str.repeat(2)
var result = compose(repeat, toUpper, classyGreeting)('dong', 'zhe')
// THE NAME'S ZHE, DONG ZHETHE NAME'S ZHE, DONG ZHE
我在这里把函数生成过程分析一下
首先我们用_pipe组合classyGreeting,toUpper
f1 = _pipe(classyGreeting, toUpper)
f1 = (...args) =& toUpper.call(this, classyGreeting.apply(this, args))
_pipe继续结合f1, repeat
f2 = _pipe(f1, repeat)
f2 = (...args) =& repeat.call(this, f1.apply(this, args))
函数的执行过程就会将参数层层传递到最里面的classyGreeting开始执行,从而完成函数的依次执行。ramda.js自己实现了reduce,不仅支持数组的reduce,还支持多种数据结构的reduce,(也更好?),下一步来分析是如何自己实现数组的reduce的,可与看出,自己分离出来逻辑之后,函数的执行过程和组合的规则部分将分离的更彻底。
3.自己写一个reduce
reduce接受三个参数,执行函数,初始值,执行队列(可以不止为一个数组),返回一个针对这些参数的reduce处理,这里只写数组部分(_arrayReduce),源码中还包含了关于迭代器的_iterableReduce 等等,而且ramda.js对执行函数也有一层对象封装,扩展了函数的功能
var reduce = (fn, acc, list) =& (fn = _xwrap(fn), _arrayReduce(fn, acc, list))
在写_arrayReduce之前,先来看一下函数的对象封装_xwrap
var _xwrap = (function(){
function XWrap(fn) {
XWrap.prototype['@@transducer/init'] = function() {
throw new Error('init not implemented on XWrap');
XWrap.prototype['@@transducer/result'] = function(acc) {
XWrap.prototype['@@transducer/step'] = function(acc, x) {
return this.f(acc, x);
return function _xwrap(fn) { return new XWrap(fn); };
其实就是对函数执行状态做了一个分类管理@@transducer/step 这种状态认为是一种过程状态@@transducer/result 这种状态被认为是一种结果状态这种状态管理通过对象也是合情合理的最后再来完成_arrayReduce,就很简单了,这个函数只是专心一件事情,就是写reduce的过程规则。
var _arrayReduce = (xf, acc, list) =& {
var idx = 0
var len = list.length
while (idx & len) {
acc = xf['@@transducer/step'](acc, list[idx]);
return xf['@@transducer/result'](acc);
至此,ramda.js简化版的reduce就完成了。
4.其他一些功能
tail用来分离初始值和执行队列的,因为初始函数是多元的(接收多个参数),执行队列都是一元(接收一个参数)的,分离还是有必要的
var tail = arr =& arr.slice(1)
reverse改变执行顺序
var reverse = arr =& arr.reverse()
_arity我把贴出来,我也不知道为什么这样做,可能是明确指定参数吧,因为reduce生成的函数是可以接受多个参数的,_arity就是处理这个函数的
var _arity = (n, fn) =& {
switch (n) {
case 0: return function() { return fn.apply(this, arguments); };
case 1: return function(a0) { return fn.apply(this, arguments); };
case 2: return function(a0, a1) { return fn.apply(this, arguments); };
case 3: return function(a0, a1, a2) { return fn.apply(this, arguments); };
case 4: return function(a0, a1, a2, a3) { return fn.apply(this, arguments); };
case 5: return function(a0, a1, a2, a3, a4) { return fn.apply(this, arguments); };
case 6: return function(a0, a1, a2, a3, a4, a5) { return fn.apply(this, arguments); };
case 7: return function(a0, a1, a2, a3, a4, a5, a6) { return fn.apply(this, arguments); };
case 8: return function(a0, a1, a2, a3, a4, a5, a6, a7) { return fn.apply(this, arguments); };
case 9: return function(a0, a1, a2, a3, a4, a5, a6, a7, a8) { return fn.apply(this, arguments); };
case 10: return function(a0, a1, a2, a3, a4, a5, a6, a7, a8, a9) { return fn.apply(this, arguments); };
default: throw new Error('First argument to _arity must be a non-negative integer no greater than ten');
最后整合出来两个最终的函数pipe和compose
var pipe = (...args) =& _arity(args[0].length, reduce(_pipe, args[0], tail(args)))
var remdaCompose = (...args) =& pipe.apply(this, reverse(args))
再把上面的demo试一下
console.log(remdaCompose(repeat, toUpper, classyGreeting)('dong', 'zhe'))
// THE NAME'S ZHE, DONG ZHETHE NAME'S ZHE, DONG ZHE
整合的完全版我放到了里
这篇文章主要分析了ramda.js实现compose的过程,其中分析了如何把函数看成一等公民,如何实现一个reduce等等。可以看出,compose的实现从头到尾都是函数式编程的思想,下一篇文章打算结合社区的一道问答题来介绍一下如何用函数式思想来解决问题。我也是初学函数式,有什么说的不准确的地方希望多多指正。一直想针对一个框架的源码好好的学习一下编程思想和技巧,提高一下自己的水平,但是看过一些框架的源码,都感觉看的莫名其妙,看不太懂,最后找到这个underscore.js由于这个比较简短,一千多行,而且读起来容易一些,所以就决定是它了,那废话不多说开始我们的源码学习。
underscore.js源码GitHub地址:&
本文解析的underscore.js版本是1.8.3
我们先从整体的结构开始分析(其中加入了注释加以解释说明)
1 (function() {
// 创建一个root对象,在浏览器中表示为window(self)对象,在Node.js中表示global对象,
// 之所以用用self代替window是为了支持Web Worker
var root = typeof self == 'object' && self.self === self && self ||
typeof global == 'object' && global.global === global && global ||
// 保存"_"(下划线变量)被覆盖之前的值
var previousUnderscore = root._;
// 原型赋值,便于压缩
var ArrayProto = Array.prototype, ObjProto = Object.
// 将内置对象原型中的常用方法赋值给引用变量,以便更方便的引用
var push = ArrayProto.push,
slice = ArrayProto.slice,
toString = ObjProto.toString,
hasOwnProperty = ObjProto.hasOwnP
// 定义了一些ECMAScript 5方法
var nativeIsArray = Array.isArray,
nativeKeys = Object.keys,
nativeCreate = Object.
//跟神马裸函数有关,我也不清楚什么意思,有知道可以告诉我
var Ctor = function(){};
// 创建一个下划线对象
var _ = function(obj) {
// 如果在"_"的原型链上(即_的prototype所指向的对象是否跟obj是同一个对象,要满足"==="的关系)
if (obj instanceof _) return
// 如果不是,则构造一个
if (!(this instanceof _)) return new _(obj);
// 将underscore对象存放在_.wrapped属性中
this._wrapped =
// 针对不同的宿主环境, 将Undersocre的命名变量存放到不同的对象中
if (typeof exports != 'undefined' && !exports.nodeType) {//Node.js
if (typeof module != 'undefined' && !module.nodeType && module.exports) {
exports = module.exports = _;
exports._ = _;
} else {//浏览器
root._ = _;
_.VERSION = '1.8.3';
//下面是各种方法以后的文章中会具体说明
// 创建一个chain函数,用来支持链式调用
_.chain = function(obj) {
var instance = _(obj);
//是否使用链式操作
instance._chain = true;
// 返回_.chain里是否调用的结果, 如果为true, 则返回一个被包装的Underscore对象, 否则返回对象本身
var chainResult = function(instance, obj) {
return instance._chain ? _(obj).chain() :
// 用于扩展underscore自身的接口函数
_.mixin = function(obj) {
//通过循环遍历对象来浅拷贝对象属性
_.each(_.functions(obj), function(name) {
var func = _[name] = obj[name];
_.prototype[name] = function() {
var args = [this._wrapped];
push.apply(args, arguments);
return chainResult(this, func.apply(_, args));
_.mixin(_);
// 将Array.prototype中的相关方法添加到Underscore对象中, 这样Underscore对象也可以直接调用Array.prototype中的方法
_.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
//方法引用
var method = ArrayProto[name];
_.prototype[name] = function() {
// 赋给obj引用变量方便调用
var obj = this._
// 调用Array对应的方法
method.apply(obj, arguments);
if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0];
//支持链式操作
return chainResult(this, obj);
// 同上,并且支持链式操作
_.each(['concat', 'join', 'slice'], function(name) {
var method = ArrayProto[name];
_.prototype[name] = function() {
//返回Array对象或者封装后的Array
return chainResult(this, method.apply(this._wrapped, arguments));
//返回存放在_wrapped属性中的underscore对象
_.prototype.value = function() {
return this._
// 提供一些方法方便其他情况使用
_.prototype.valueOf = _.prototype.toJSON = _.prototype.
_.prototype.toString = function() {
return '' + this._
// 对AMD支持的一些处理
if (typeof define == 'function' && define.amd) {
define('underscore', [], function() {
具体分析在上面源码中的注释里写的已经很详细了,下面再从头理顺一下整体的结构:
首先underscore包裹在一个匿名自执行的函数当中
内部定义了一个"_"变量
将underscore中的相关方法添加到_原型中,创建的_对象就具备了underscore方法
将Array.prototype中的相关方法添加到Underscore对象中, 这样Underscore对象也可以直接调用Array.prototype中的方法
之后的文章中,我会针对underscore中的方法进行具体解析,感谢大家的观看,也希望能够和大家互相交流学习,有什么分析的不对的地方欢迎大家批评指出
阅读(...) 评论()

我要回帖

更多关于 js解析json 的文章

 

随机推荐