likes
comments
collection
share

前端灵魂~,函数式编程,也是成为JS高手最不可或缺的一块技能版图

作者站长头像
站长
· 阅读数 47

前言

函数之于JavaSciprt,就如面向对象之于Java,函数是JS的一等公民,也是理解Vue、React等现代化框架所倡导的函数式编程的必备知识

1.执行栈

执行栈(调用栈),是一种拥有后进先出数据结构的栈(想象一个桶,放里面放东西,越晚放进去得东西在越上面),被用来存储代码运行时创建的所有执行上下文

function outer() {
  var outerName = "outerName";
  console.log(outerName);
  inner();
}

function inner() {
  var innerName = "innerName";
  console.log(innerName);
}

outer();
// 一开始只有全局上下文
// 执行outer函数生成outer上下文
// outer函数里面执行了inner函数,生成inner上下文
// 执行完inner函数释放其上下文,然后就又回到outer函数上下文执行余下代码后释放其上下文
// 最后又只剩下了全局上下文

2.Function.name

作用:

  • 递归
  • 调试和跟踪错误和调用栈的信息

具名函数

function sum1(num1, num2) {
  return num1 + num2;
}

const sum2 = (num1, num2) => {
  return num1 + num2;
};

console.log("name:", sum1.name); // name: sum1
console.log("name:", sum2.name); // name: sum2

匿名函数

const person1 = {
  name: "Yunmu1",
  getName: function () {
    return this.name;
  },
};

const person2 = {
  name: "Yunmu2",
  getName() {
    return this.name;
  },
};

const person3 = {
  name: "Yunmu3",
};
person3.getName = function () {
  return this.name;
};

const person4 = {
  name: "Yunmu4",
  getName: function getNameMethod() {
    return this.name;
  },
};

console.log("person1.getName.name:", person1.getName.name); // person1.getName.name: getName
console.log("person2.getName.name:", person2.getName.name); // person2.getName.name: getName
console.log("person3.getName.name:", person3.getName.name); // person3.getName.name:
console.log("person4.getName.name:", person4.getName.name); // person4.getName.name: getNameMethod

new Function

// 可以传递任意数量的参数给Function构造函数 只有最后一个参数会被当做函数体
// 如果只有一个参数,该参数就是函数体
const addFn = new Function("num1", "num2", "return num1 + num2");

console.log(addFn(1, 2)); // 3

console.log("name:", addFn.name); // name: anonymous

bind

function sum(num1, num2) {
  return num1 + num2;
}

const sumBound = sum.bind({ a: 1 });
console.log("sumBound.name:", sumBound.name); // sumBound.name: bound sum

getter和setter

const person = {
  _name: "Yunmu",
  get name() {
    return this._name;
  },
  set name(val) {
    this._name = val;
  },
};

const descriptor = Object.getOwnPropertyDescriptor(person, "name");
console.log("get.name:", descriptor.get.name); // get.name: get name
console.log("set.name:", descriptor.set.name); // set.name: set name

Symbol

const symbolGetName1 = Symbol("getName1");
const symbolGetName2 = Symbol("getName2");
const symbolGetName3 = Symbol("getName3");
const person = {
  name: "Tom",
  [symbolGetName1]: function () {
    return this.name;
  },
  [symbolGetName2]() {
    return this.name;
  },
  [symbolGetName3]: function getNameMethod() {
    return this.name;
  },
};

console.log("symbolGetName1.name:", person[symbolGetName1].name); // symbolGetName1.name: [getName1]

console.log("symbolGetName2.name:", person[symbolGetName2].name); // symbolGetName2.name: [getName2]

console.log("symbolGetName3.name:", person[symbolGetName3].name); // symbolGetName3.name: getNameMethod

3.Function.length

  • length是函数对象的一个属性值,指的是接受形参的个数,作用可以用来函数柯里化
function sum(num1, num2) {
  return num1 + num2;
}

console.log("length:", sum.length); // length: 2

不包含剩余参数

function sum(num1, num2, ...args) {
  console.log("...args:", ...args);
  return num1 + num2;
}

console.log("length:", sum.length); // length: 2

参数默认值的情况

  • 全部有默认值,Function.length为0
  • 非全部包含默认值,length等于第一个具有默认值之前的参数个数
function sum(num1, num2 = 1) {
  return num1 + num2;
}

console.log("length:", sum.length); // length: 1

bind之后的length

  • length = 函数的 length - bind 的参数个数
  • 最小值为0
function sum(num1, num2, num3) {
  return num1 + num2 + num3;
}

console.log("sum.length", sum.length); // sum.length 3

const boundSum0 = sum.bind(null);
console.log("boundSum0.length:", boundSum0.length); // boundSum0.length: 3

const boundSum1 = sum.bind(null, 1);
console.log("boundSum1.length:", boundSum1.length); // boundSum1.length: 2

const boundSum2 = sum.bind(null, 1, 2);
console.log("boundSum2.length:", boundSum2.length); // boundSum2.length: 1

const boundSum3 = sum.bind(null, 1, 2, 3);
console.log("boundSum3.length:", boundSum3.length); // boundSum3.length: 0

