前言
函数之于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();
复制代码
执行差异:
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();
复制代码
执行结果:
间接调用
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();
复制代码
执行结果:
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>
复制代码
解析后如下:
动态执行异步代码
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());
复制代码
执行结果如下:
当发生嵌套,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绑定的优先级
- 箭头函数
- 显示绑定
- new
- 隐式绑定
- 默认绑定
小练习
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();
复制代码
执行结果:
锁定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());
复制代码
person对象有__proto__
指向Person.prototype
,Person.prototype
是个对象,所以他自己的__proto__
指向Object.prototype
,这个Object.prototype
对象的__proto__
则指向了null
更完整的图示:
总结
- 函数最终的本质上是对象
- 普通对象都有
constructor
,指向自己的构造函数,可以被改变,不一样安全 - 函数和
class
的prototype.constructor
指向函数自身 Function
、Object
、Regexp
、Error
等本质是函数,Function.constructor = Function
- 普通对象都有
_proto
_,其等于构造函数的原型,推荐使用Object.getPrototypeof
- 所有普通函数的构造函数都是
Function
,ES6另外出现的函数种类AsyncFunction
,GeneratorFunction
- 原型链的尽头是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、预解析,纯函数,高阶函数,副作用等,对这些不熟悉的可以瞅瞅我这些之前的基础文章,查漏补缺,另外本文有什么错误烦请指出,大家共同进步。