ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
[TOC] # instanceof [参考链接](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/instanceof) `instanceof` 运算符用来测试一个对象在其原型链中是否存在一个构造函数的`prototype`属性 `object instanceof constructor`,`object`要检测的对象,`constructor`某个构造函数 ```javascript // 定义构造函数 function C(){} function D(){} var o = new C(); o instanceof C; // true 因为Object.getPrototypeOf(o) === C.prototype o instanceof D; // false 因为D.prototype不在o的原型链上 o instanceof Object; // true 因为Object.prototype.isPrototypeOf(o) 返回true C.prototype instanceof Object // true 同上 C.prototype = {}; var o2 = new C(); o2 instanceof C; // true o instanceof C; // false C.prototype指向了一个空对象,这个空对象不再o的原型链上 D.prototype = new C(); //继承 var o3 = new D(); o3 instanceof D; // true o3 instanceof C; // true ``` # Symbol 创建私有属性 [参考链接1](https://developer.mozilla.org/zh-CN/docs/Glossary/Symbol) [参考链接2](http://es6.ruanyifeng.com/#docs/symbol) `Symbol`为一种数据类型,可以使用此类型的值来创建匿名的对象属性,此类型作为一个私有对象属性的键,用于类或对象类型的内部使用。 ```javascript var privateVal = Symbol(); this[privateVal] = function(){}; ``` 该属性为匿名且不可枚举,访问全局`Symbol`的方式为`Symbol.for()`和`Symbol.keyFor()` `Symbol`值作为属性名时,该属性还是公开属性,不是私有属性 消除魔术字符串 ```javascript const shapeType = { triangle: 'Triangle' }; function getArea(shape, options) { let area = 0; switch (shape) { case shapeType.triangle: area = .5 * options.width * options.height; break; } return area; } getArea(shapeType.triangle, { width: 100, height: 100 }); // 改写为 const shapeType = { triangle: Symbol() }; ``` # Object.create [参考链接1](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/create) [参考链接2](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty) `Object.create(proto, [propertiesObject])` 方法会使用指定的原型对象及属性去创建一个新的对象,`proto`一个对象,新创建对象的原型,`propertiesObject`一组属性与值,该对象的属性名称将是新创建对象的熟悉名称 单继承 ```javascript //Shape - 超类 function Shape() { this.x = 0; this.y = 0; } Shape.prototype.move = function(x, y) { this.x += x; this.y += y; console.info("Shape moved."); }; // Rectangle - 子类 function Rectangle() { Shape.call(this); //超类的this指向子类. } // 子类继承超类 Rectangle.prototype = Object.create(Shape.prototype); Rectangle.prototype.constructor = Rectangle; // 将构造器函数指向子类本身 var rect = new Rectangle(); console.log('Is rect an instance of Rectangle?', rect instanceof Rectangle); // true console.log('Is rect an instance of Shape?', rect instanceof Shape); // true rect.move(1, 1); //Outputs, "Shape moved." ``` 多对象继承 ```javascript function MyClass() { SuperClass.call(this); OtherSuperClass.call(this); } // 继承其中一个超类 MyClass.prototype = Object.create(SuperClass.prototype); // 通过合并prototype继承另一个超类 Object.assign(MyClass.prototype, OtherSuperClass.prototype); // 将prototype的构造器指向本身 MyClass.prototype.constructor = MyClass; MyClass.prototype.myMethod = function() { // do a thing }; ``` propertyObject参数 ```javascript var o; // 创建一个原型为null的空对象 o = Object.create(null); o = {}; // 以字面量方式创建的空对象就相当于: o = Object.create(Object.prototype); o = Object.create(Object.prototype, { // foo会成为所创建对象的数据属性 foo: { writable:true, configurable:true, value: "hello" }, // bar会成为所创建对象的访问器属性 bar: { configurable: false, get: function() { return 10 }, set: function(value) { console.log("Setting `o.bar` to", value); } } }); function Constructor(){} o = new Constructor(); // 上面的一句就相当于: o = Object.create(Constructor.prototype); // 当然,如果在Constructor函数中有一些初始化代码,Object.create不能执行那些代码 // 创建一个以另一个空对象为原型,且拥有一个属性p的对象 o = Object.create({}, { p: { value: 42 } }) // 省略了的属性特性默认为false,所以属性p是不可写,不可枚举,不可配置的: o.p = 24 o.p //42 o.q = 12 for (var prop in o) { console.log(prop) } //"q" delete o.p //false //创建一个可写的,可枚举的,可配置的属性p o2 = Object.create({}, { p: { value: 42, writable: true, enumerable: true, configurable: true } }); ``` 兼容写法 ```javascript if (typeof Object.create !== "function") { Object.create = function (proto, propertiesObject) { if (!(proto === null || typeof proto === "object" || typeof proto === "function")) { throw TypeError('Argument must be an object, or null'); } var temp = new Object(); temp.__proto__ = proto; if(typeof propertiesObject ==="object") Object.defineProperties(temp,propertiesObject); return temp; }; } return Object.create(proto, propertiesObject) ``` # 继承与原型链 [参考链接1](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Inheritance_and_the_prototype_chain) 每个对象都有一个私有属性[[Prototype]],它持有一个连接到另一个称其为`prototype`对象的链接。`null`没有`prototype`。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。 可以用`Object.getPrototypeOf()`和`Object.setPrototypeOf()`进行访问。 当继承的函数被调用时,`this` 指向的是当前继承的对象,而不是继承的函数所在的原型对象。 要清楚代码中原型链的长度,并在必要时结束原型链,以避免可能存在的性能问题。此外,除非为了兼容新 JavaScript 特性,否则,永远不要扩展原生的对象原型。 # call、apply和bind [参考链接1](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this) `this`的指向和函数的调用方式有关系。 函数直接调用,`this`指向全局对象 ```javascript function f1(){ return this; } //在浏览器中: f1() === window; //在浏览器中,全局对象是window //在Node中: f1() === global; // 严格模式下则会变为undefined function f2(){ "use strict"; // 这里是严格模式 return this; } f2() === undefined; // true ``` 通过`call`和`apply`方法改变`this`的指向,从一个`context`传到另一个 ```javascript // 一个对象可以作为call和apply的第一个参数,并且this会被绑定到这个对象。 var obj = {a: 'Custom'}; // 这个属性是在global对象定义的。 var a = 'Global'; function whatsThis(arg) { return this.a; // this的值取决于函数的调用方式 } whatsThis(); // 直接调用, 返回'Global' whatsThis.call(obj); // 通过call调用, 返回'Custom' whatsThis.apply(obj); // 通过apply调用 ,返回'Custom' ``` ```javascript function add(c, d) { return this.a + this.b + c + d; } var o = {a: 1, b: 3}; // 第一个参数是作为‘this’使用的对象 // 后续参数作为参数传递给函数调用 add.call(o, 5, 7); // 1 + 3 + 5 + 7 = 16 // 第一个参数也是作为‘this’使用的对象 // 第二个参数是一个数组,数组里的元素用作函数调用中的参数 add.apply(o, [10, 20]); // 1 + 3 + 10 + 20 = 34 ``` `bind`,调用`f.bind(xx)`会创建一个与f具有相同函数体和作用域的函数,`this`将永久被绑定到了`bind`的第一个参数,无论这个函数如何被调用 ```javascript function f(){ return this.a; } //this被固定到了传入的对象上 var g = f.bind({a:"azerty"}); console.log(g()); // azerty var h = g.bind({a:'yoo'}); //bind只生效一次! console.log(h()); // azerty var o = {a:37, f:f, g:g, h:h}; console.log(o.f(), o.g(), o.h()); // 37, azerty, azerty ``` `箭头函数`的`this`是根据当前的词法的作用域来决定的 ```javascript var globalObject = this; var foo = (() => this); console.log(foo() === globalObject); // true ``` 无法通过`call`、`apply`或者`bind`进行改变 ```javascript // 接着上面的代码 // 作为对象的一个方法调用 var obj = {foo: foo}; console.log(obj.foo() === globalObject); // true // 尝试使用call来设定this console.log(foo.call(obj) === globalObject); // true // 尝试使用bind来设定this foo = foo.bind(obj); console.log(foo() === globalObject); // true ``` `this`被当做对象的一个方法调用是,指向的是调用该函数的对象 ```javascript var o = { prop: 37, f: function() { return this.prop; } }; console.log(o.f()); // logs 37 ``` 不受函数定义方式或者位置的影响,只和调用方式有关 ```javascript var o = {prop: 37}; function independent() { return this.prop; } o.f = independent; console.log(o.f()); // logs 37 ``` 在原型链中也是一致,`this`指向的是调用改方法的对象 ```javascript var o = { f : function(){ return this.a + this.b; } }; var p = Object.create(o); p.a = 1; p.b = 4; console.log(p.f()); // 5 ``` 在`getter`和`setter`中,会把`this`绑定到正在设置或获取属性的对象 ```javascript function sum() { return this.a + this.b + this.c; } var o = { a: 1, b: 2, c: 3, get average() { return (this.a + this.b + this.c) / 3; } }; Object.defineProperty(o, 'sum', { get: sum, enumerable: true, configurable: true}); console.log(o.average, o.sum); // logs 2, 6 ``` 当一个函数用作构造函数时(`new`),它的`this`被绑定到正在构造的新对象 ```javascript /* * 构造函数这样工作: * * function MyConstructor(){ * // 函数实体写在这里 * // 根据需要在this上创建属性,然后赋值给它们,比如: * this.fum = "nom"; * // 等等... * * // 如果函数具有返回对象的return语句,则该对象将是 new 表达式的结果。 * // 否则,表达式的结果是当前绑定到 this 的对象。 * //(即通常看到的常见情况)。 * } */ function C(){ this.a = 37; } var o = new C(); console.log(o.a); // logs 37 function C2(){ this.a = 37; return {a:38}; // 手动设置了返回,若为对象则 this.a 变为`僵尸代码` } o = new C2(); console.log(o.a); // logs 38 ``` # 变量的作用域 没有加上`var`关键字则代表全局作用域。 如果在函数内定义变量,则变量的作用域在函数体内。 如果在函数外定义变量,则可以在函数内调用。 # 变量的生存周期 函数内调用的变量,则生存周期在函数调用期间有效 # 闭包 [参考链接1](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures) [参考链接2](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Details_of_the_Object_Model) 被作用域封闭的变量,函数等执行的一个函数的作用域,可以在一定程度上延长变量的生存周期。 由两部分构成:函数,以及创建该函数的环境。 ```javascript // 创建一个闭包函数 function makeAdder(x) { return function(y) { return x + y; }; } var add5 = makeAdder(5); // 第一次调用 返回一个function(y){...}的匿名函数,这时保存了变量x的值,不会因为makeAdder函数执行结束而销毁 var add10 = makeAdder(10); console.log(add5(2)); // 7,第二次调用 console.log(add10(2)); // 12 ``` 用闭包模拟私有方法 ```javascript var makeCounter = function() { // 创建一个环境 var privateCounter = 0; // 只能在该环境中访问到的变量,外界无法访问 function changeBy(val) { privateCounter += val; } return { // 定义接口 increment: function() { changeBy(1); }, decrement: function() { changeBy(-1); }, value: function() { return privateCounter; } } }; var Counter1 = makeCounter(); // 再分配到不同的环境中 var Counter2 = makeCounter(); console.log(Counter1.value()); /* logs 0 */ Counter1.increment(); Counter1.increment(); console.log(Counter1.value()); /* logs 2 */ Counter1.decrement(); console.log(Counter1.value()); /* logs 1 */ console.log(Counter2.value()); /* logs 0 */ ``` 匿名闭包 ```javascript function showHelp(help) { document.getElementById('help').innerHTML = help; } function setupHelp() { var helpText = [ {'id': 'email', 'help': 'Your e-mail address'}, {'id': 'name', 'help': 'Your full name'}, {'id': 'age', 'help': 'Your age (you must be over 16)'} ]; for (var i = 0; i < helpText.length; i++) { (function() { // 每次循环都创建一个新的环境 var item = helpText[i]; document.getElementById(item.id).onfocus = function() { showHelp(item.help); } })(); // Immediate event listener attachment with the current value of item (preserved until iteration). } } setupHelp(); ``` # 高阶函数 [参考链接1](http://www.cnblogs.com/laixiangran/p/5468567.html) 满足其中一个条件即可:函数可以作为参数被传递;函数可以作为返回值输出。 显然`js`中的函数满足高阶函数的需求,例如`ajax`和`Array.prototype.sort` # AOP [参考链接1](http://www.cnblogs.com/laixiangran/p/5468567.html) 面向切面编程的主要作用是把一些跟核心业务逻辑模块无关的功能抽离出来,这些跟业务逻辑无关的功能通常包括日志统计、安全控制、异常处理等。把这些功能抽离出来之后,再通过“动态织入”的方式掺入业务逻辑模块中。 ```javascript Function.prototype.before = function(beforefn) { var __self = this; // 保存原函数的引用 return function() { // 返回包含了原函数和新函数的"代理"函数 beforefn.apply(this, arguments); // 执行新函数,修正this return __self.apply(this, arguments); // 执行原函数 } }; Function.prototype.after = function(afterfn) { var __self = this; return function() { var ret = __self.apply(this, arguments); afterfn.apply(this, arguments); return ret; } }; var func = function() { console.log(2); }; func = func.before(function() { console.log(1); }).after(function() { console.log(3); }); func(); // 按顺序打印出1,2,3 ``` # 函数科里化 [参考链接1](http://www.cnblogs.com/laixiangran/p/5468567.html) 部分求值。一个currying的函数首先会接受一些参数,接受了这些参数之后,该函数并不会立即求值,而是继续返回另外一个函数,刚才传入的参数在函数形成的闭包中被保存起来。待到函数被真正需要求值的时候,之前传入的所有参数都会被一次性用于求值。 ```javascript // 通用currying函数,接受一个参数,即将要被currying的函数 var currying = function(fn) { var args = []; return function() { if (arguments.length === 0) { return fn.apply(this, args); } else { [].push.apply(args, arguments); return arguments.callee; } } }; // 将被currying的函数 var cost = (function() { var money = 0; return function() { for (var i = 0, l = arguments.length; i < l; i++) { money += arguments[i]; } return money; } })(); var cost = currying( cost ); // 转化成currying函数 cost( 100 ); // 未真正求值 cost( 200 ); // 未真正求值 cost( 300 ); // 未真正求值 console.log (cost()); // 求值并输出:600 ``` # uncurrying [参考链接1](http://www.cnblogs.com/laixiangran/p/5468567.html) 将泛化this的过程提取出来,将fn.call或者fn.apply抽象成通用的函数。 ```javascript // uncurrying实现 Function.prototype.uncurrying = function() { var self = this; return function() { return Function.prototype.call.apply(self, arguments); } }; // 将Array.prototype.push进行uncurrying,此时push函数的作用就跟Array.prototype.push一样了,且不仅仅局限于只能操作array对象。 var push = Array.prototype.push.uncurrying(); var obj = { "length": 1, "0": 1 }; push(obj, 2); console.log(obj); // 输出:{0: 1, 1: 2, length: 2} ``` # 函数节流 [参考链接1](http://www.cnblogs.com/laixiangran/p/5468567.html) 当一个函数被频繁调用时,如果会造成很大的性能问题的时候,这个时候可以考虑函数节流,降低函数被调用的频率。 throttle函数的原理是,将即将被执行的函数用setTimeout延迟一段时间执行。如果该次延迟执行还没有完成,则忽略接下来调用该函数的请求。throttle函数接受2个参数,第一个参数为需要被延迟执行的函数,第二个参数为延迟执行的时间。 ```javascript var throttle = function(fn, interval) { var __self = fn, // 保存需要被延迟执行的函数引用 timer, // 定时器 firstTime = true; // 是否是第一次调用 return function() { var args = arguments, __me = this; if (firstTime) { // 如果是第一次调用,不需延迟执行 __self.apply(__me, args); return firstTime = false; } if (timer) { // 如果定时器还在,说明前一次延迟执行还没有完成 return false; } timer = setTimeout(function() { // 延迟一段时间执行 clearTimeout(timer); timer = null; __self.apply(__me, args); }, interval || 500 ); }; }; window.onresize = throttle(function() { console.log(1); }, 500 ); ``` # 分时函数 [参考链接1](http://www.cnblogs.com/laixiangran/p/5468567.html) 当一次的用户操作会严重地影响页面性能,如在短时间内往页面中大量添加DOM节点显然也会让浏览器吃不消,我们看到的结果往往就是浏览器的卡顿甚至假死。 timeChunk函数接受3个参数,第1个参数是创建节点时需要用到的数据,第2个参数是封装了创建节点逻辑的函数,第3个参数表示每一批创建的节点数量。 ```javascript var timeChunk = function(ary, fn, count) { var t; var start = function() { for ( var i = 0; i < Math.min( count || 1, ary.length ); i++ ){ var obj = ary.shift(); fn( obj ); } }; return function() { t = setInterval(function() { if (ary.length === 0) { // 如果全部节点都已经被创建好 return clearInterval(t); } start(); }, 200); // 分批执行的时间间隔,也可以用参数的形式传入 }; }; ``` # 惰性加载函数 [参考链接1](http://www.cnblogs.com/laixiangran/p/5468567.html) 在Web开发中,因为浏览器之间的实现差异,一些嗅探工作总是不可避免。比如我们需要一个在各个浏览器中能够通用的事件绑定函数addEvent,常见的写法如下: 方案一: ```javascript var addEvent = function(elem, type, handler) { if (window.addEventListener) { return elem.addEventListener(type, handler, false); } if (window.attachEvent) { return elem.attachEvent('on' + type, handler); } }; ``` 缺点:当它每次被调用的时候都会执行里面的if条件分支,虽然执行这些if分支的开销不算大,但也许有一些方法可以让程序避免这些重复的执行过程。 方案二: ```javascript var addEvent = (function() { if (window.addEventListener) { return function(elem, type, handler) { elem.addEventListener(type, handler, false); } } if (window.attachEvent) { return function(elem, type, handler) { elem.attachEvent('on' + type, handler); } } })(); ``` 缺点:也许我们从头到尾都没有使用过addEvent函数,这样看来,一开始的浏览器嗅探就是完全多余的操作,而且这也会稍稍延长页面ready的时间。 方案三: ```javascript var addEvent = function(elem, type, handler) { if (window.addEventListener) { addEvent = function(elem, type, handler) { elem.addEventListener(type, handler, false); } } else if (window.attachEvent) { addEvent = function(elem, type, handler) { elem.attachEvent('on' + type, handler); } } addEvent(elem, type, handler); }; ``` 此时addEvent依然被声明为一个普通函数,在函数里依然有一些分支判断。但是在第一次进入条件分支之后,在函数内部会重写这个函数,重写之后的函数就是我们期望的addEvent函数,在下一次进入addEvent函数的时候,addEvent函数里不再存在条件分支语句。