const boundSum4 = sum.bind(null, 1, 2, 3, 4);
console.log("boundSum4.length:", boundSum4.length); // boundSum4.length: 0

与arguments.length区别

  • arguments.length是实际参数长度
  • Function.length是形参的长度
function sum(num1, num2) {
  console.log("arguments.length:", arguments.length); // arguments.length: 4
  return num1 + num2;
}

sum(1, 2, 3, 4);
console.log("length:", sum.length); // length: 2

4.链式调用

  • 数组、Promise、Rxjs、lodash、JQuery都有链式调用的影子,它很普遍和常用
  • 本质其实都是返回对象本身同类型的实例对象

优点

  • 代码语义化和可读性增强
  • 代码简洁
  • 易于维护

缺点

  • 逻辑要求高
  • 调试不方便

适用场景

  • 多次计算赋值
  • 逻辑有特定顺序
  • 相似业务集中处理

案例:计算器

返回对象本身

class Calculator {
	constructor(val) {
		this.val = val;
	}

	double() {
		this.val = this.val * 2;
		return this;
	}

	add(num) {
		this.val = this.val + num;
		return this;
	}

	minus(num) {
		this.val = this.val - num;
		return this;
	}

	multi(num) {
		this.val = this.val * num;
		return this;
	}

	divide(num) {
		this.val = this.val / num;
		return this;
	}

	pow(num) {
		this.val = Math.pow(this.val, num);
		return this;
	}

	// ES5 getter, 表现得像个属性,实则是一个方法
	get value() {
		return this.val;
	}
}

const cal = new Calculator(10);

const val = cal
	.add(10) // 20
	.minus(5) // 15
	.double() // 30
	.multi(10) // 300
	.divide(2) // 150
	.pow(2).value; // 22500

console.log(val); // 22500

返回同类型实例对象

class Calculator {
	constructor(val) {
		this.val = val;
	}

	double() {
		const val = this.val * 2;
		return new Calculator(val);
	}

	add(num) {
		const val = this.val + num;
		return new Calculator(val);
	}

	minus(num) {
		const val = this.val - num;
		return new Calculator(val);
	}

	multi(num) {
		const val = this.val * num;
		return new Calculator(val);
	}

	divide(num) {
		const val = this.val / num;
		return new Calculator(val);
	}

	pow(num) {
		const val = Math.pow(this.val, num);
		return new Calculator(val);
	}

	get value() {
		return this.val;
	}
}

const cal = new Calculator(10);

const val = cal
	.add(10) // 20
	.minus(5) // 15
	.double() // 30
	.multi(10) // 300
	.divide(2) // 150
	.pow(2).value; // 22500

console.log(val); // 22500

其他类似的方案例如pipe

function double(val) {
	return val * 2;
}

function add(val, num) {
	return val + num;
}

function minus(val, num) {
	return val - num;
}

function multi(val, num) {
	return val * num;
}

function divide(val, num) {
	return val / num;
}

function pow(val, num) {
	return Math.pow(val, num);
}

function pipe(...funcs) {
	if (funcs.length === 0) {
		return (arg) => arg;
	}

	if (funcs.length === 1) {
		return funcs[0];
	}
  
	return funcs.reduceRight(
		(a, b) =>
			(...args) =>
				a(b(...args))
	);
}

const cal = pipe(
	(val) => add(val, 10),
	(val) => minus(val, 5),
	double,
	(val) => multi(val, 10),
	(val) => divide(val, 2),
	(val) => pow(val, 2)
);

console.log(cal(10)); // 22500

5.(反)柯里化和偏函数

  • 柯里化:柯里化是将一个N元函数转换为N个一元函数(元:指的是函数参数的数量)
  • 它持续的返回一个新的函数,直到所有的参数用尽为止,然后柯里化链中最后一个函数被返回并且执行时,才会全部执行
  • 一句话:柯里化其实就是一种函数转换,多元函数转换为一元函数

举个例子

function calcSum(num1, num2, num3) {
	return num1 + num2 + num3;
}

// 柯里化
function curryCalcSum(num1) {
	return function (num2) {
		return function (num3) {
			return num1 + num2 + num3;
		};
	};
}

console.log("calcSum:", calcSum(3, 4, 5)); // calcSum: 12
console.log("curryCalcSum:", curryCalcSum(3)(4)(5)); // curryCalcSum: 12

实现通用柯里化方法

  • 接受一个需要柯里化的方法
  • 存放每次函数调用的参数
  • 参数数目不够原函数参数数目,不调用原函数,返回新的函接受下一个参数。反之调用原函数
const slice = Array.prototype.slice;
const curry = function (fn, length) {
	// 截取从传入的第三个参数起(args已经变为数组了)
	const args = slice.call(arguments, 2);
	return _curry.apply(this, [fn, length || fn.length].concat(args));
};

function _curry(fn, len) {
	const oArgs = slice.call(arguments, 2);
	return function () {
		const args = oArgs.concat(slice.call(arguments));
		if (args.length >= len) {
			return fn.apply(this, args);
		} else {
			return _curry.apply(this, [fn, len].concat(args));
		}
	};
}

//使用
function calcSum() {
	return [...arguments].reduce((pre, value) => {
		return pre + value;
	}, 0);
}

// 转换curry函数,需要传入三个参数,首先先传入1
const calcSumCurry = curry(calcSum, 3, 1);

console.log(calcSumCurry(4, 5));
console.log(calcSumCurry(4)(5));

上面的方法calcSumCurry函数如果少传递参数显然不会得到我们想要结果,我们改进一下

const slice = Array.prototype.slice;
const curry = function (fn, length) {
	const args = slice.call(arguments, 2);
	return _curry.apply(this, [fn, length || fn.length].concat(args));
};

function _curry(fn, len) {
	const oArgs = slice.call(arguments, 2);
	return function () {
		const args = oArgs.concat(slice.call(arguments));
    // 当我们参数没有传递完全时
		if (arguments.length === 0) {
			if (args.length >= len) {
				return fn.apply(this, args);
			}
			return console.warn("curry:参数长度不足");
		} else {
			return _curry.apply(this, [fn, len].concat(args));
		}
	};
}

function calcSum() {
	return [...arguments].reduce((pre, value) => {
		return pre + value;
	}, 0);
}

const fn = curry(calcSum, 5);
console.log("执行添加:", fn(2, 3)(5)());
console.log("手动调用:", fn());

柯里化作用

  • 参数复用,逻辑复用
  • 延迟计算/执行
function log(logLevel, msg) {
	console.log(`${logLevel}:${msg}:::${Date.now()}`);
}

//柯里化log 方法
const curryLog = curry(log);

const debugLog = curryLog("debug");

const errLog = curryLog("error");

//复用参数debug
debugLog("testDebug1");
debugLog("testDebug2");

//复用参数error
errLog("testError1");
errLog("testError2");

偏函数

  • 偏函数就是固定一部分参数,然后产生更小单元的函数
  • 简单理解就是:分为两次传递参数
function partial(fn) {
	const args = [].slice.call(arguments, 1);
	return function () {
		const newArgs = args.concat([].slice.call(arguments));
		return fn.apply(this, newArgs);
	};
}

function calcSum(num1, num2, num3) {
	return num1 + num2 + num3;
}
const pCalcSum = partial(calcSum, 10);

console.log(pCalcSum(11, 12)); // 33

偏函数与柯里化的区别

  • 柯里化是将一个多参数转换为单参数的函数,将一个N元函数转换为N个一元函数
  • 偏函数是固定一部分参数(一个或者多个参数),将一个N元函数转换成一个N-X函数

反柯里化

  • 反柯里化的作用就是扩大适用性,使原来作为特定对象所拥有的功能的函数可以被任意对象所用
  • 非我之物,为我所用(拿来主义)
// 反柯里化实现一
function unCurry(fn) {
	return function (context) {
		return fn.apply(context, Array.prototype.slice.call(arguments, 1));
	};
}

// 反柯里化实现二
Function.prototype.unCurry = function () {
	var self = this;
	return function () {
		return Function.prototype.call.apply(self, arguments);
	};
};

// 反柯里化实现三
Function.prototype.unCurry = function () {
	return this.call.bind(this);
};

// 反柯里化实现四
Function.prototype.unCurry = function () {
	return (...args) => this.call(...args);
};

// 不使用反柯里化
Object.prototype.toString.call({});

// 使用反柯里化
const toString = unCurry(Object.prototype.toString);
toString({});
toString(() => {});
toString(1);

使用场景:

  • 借用数组方法
  • 复制数组
  • 发送事件
// 借用数组方法
const push = Array.prototype.push.unCurry();
const obj = {};
push(obj, 666, 999);
console.log(obj); // { '0': 666, '1': 999, length: 2 }

// 复制数组
const clone = Array.prototype.slice.unCurry();
const a = [1, 2, 3];
const b = clone(a);
console.log(a, b, a === b); // [ 1, 2, 3 ] [ 1, 2, 3 ] false

// 发送事件
const dispatch = EventTarget.prototype.dispatchEvent.unCurry();
window.addEventListener("event-x", (ev) => {
	console.log("event-x", ev.detail); // event-x ok
});
dispatch(window, new CustomEvent("event-x", { detail: "ok" }));

6.动态解析和执行函数

eval

  • 功能:会将传入的字符串当做 JavaScript 代码进行执行
  • 语法:eval(string)
console.log(eval("2+2")); // 4

使用场景

// 计时器, 浏览器中执行
// setTimeout('console.log("setTimeout:", Date.now())', 1000);
// setInterval('console.log("setInterval", Date.now())', 5000);

// JSON字符串转对象
const jsonStr = `{a:1, b:1}`;
const obj = eval("(" + jsonStr + ")");
console.log("eval json:", obj, typeof obj);

// 生成函数
const sumAdd = eval(`(function add(num1, num2){
    return num1 + num2
}
)`);
console.log("sumAdd:", sumAdd(10, 20));

// 数字数组相加
const arr = [1, 2, 3, 7, 9];
const r = eval(arr.join("+"));
console.log("数组相加:", r);

// 获取全局对象
const globalThis = (function () {
	return (void 0, eval)("this");
})();

console.log("globalThis:", globalThis);

注意事项

  • 安全性(服务端返回的content-security-policy无unsafe-eval则不允许使用)
  • 调试困难(只能在其内部console或debugger)
  • 性能低
  • 过于神秘,不好把握(直接调用,)
  • 可读性可维护性也比较差

性能问题

const count = 1000000;
function test1() {
	console.time("sum1");
	for (let i = 0; i < count; i++) {
		i + i + 1;
	}
	console.timeEnd("sum1");
}

function test2() {
	console.time("sum2");
	for (let i = 0; i < count; i++) {
		eval(`i + i + 1`);
	}
	console.timeEnd("sum2");
}

test1();
test2();

执行差异:

前端灵魂~,函数式编程,也是成为JS高手最不可或缺的一块技能版图

eval直接调用和间接调用

调用方式作用域是否是严格模式
直接调用正常的作用域链继承当前
间接调用只有全局作用域非严格模式

直接调用,除开下面场景其他则是间接调用

  • eval
  • (eval)
  • eval = window.eval
  • { eval } = window
  • with({eval})
const name = "全局的name";
const log = console.log;

// 直接调用
function test1() {
  const name = "local的name";
  log(eval("name"));
}

// 直接分组
function test2() {
  const name = "local的name";
  log(eval("name"));
}

// 直接复制,不修改名字
function test3() {
  const name = "local的name";

  const eval = window.eval;
  log(eval("name"));
}

// 解构不修改名字
function test4() {
  const name = "local的name";

  // 切记,不能修改名字
  const { eval } = window;
  log(eval("name"));
}

// with
function test5() {
  const name = "local的name";
  with ({ eval }) {
    log(eval("name"));
  }
}

test1();
test2();
test3();
test4();
test5();

执行结果:

前端灵魂~,函数式编程,也是成为JS高手最不可或缺的一块技能版图

间接调用

const name = "全局的name";
const log = console.log;

// ,分组
function test1() {
  const name = "local的name";
  log((0, eval)("name"));
}

// 直接复制,修改名字
function test2() {
  const name = "local的name";

  const eval = window.eval;
  const eval2 = eval;
  log(eval2("name"));
}

// 解构修改名字
function test3() {
  const name = "local的name";

  // 切记,不能修改名字
  const { eval: eval2 } = window;
  log(eval2("name"));
}

test1();
test2();
test3();

执行结果:

前端灵魂~,函数式编程,也是成为JS高手最不可或缺的一块技能版图

new Function

经典案例

  • webpack的事件通知系统 tapable
  • fast-json-stringify

注意事项

  • new Function默认是基于全局环境创建
  • 方法的 name 属性是 'anonymous'

经典应用

获取全局this

const globalObj = new Function("return this")();

console.log(globalObj === global);

在线代码编辑器

<style>
  textarea {
    overflow-y: auto;
    font-size: 22px;
  }
</style>
</head>

<body>
  <div><button type="button" id="btn">运行</button></div>

  <textarea id="code" rows="30" cols="80"></textarea>
  <div id="result"></div>

  <script>
    const scriptStr = `
                    function sum(num1, num2){
                        return num1+ num2
                    }
                    return sum(10,20)
                `;

    const codeEl = document.getElementById("code");
    const resultEl = document.getElementById("result");
    const btnEl = document.getElementById("btn");

    codeEl.value = scriptStr;

    function createFun(body) {
      return new Function(body);
    }

    btnEl.addEventListener("click", () => {
      const fn = createFun(codeEl.value);
      const result = fn();
      resultEl.innerHTML = result;
    });
  </script>
</body>

模板引擎

<body>
  <div id="template">
    <div>名字:${name}</div>
    <div>年龄:${age}</div>
    <div>性别:${sex}</div>
    <div>属性:${c.b}</div>
    <div>商品:${products.join(",")}</div>
  </div>

  <script>
    function parse(source, data) {
      return new Function(
        "data",
        `
                with(data){
                    return \`${source}\`
                }
            `
      )(data);
    }

    const result = parse(template.innerHTML, {
      name: "云牧",
      age: 18,
      sex: "男",
      products: ["杯子", "瓜子"],
      c: {
        b: "靓仔",
      },
    });

    template.innerHTML = result;
  </script>
</body>

解析后如下:

前端灵魂~,函数式编程,也是成为JS高手最不可或缺的一块技能版图

动态执行异步代码

const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor;

function createAsyncFun(...args) {
	return new AsyncFunction(...args);
}

const fn = createAsyncFun(`
    const res = await fetch("/");
    console.log("res:");
    return res.text();
`);

fn().then((res) => {
		console.log("res:", res);
	})
	.catch((err) => {
		console.log("err:", err);
	});

7.this

  • 执行上下文(global function 或 eval ) 的一个属性
  • 在非严格模式下,总是指向一个对象
  • 严格模式下可以是任意值

this的绑定规则

  • 默认绑定
  • 显示绑定
  • 隐式绑定
  • new
  • 箭头函数

默认绑定

默认绑定非严格模式

  • 浏览器:this指向 window 对象
  • nodejs:this指向 global 对象

默认绑定严格模式

  • undefined
  • undefined

隐式绑定

  • 作为某个对象的属性被调用的时候,指向调用者本身
var name = "哈士奇";
function getName(){
    console.log("this:", this)
  	return this.name;
}
const person1 = {
	name: "person1的name",
  getName,
  person2: {
  	name: "person2的name",
    getName
  }
}

console.log(person1.getName()); // person1的name
console.log(person1.person2.getName()); // person2的name
  • DOM事件函数,绑定函数内部this指向其绑定的DOM
btn.addEventListener("click", function () {
  console.log("this:btn", this); // btn
});

const request = new XMLHttpRequest();
request.open("GET", "./");
request.send();
request.onloadend = function () {
  console.log("this:XMLHttpRequest", this); // XMLHttpRequest
};

显示绑定

  • Function.prototype.call
  • Function.prototype.apply
  • Function.prototype.bind
  • 属性绑定符
const obj = { name: "张三" };

function logName() {
	console.log(this.name, this);
}

// call和apply
logName.call(obj); // 张三 { name: '张三' }
logName.apply(obj); // 张三 { name: '张三' } 
//  非严格模式下,等同于默认绑定规则
console.log(getName.call(null));
console.log(getName.call(undefined));

// bind
const bindLogName = logName.bind(obj);
bindLogName(); // 张三 { name: '张三' }
console.log(getName.bind(person1).bind(person2)()); // 多次绑定,第一次为主

function add(num1, num2, num3, num4) {
	return num1 + num2 + num3 + num4;
}
const add2 = add.bind(null, 10, 20); // 先传入部分参数,后续再补上
console.log(add2(30, 40)); // 100

// 属性绑定符,需要babel转译
function logName() {
	console.log(this.name, this);
}

({ name: "123" }::logName()); //等同于 logName.call({name:"123"})
obj::getPerson()::getName(); // 可以连续调用

new

  • 实例化一个函数或者ES6的class
function Person(name) {
	this.name = name;

	this.getName = function () {
		return this.name;
	};
}

const person = new Person("二哈"); // Person构造函数内部的this指向person实例
console.log(person.getName());

对于构造函数的返回值

  • return 非对象,实际返回构造函数隐式创建的对象
  • return 对象,实际返回该对象
function MyObject() {
	this.name = "myObject";
}

function MyObject2() {
	this.name = "myObject";
	return {
		name: "myObject2",
	};
}

function MyObject3() {
	this.name = "myObject3";
	return undefined;
}

console.log(new MyObject()); // MyObject { name: 'myObject' }
console.log(new MyObject2()); // { name: 'myObject2' }
console.log(new MyObject3()); // MyObject3 { name: 'myObject3' }

new解密

  • 创建一个空对象
  • 设置空对象的原型为构造函数的原型
  • 绑定this为之前创建的对象,执行构造函数方法
  • 如果构造函数显式返回对象类型,就直接返回该对象,反之返回第一步创建的对象
const slice = Array.prototype.slice;
function newObject(constructor) {
	const args = slice.call(arguments, 1);
	const obj = {}; // 1
	obj.__proto__ = constructor.prototype; // 2
	const res = constructor.apply(obj, args); // 3
	return res instanceof Object ? res : obj; // 4
}

箭头函数

  • 简单,没有自己this,永远指向上层作用域
  • 同样没有arguments、super、new.target
  • 适合需要匿名函数的地方
// 浏览器中执行
var name = "全局的name";
const getName = () => this.name;
console.log(getName());

const person = {
	name: "person的name",
	getName: () => this.name,
};
console.log(person.getName());

const person2 = {
	name: "person2的name",
	getPerson() {
		return {
			getName: () => this.name,
		};
	},
};
console.log(person2.getPerson().getName());

执行结果如下:

前端灵魂~,函数式编程,也是成为JS高手最不可或缺的一块技能版图

当发生嵌套,this一层层往上继承

class Person {
	constructor(name) {
		this.name = name;
	}

	getName() {
		return {
			getName2: () => ({
				getName3: () => ({
					getName4: () => this.name,
				}),
			}),
		};
	}
}

const p = new Person("person的name");
console.log(p.getName().getName2().getName3().getName4()); // person的name

箭头函数this不变,如果要变,我们不妨直接改他上层作用域的this

// 浏览器中执行
var name = "global.name";
const person = {
	name: "person.name",
	getName() {
		return () => this.name;
	},
};

console.log(person.getName()()); // person.name

console.log(person.getName.call({ name: "name" })()); // name

this绑定的优先级

  1. 箭头函数
  2. 显示绑定
  3. new
  4. 隐式绑定
  5. 默认绑定

小练习

var name = "window";
var obj = { name: "张三" };

function logName() {
	console.log(this.name);
}

function logName2() {
	"use strict";
	console.log(this.name);
}

var person = {
	name: "person",
	logName,
	logName2: () => logName(),
};

logName();
person.logName();
person.logName2();
logName.bind(obj)();
logName2();

执行结果:

前端灵魂~,函数式编程,也是成为JS高手最不可或缺的一块技能版图

锁定this

  • bind
  • 箭头函数

8.原型

  • 原型不是 JavaScript 首创
  • 借鉴 Self 语言,基于原型(prototype)的实现继承机制

解决的问题

  • 共享数据,减少空间占用,节省内存
  • 实现继承
function Person(name, age) {
	this.name = name;
	this.age = age;
	this.getName = function () {
		return this.name;
	};
	this.getAge = function () {
		return this.age;
	};
}

const person1 = new Person();
const person2 = new Person();

console.log(person1.getName === person2.getName); // false

通过原型可以共享getName及getAge方法

Person.prototype.getName = function () {
	return this.name;
};
Person.prototype.getAge = function () {
	return this.age;
};

prototype

  • 函数和class的共享属性,本质就是一个对象
const obj = {};

console.log(obj.toString());

// toString 方法是不是来自原型
console.log(obj.toString === Object.prototype.toString); // true

console.log(obj instanceof Object); // true

constructor

  • 实例的构造函数
  • 可被更改
  • 如果是普通对象,则该属性在其原型上
const obj = {};

console.log(obj.constructor === Object); // true

_proto_

  • _proto__proto_属性是一个访问器属性(一个getter函数和一个setter函数),暴露了通过它访问的对象的内部[[Prototype]](一个对象或 null)
  • 等于构造函数的原型prototype
  • 推荐使用:Object.getPrototypeof 获取
  • 注意:Object.prototype.__proto__ === null
const des = Object.getOwnPropertyDescriptor(Object.prototype, "__proto__");

console.log(des);

// __proto__ 构造函数的原型
const obj = {};
console.log(obj.__proto__ === obj.constructor.prototype); // true

// Object.getPrototypeOf 替代 __proto__
console.log(Object.getPrototypeOf(obj) === obj.__proto__); // true

原型链

function Person(name, age) {
	this.name = name;
	this.age = age;
}

Person.prototype.getName = function () {
	return this.name;
};

Person.prototype.getAge = function () {
	return this.age;
};

const person = new Person();
console.log(person.toString());

前端灵魂~,函数式编程,也是成为JS高手最不可或缺的一块技能版图

person对象有__proto__指向Person.prototypePerson.prototype是个对象,所以他自己的__proto__指向Object.prototype,这个Object.prototype对象的__proto__则指向了null

更完整的图示:

前端灵魂~,函数式编程,也是成为JS高手最不可或缺的一块技能版图

总结

  • 函数最终的本质上是对象
  • 普通对象都有constructor,指向自己的构造函数,可以被改变,不一样安全
  • 函数和classprototype.constructor指向函数自身
  • FunctionObjectRegexpError等本质是函数,Function.constructor = Function
  • 普通对象都有_proto_,其等于构造函数的原型,推荐使用Object.getPrototypeof
  • 所有普通函数的构造函数都是Function,ES6另外出现的函数种类 AsyncFunctionGeneratorFunction
  • 原型链的尽头是null:Object.prototype._proto_= null
  • Function.proto_指向 Function.prototype

小Tips

  • 普通对象的二次_proto_是 null
  • 普通函数的三次__proto__是null
  • 如果是经历过n次显式继承,被实例化的普通对象,n+3层的__proto__是null

9.组合和继承

组合(has-a)

  • 在一个类/对象内使用其他的类/对象。
  • has-a: 包含关系,体现的是整体和部分的思想
  • 黑盒复用:对象的内部细节不可见。知道怎么使用就可以了
class Logger {
	log() {
		console.log(...arguments);
	}
	error() {
		console.error(...arguments);
	}
}

class Reporter {
	constructor(logger) {
		this.logger = logger || new Logger();
	}
	report() {
		// TODO:
		this.logger.log("report");
	}
}

const reporter = new Reporter();
reporter.report(); // report

组合优点

  • 功能相对独立,松耦合
  • 扩展性好
  • 符合单一职责,复用性好
  • 支持动态组合,即程序运行中组合
  • 具备按需组装的能力

组合的缺点

  • 使用上相比继承,更加复杂一些
  • 容易产生过多的类/对象

继承( is - a 关系)

  • 继承是 is-a 的关系,比如人是动物
  • 白盒复用:你需要了解父类的实现细节,从而决定怎么重写父类的方法
class Logger {
	log() {
		console.log(...arguments);
	}
	error() {
		console.error(...arguments);
	}
}

class Reporter extends Logger {
	report() {
		// TODO:
		this.log("report");
	}
}

const reporter = new Reporter();
reporter.report(); // report

继承优点

  • 初始化简单,子类自动具备父类的能力
  • 无需显式初始化父类

继承缺点

  • 继承层级多,会导致代码混乱,可读性变差
  • 耦合紧
  • 扩展性相对组合较差

其实组合和继承的最终目的都是为了更好代码复用

多态

形成条件

  • 需要有继承关系
  • 子类重写父类的方法
  • 父类指向子类
class Animal {
	eat() {
		console.log("Animal is eating");
	}
}

class Person extends Animal {
	eat() {
		console.log("Person is eating");
	}
}

// 同一个方法看似是一个实例,输出却是不同
const animal: Animal = new Animal();
animal.eat(); // Animal is eating

const person: Animal = new Person();
person.eat(); // Person is eating

如何选择

  • 有多态的需求的时候,考虑使用继承
  • 如何有多重继承的需求,考虑使用组合
  • 既有多态又有多重继承,考虑使用继承+组合

寄生组合继承

function Animal(options) {
	this.age = options.age || 0;
	this.sex = options.sex || 1;
	this.testProperties = [1, 2, 3];
}

Animal.prototype.eat = function (something) {
	console.log("eat:", something);
};

function Person(options) {
	// 初始化父类, 独立各自的属性
	Animal.call(this, options);
	this.name = options.name || "";
}

// 设置原型
Person.prototype = Object.create(Animal.prototype);
// 修复构造函数
Person.prototype.constructor = Person;

Person.prototype.eat = function eat(something) {
	console.log(this.name, ":is eating", something);
};
Person.prototype.walk = function walk() {
	console.log(this.name, ":is waking");
};

const person = new Person({ sex: 1, age: 18, name: "小红" });
person.eat("大米"); // 小红 :is eating 大米
person.walk(); // 小红 :is waking

person.testProperties.push("4");

const person2 = new Person({ sex: 1, age: 18, name: "小红" });
// 表示testProperties是相互独立的
console.log(person2.testProperties); // [ 1, 2, 3 ]

寄生组合继承解决的问题

  • 各个实例的属性独立,不会发生修改一个实例,影响另外一个实例
  • 实例化过程中没有多余的函数调用
  • 原型上的 constructor 属性指向正确的构造函数

混合

class Logger {
	log() {
		console.log("Logger::", ...arguments);
	}
}

class Animal {
	eat() {
		console.log("Animal:: is eating");
	}
}

class Person extends Animal {
	walk() {
		console.log("Person:: is walking");
	}
}

const whiteList = ["constructor"];
function mixin(targetProto, sourceProto) {
	const keys = Object.getOwnPropertyNames(sourceProto);
	keys.forEach((k) => {
		if (whiteList.indexOf(k) <= 0) {
			targetProto[k] = sourceProto[k];
		}
	});
}

mixin(Person.prototype, Logger.prototype);

console.log(Person.prototype);
const person = new Person();
person.log("log test");  // Logger:: log test

ES6继承

  • 构造函数this使用之前,必须先调用super方法
  • 注意箭头函数形式属性和clas如何在原型上添加非函数的属性
class Animal {
	constructor(options) {
		this.age = options.age || 0;
		this.sex = options.sex || 1;
	}

	eat(something) {
		console.log("eat:", something);
	}
}
// class原型上添加非函数的属性
Animal.prototype.name = "prototype的name";

class Person extends Animal {
	// 私有变量
	#friends = [];

	constructor(options) {
		super(options);
		this.name = options.name || name;
	}
	eat(something) {
		console.log(this.name, "eat:", something);
	}
	run() {
		return `${this.name}正在跑步`;
	}
	// 此为实例上面的属性
	say = () => {
		console.log("say==", say);
	};
}

const p1 = new Person({ name: "张三" });
console.log("name:", p1.name); // name: 张三
p1.eat("鲍鱼"); // 张三 eat: 鲍鱼
// console.log("p1.friends:", p1.friends, p1.#friends);
console.log(Object.getOwnPropertyNames(p1.__proto__)); // 'constructor', 'eat', 'run'
console.log(p1.__proto__.name); // prototype的name

10.call

  • call:使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数
  • this:执行上下文的一个变量。

有人会说,这个有啥讲的我都会,真的嘛,我们看下面的代码

function a() {
	console.log(this, typeof this, "a");
}
function b() {
	console.log(this, typeof this, "b");
}
a.call.call(b, "b"); // [String: 'b'] object b
a.call.call.call(b, "b"); // [String: 'b'] object b
a.call.call.call.call(b, "b"); // [String: 'b'] object b

为什么2,3,4个call的结果一样

  • a.call(b):a被调用
  • a.call.call(b):a.call 被调用
  • a.cal.call.call(b):a.call.call 被调用

而call的函数来源于函数原型上,无论call多少次,其实都是调用一次原型上的call函数

function a() {
	console.log(this, "a");
}
function b() {
	console.log(this, "b");
}

console.log(a.call === Function.prototype.call); // true
console.log(a.call === a.call.call); // true
console.log(a.call === a.call.call.call); // true

一个函数进行 call 调用,等同于在一个对象上执行该函数

(a.call).call(b, 'b'),等于在 b 对象上调用 a.call(Function.prototype.call) 函数

b.call("b");

为什么this是String{"b"}

  • this:非严格模式下,Object包装
  • this:严格模式下,任意值(传啥是啥)

万能函数调用方法

  • Function.prototype.call.call.bind(Function.prototype.call)
  • 前提是没有锁定this哈
const person = {
	hello() {
		console.log("hello", this.name);
	},
};

const call = Function.prototype.call.call.bind(Function.prototype.call);

call(person.hello, { name: "tom" }); // hello tom

11.手写call

  • 环境识别:识别浏览器环境和Nodejs环境
  • 是否支持和处于严格模式
  • 避免副作用,即函数调用后不破坏原对象
  • 判断是不是函数

eval()

var hasStrictMode = (function () {
	"use strict";
	return this == undefined;
})();

var isStrictMode = function () {
	return this === undefined;
};

var getGlobal = function () {
	if (typeof self !== "undefined") {
		return self;
	}
	if (typeof window !== "undefined") {
		return window;
	}
	if (typeof global !== "undefined") {
		return global;
	}
	throw new Error("unable to locate global object");
};

function isFunction(fn) {
	return (
		typeof fn === "function" ||
		Object.prototype.toString.call(fn) === "[object Function]"
	);
}

function getContext(context) {
	// 是否是严格模式
	var isStrict = isStrictMode();
	// 没有严格模式,或者有严格模式但不处于严格模式
	if (!hasStrictMode || (hasStrictMode && !isStrict)) {
		return context === null || context === void 0
			? getGlobal()
			: Object(context);
	}

	// 严格模式下, 妥协方案
	return Object(context);
}

Function.prototype.call = function (context) {
	// 不可以被调用
	if (!isFunction(this)) {
		throw new TypeError(this + " is not a function");
	}

	// 获取上下文
	var ctx = getContext(context);

	// 更为稳妥的是创建唯一ID, 以及检查是否有重名
	var propertyName = "__fn__" + Math.random() + "_" + new Date().getTime();
	var originVal;
	var hasOriginVal = isFunction(ctx.hasOwnProperty)
		? ctx.hasOwnProperty(propertyName)
		: false;
	if (hasOriginVal) {
		originVal = ctx[propertyName];
	}

	ctx[propertyName] = this;

	// 采用string拼接
	var argStr = "";
	var len = arguments.length;
	for (var i = 1; i < len; i++) {
		argStr += i === len - 1 ? "arguments[" + i + "]" : "arguments[" + i + "],";
	}
	var r = eval('ctx["' + propertyName + '"](' + argStr + ")");

	// 还原现场
	if (hasOriginVal) {
		ctx[propertyName] = originVal;
	} else {
		delete ctx[propertyName];
	}

	return r;
};

// 测试
function log() {
	console.log("name:", this.name);
}

log.call({ name: "name" });

new Function()

var hasStrictMode = (function () {
	"use strict";
	return this == undefined;
})();

var isStrictMode = function () {
	return this === undefined;
};

var getGlobal = function () {
	if (typeof self !== "undefined") {
		return self;
	}
	if (typeof window !== "undefined") {
		return window;
	}
	if (typeof global !== "undefined") {
		return global;
	}
	throw new Error("unable to locate global object");
};

function isFunction(fn) {
	return (
		typeof fn === "function" ||
		Object.prototype.toString.call(fn) === "[object Function]"
	);
}

function getContext(context) {
	var isStrict = isStrictMode();

	if (!hasStrictMode || (hasStrictMode && !isStrict)) {
		return context === null || context === void 0
			? getGlobal()
			: Object(context);
	}
	// 严格模式下, 妥协方案
	return Object(context);
}

function createFun(argsLength) {
	// return ctx[propertyName](arg1, arg2, arg3,...)
	// 拼接函数
	var code = "return ctx[propertyName](";

	// 拼接参数, 第二个起是参数
	for (var i = 0; i < argsLength; i++) {
		if (i > 0) {
			code += ",";
		}
		code += "args[" + i + "]";
	}
	code += ")";

	return new Function("ctx", "propertyName", "args", code);
}

Function.prototype.call = function (context) {
	// 不可以被调用
	if (typeof this !== "function") {
		throw new TypeError(this + " is not a function");
	}

	// 获取上下文
	var ctx = getContext(context);

	// 更为稳妥的是创建唯一ID, 以及检查是否有重名
	var propertyName = "__fn__" + Math.random() + "_" + new Date().getTime();
	var originVal;
	var hasOriginVal = isFunction(ctx.hasOwnProperty)
		? ctx.hasOwnProperty(propertyName)
		: false;
	if (hasOriginVal) {
		originVal = ctx[propertyName];
	}

	ctx[propertyName] = this;

	var argArr = [];
	var len = arguments.length;
	for (var i = 1; i < len; i++) {
		argArr[i - 1] = arguments[i];
	}

	var r = createFun(len - 1)(ctx, propertyName, argArr);

	// 还原现场
	if (hasOriginVal) {
		ctx[propertyName] = originVal;
	} else {
		delete ctx[propertyName];
	}
	return r;
};

function getName() {
	console.log(this.name, arguments[0], arguments[1]);
}

getName.call({ name: "哈哈" }, 1, 2); // 哈哈 1 2

函数还有具有非常多的基础知识,比如作用域(链)、闭包、IIFE、预解析,纯函数,高阶函数,副作用等,对这些不熟悉的可以瞅瞅我这些之前的基础文章,查漏补缺,另外本文有什么错误烦请指出,大家共同进步。